@otmc/ui 0.1.5 → 0.1.7

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/editor.js ADDED
@@ -0,0 +1,4807 @@
1
+ import {
2
+ BACKEND_URL,
3
+ NotificationCenter_default,
4
+ apiClient,
5
+ apiUtils,
6
+ copyToClipboard,
7
+ debug,
8
+ getBackendUrl,
9
+ instructionsApi,
10
+ llmApi,
11
+ resolveColorScheme,
12
+ setAccessTokenGetter,
13
+ setBackendUrl,
14
+ useTheme
15
+ } from "./chunk-BGUL4HIW.js";
16
+
17
+ // src/editor/MarkdownEditor.tsx
18
+ import { memo as memo6, useState as useState7, useCallback as useCallback8, useMemo as useMemo2, useEffect as useEffect6 } from "react";
19
+ import { useMantineColorScheme as useMantineColorScheme3, useMantineTheme as useMantineTheme6, Card } from "@mantine/core";
20
+
21
+ // src/editor/hooks/useEditor.ts
22
+ import { useEditor } from "@tiptap/react";
23
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
24
+
25
+ // src/editor/hooks/markdownSerializer.ts
26
+ var serializeNode = (node, useHtmlForMarks = false) => {
27
+ if (!node) return "";
28
+ if (node.isText) {
29
+ let text = node.text || "";
30
+ const marks = node.marks || [];
31
+ const bold = marks.some((m) => m.type.name === "bold");
32
+ const italic = marks.some((m) => m.type.name === "italic");
33
+ const code = marks.some((m) => m.type.name === "code");
34
+ const link = marks.find((m) => m.type.name === "link");
35
+ const textStyle = marks.find((m) => m.type.name === "textStyle");
36
+ const color = textStyle?.attrs?.color;
37
+ const bgColorMark = marks.find((m) => m.type.name === "backgroundColor");
38
+ const backgroundColor = bgColorMark?.attrs?.color;
39
+ if (code) {
40
+ text = `\`${text}\``;
41
+ } else {
42
+ if (useHtmlForMarks) {
43
+ if (bold) text = `<strong>${text}</strong>`;
44
+ if (italic) text = `<em>${text}</em>`;
45
+ if (color) text = `<span style="color: ${color}">${text}</span>`;
46
+ if (backgroundColor) text = `<span data-bg-color="${backgroundColor}">${text}</span>`;
47
+ } else {
48
+ if (bold) text = `**${text}**`;
49
+ if (italic) text = `*${text}*`;
50
+ }
51
+ if (link) text = `[${text}](${link.attrs.href})`;
52
+ if (!useHtmlForMarks && color) {
53
+ text = `<span style="color: ${color}">${text}</span>`;
54
+ }
55
+ if (!useHtmlForMarks && backgroundColor) {
56
+ text = `<span data-bg-color="${backgroundColor}">${text}</span>`;
57
+ }
58
+ }
59
+ return text;
60
+ }
61
+ let content = "";
62
+ if (node.content && node.content.content) {
63
+ content = node.content.content.map((child) => serializeNode(child)).join("");
64
+ }
65
+ switch (node.type.name) {
66
+ case "text":
67
+ return content;
68
+ case "paragraph": {
69
+ const backgroundColor = node.attrs?.backgroundColor;
70
+ if (backgroundColor) {
71
+ const content2 = node.content.content.map((child) => serializeNode(child, true)).join("");
72
+ return `<p style="background-color: ${backgroundColor}">${content2}</p>`;
73
+ }
74
+ return content;
75
+ }
76
+ case "heading": {
77
+ const level = node.attrs?.level || 1;
78
+ const backgroundColor = node.attrs?.backgroundColor;
79
+ if (backgroundColor) {
80
+ const content2 = node.content.content.map((child) => serializeNode(child, true)).join("");
81
+ return `<h${level} style="background-color: ${backgroundColor}">${content2.trim()}</h${level}>`;
82
+ }
83
+ const prefix = "#".repeat(level);
84
+ return `${prefix} ${content.trim()}`;
85
+ }
86
+ case "bulletList": {
87
+ const items = [];
88
+ for (const item of node.content?.content || []) {
89
+ const itemText = serializeNode(item).trim();
90
+ items.push(`- ${itemText}`);
91
+ }
92
+ return items.join("\n");
93
+ }
94
+ case "orderedList": {
95
+ const items = [];
96
+ let index = node.attrs?.start || 1;
97
+ for (const item of node.content?.content || []) {
98
+ const itemText = serializeNode(item).trim();
99
+ items.push(`${index}. ${itemText}`);
100
+ index++;
101
+ }
102
+ return items.join("\n");
103
+ }
104
+ case "taskItem": {
105
+ const checked = node.attrs?.checked ? "[x]" : "[ ]";
106
+ const itemText = content.trim();
107
+ return `- ${checked} ${itemText}`;
108
+ }
109
+ case "taskList": {
110
+ const items = [];
111
+ for (const item of node.content?.content || []) {
112
+ const itemText = serializeNode(item).trim();
113
+ items.push(itemText);
114
+ }
115
+ return items.join("\n");
116
+ }
117
+ case "blockquote": {
118
+ const lines = content.trim().split("\n");
119
+ return lines.map((line) => `> ${line}`).join("\n");
120
+ }
121
+ case "codeBlock": {
122
+ const language = node.attrs?.language || "";
123
+ return `\`\`\`${language}
124
+ ${content}
125
+ \`\`\``;
126
+ }
127
+ case "horizontalRule":
128
+ return "---";
129
+ case "hardBreak":
130
+ return "\n";
131
+ case "image":
132
+ const src = node.attrs?.src || "";
133
+ const alt = node.attrs?.alt || "";
134
+ return `![${alt}](${src})`;
135
+ case "notice": {
136
+ const variant = node.attrs?.variant || "info";
137
+ const title = node.attrs?.title || "Notice";
138
+ const backgroundColor = node.attrs?.backgroundColor;
139
+ const contentText = node.content.content.map((child) => serializeNode(child, true)).join("").trim();
140
+ let html = `<div data-type="notice" data-variant="${variant}" data-title="${title}"`;
141
+ if (backgroundColor) {
142
+ html += ` style="background-color: ${backgroundColor}"`;
143
+ }
144
+ html += `>${contentText}</div>`;
145
+ return html;
146
+ }
147
+ case "table": {
148
+ const rows = [];
149
+ let headerIndex = 0;
150
+ for (const row of node.content?.content || []) {
151
+ const cells = [];
152
+ for (const cell of row.content?.content || []) {
153
+ cells.push(serializeNode(cell).trim());
154
+ }
155
+ rows.push(`| ${cells.join(" | ")} |`);
156
+ if (headerIndex === 0) {
157
+ const separators = cells.map(() => "---");
158
+ rows.push(`| ${separators.join(" | ")} |`);
159
+ headerIndex++;
160
+ }
161
+ }
162
+ return rows.join("\n");
163
+ }
164
+ default:
165
+ return content;
166
+ }
167
+ };
168
+ var serializeToMarkdown = (editor) => {
169
+ if (!editor) return "";
170
+ const nodes = editor.state.doc.content.content;
171
+ const results = [];
172
+ for (const node of nodes) {
173
+ const serialized = serializeNode(node);
174
+ if (serialized || node.type.name === "paragraph") {
175
+ results.push(serialized);
176
+ }
177
+ }
178
+ return results.join("\n\n");
179
+ };
180
+ var serializeSelectionToMarkdown = (editor, from, to) => {
181
+ if (!editor) return "";
182
+ const { doc } = editor.state;
183
+ const $from = doc.resolve(from);
184
+ for (let depth = $from.depth; depth > 0; depth--) {
185
+ const node = $from.node(depth);
186
+ if (["bulletList", "orderedList", "taskList"].includes(node.type.name)) {
187
+ return serializeNode(node);
188
+ }
189
+ }
190
+ const selectionNode = doc.slice(from, to);
191
+ const results = [];
192
+ selectionNode.content.content.forEach((node) => {
193
+ const serialized = serializeNode(node);
194
+ if (serialized || node.type.name === "paragraph") {
195
+ results.push(serialized);
196
+ }
197
+ });
198
+ return results.join("\n\n");
199
+ };
200
+
201
+ // src/editor/hooks/useEditor.ts
202
+ import StarterKit from "@tiptap/starter-kit";
203
+
204
+ // src/editor/extensions/CodeBlockExtension.ts
205
+ import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight";
206
+ import { ReactNodeViewRenderer } from "@tiptap/react";
207
+ import { createLowlight, common } from "lowlight";
208
+
209
+ // src/editor/blocks/CodeBlock.tsx
210
+ import { useState, memo, useMemo, useCallback } from "react";
211
+ import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
212
+ import { ActionIcon, Box as Box2, Text, useMantineTheme, Select } from "@mantine/core";
213
+ import { IconCheck, IconCopy, IconCode, IconChevronDown } from "@tabler/icons-react";
214
+
215
+ // src/editor/components/BlockWrapper.tsx
216
+ import { Box } from "@mantine/core";
217
+ import { jsx } from "react/jsx-runtime";
218
+ function BlockWrapper({ children, selected, ...props }) {
219
+ return /* @__PURE__ */ jsx(
220
+ Box,
221
+ {
222
+ px: 0,
223
+ py: 4,
224
+ style: {
225
+ borderRadius: 8,
226
+ backgroundColor: selected ? "var(--mantine-color-blue-light)" : "transparent",
227
+ transition: "background-color 0.15s ease"
228
+ },
229
+ ...props,
230
+ children
231
+ }
232
+ );
233
+ }
234
+
235
+ // src/editor/blocks/CodeBlock.tsx
236
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
237
+ var COMMON_LANGUAGES = [
238
+ { value: "", label: "Plain Text" },
239
+ { value: "bash", label: "Bash" },
240
+ { value: "powershell", label: "PowerShell" },
241
+ { value: "cpp", label: "C++" },
242
+ { value: "c", label: "C" },
243
+ { value: "go", label: "Golang" },
244
+ { value: "python", label: "Python" },
245
+ { value: "rust", label: "Rust" },
246
+ { value: "csharp", label: "C#" },
247
+ { value: "java", label: "Java" },
248
+ { value: "javascript", label: "JavaScript" },
249
+ { value: "typescript", label: "TypeScript" },
250
+ { value: "php", label: "PHP" },
251
+ { value: "ruby", label: "Ruby" },
252
+ { value: "swift", label: "Swift" },
253
+ { value: "kotlin", label: "Kotlin" },
254
+ { value: "sql", label: "SQL" },
255
+ { value: "html", label: "HTML" },
256
+ { value: "css", label: "CSS" },
257
+ { value: "json", label: "JSON" },
258
+ { value: "xml", label: "XML" },
259
+ { value: "yaml", label: "YAML" }
260
+ ];
261
+ var CodeBlockHeader = memo(
262
+ ({
263
+ language,
264
+ isDark,
265
+ theme
266
+ }) => {
267
+ if (!language) return null;
268
+ return /* @__PURE__ */ jsxs(
269
+ Text,
270
+ {
271
+ pos: "absolute",
272
+ top: 8,
273
+ left: 18,
274
+ size: "xs",
275
+ c: isDark ? theme.colors.dark[2] : theme.colors.gray[6],
276
+ style: {
277
+ zIndex: 1,
278
+ textTransform: "capitalize",
279
+ fontSize: 12,
280
+ fontWeight: 600,
281
+ display: "flex",
282
+ alignItems: "center",
283
+ pointerEvents: "none",
284
+ gap: 4
285
+ },
286
+ children: [
287
+ /* @__PURE__ */ jsx2(IconCode, { size: 14 }),
288
+ language
289
+ ]
290
+ }
291
+ );
292
+ }
293
+ );
294
+ CodeBlockHeader.displayName = "CodeBlockHeader";
295
+ function CodeBlock({ node, updateAttributes, selected }) {
296
+ const [copied, setCopied] = useState(false);
297
+ const { currentTheme } = useTheme();
298
+ const theme = useMantineTheme();
299
+ const isDark = currentTheme === "dark";
300
+ const language = node.attrs?.language || "";
301
+ const handleCopy = useCallback(async () => {
302
+ const success = await copyToClipboard(node.textContent);
303
+ if (success) {
304
+ setCopied(true);
305
+ setTimeout(() => setCopied(false), 1500);
306
+ }
307
+ }, [node.textContent]);
308
+ const handleLanguageChange = useCallback(
309
+ (value) => {
310
+ updateAttributes({ language: value || "" });
311
+ },
312
+ [updateAttributes]
313
+ );
314
+ const preStyle = useMemo(
315
+ () => ({
316
+ border: `1px solid ${isDark ? theme.colors.dark[5] : theme.colors.gray[3]}`,
317
+ borderRadius: 10,
318
+ padding: "8px 12px",
319
+ overflow: "auto",
320
+ cursor: "text",
321
+ background: isDark ? "#0d1117" : "#e8f1fa",
322
+ margin: 0,
323
+ position: "relative"
324
+ }),
325
+ [isDark, theme]
326
+ );
327
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, { className: "code-block-wrapper", children: [
328
+ /* @__PURE__ */ jsx2(BlockWrapper, { selected, children: /* @__PURE__ */ jsxs(
329
+ Box2,
330
+ {
331
+ pos: "relative",
332
+ bg: isDark ? theme.colors.dark[8] : theme.colors.gray[0],
333
+ style: { borderRadius: 10 },
334
+ children: [
335
+ /* @__PURE__ */ jsx2(
336
+ Box2,
337
+ {
338
+ pos: "absolute",
339
+ top: 5,
340
+ right: 50,
341
+ style: {
342
+ zIndex: 2,
343
+ display: "flex",
344
+ alignItems: "center",
345
+ gap: 6
346
+ },
347
+ children: /* @__PURE__ */ jsx2(
348
+ Select,
349
+ {
350
+ value: language,
351
+ onChange: handleLanguageChange,
352
+ data: COMMON_LANGUAGES,
353
+ size: "xs",
354
+ w: 115,
355
+ comboboxProps: {
356
+ position: "bottom-end",
357
+ withinPortal: true
358
+ },
359
+ rightSection: /* @__PURE__ */ jsx2(IconChevronDown, { size: 14 }),
360
+ leftSection: /* @__PURE__ */ jsx2(IconCode, { size: 14, color: isDark ? theme.colors.dark[2] : theme.colors.gray[6] }),
361
+ placeholder: "Language",
362
+ clearable: false,
363
+ searchable: false,
364
+ styles: {
365
+ input: {
366
+ background: isDark ? "#292a2b" : "#f6f8fa",
367
+ borderColor: isDark ? theme.colors.dark[5] : theme.colors.gray[3],
368
+ height: 28,
369
+ minHeight: 28,
370
+ borderRadius: 12
371
+ },
372
+ dropdown: {
373
+ borderRadius: 12
374
+ }
375
+ }
376
+ }
377
+ )
378
+ }
379
+ ),
380
+ /* @__PURE__ */ jsx2(
381
+ ActionIcon,
382
+ {
383
+ pos: "absolute",
384
+ top: 5,
385
+ right: 5,
386
+ color: isDark ? "dark" : "gray",
387
+ variant: "subtle",
388
+ radius: "md",
389
+ onClick: handleCopy,
390
+ style: { zIndex: 2 },
391
+ title: copied ? "Copied!" : "Copy code",
392
+ children: copied ? /* @__PURE__ */ jsx2(IconCheck, { color: "green", size: 16 }) : /* @__PURE__ */ jsx2(IconCopy, { size: 16 })
393
+ }
394
+ ),
395
+ /* @__PURE__ */ jsx2("pre", { style: preStyle, className: isDark ? "hljs-dark" : "hljs-light", children: /* @__PURE__ */ jsx2("code", { children: /* @__PURE__ */ jsx2(NodeViewContent, {}) }) })
396
+ ]
397
+ }
398
+ ) }),
399
+ /* @__PURE__ */ jsx2("style", { children: `
400
+ .code-block-wrapper .ProseMirror-trailingBreak {
401
+ display: none;
402
+ }
403
+ .hljs-light {
404
+ color: #24292f;
405
+ background: #f3faff !important;
406
+ }
407
+ .hljs-light .hljs-doctag,
408
+ .hljs-light .hljs-keyword,
409
+ .hljs-light .hljs-meta .hljs-keyword,
410
+ .hljs-light .hljs-template-tag,
411
+ .hljs-light .hljs-template-variable,
412
+ .hljs-light .hljs-type,
413
+ .hljs-light .hljs-variable.language_ {
414
+ color: #d73a49;
415
+ }
416
+ .hljs-light .hljs-title,
417
+ .hljs-light .hljs-title.class_,
418
+ .hljs-light .hljs-title.class_.inherited__,
419
+ .hljs-light .hljs-title.function_ {
420
+ color: #6f42c1;
421
+ }
422
+ .hljs-light .hljs-attr,
423
+ .hljs-light .hljs-attribute,
424
+ .hljs-light .hljs-literal,
425
+ .hljs-light .hljs-meta,
426
+ .hljs-light .hljs-number,
427
+ .hljs-light .hljs-operator,
428
+ .hljs-light .hljs-selector-attr,
429
+ .hljs-light .hljs-selector-class,
430
+ .hljs-light .hljs-selector-id,
431
+ .hljs-light .hljs-variable {
432
+ color: #005cc5;
433
+ }
434
+ .hljs-light .hljs-meta .hljs-string,
435
+ .hljs-light .hljs-regexp,
436
+ .hljs-light .hljs-string {
437
+ color: #0a3069;
438
+ }
439
+ .hljs-light .hljs-built_in,
440
+ .hljs-light .hljs-symbol {
441
+ color: #b56b3a;
442
+ }
443
+ .hljs-light .hljs-code,
444
+ .hljs-light .hljs-comment,
445
+ .hljs-light .hljs-formula {
446
+ color: #6a737d;
447
+ }
448
+ .hljs-light .hljs-name,
449
+ .hljs-light .hljs-quote,
450
+ .hljs-light .hljs-selector-pseudo,
451
+ .hljs-light .hljs-selector-tag {
452
+ color: #22863a;
453
+ }
454
+ .hljs-light .hljs-subst {
455
+ color: #24292f;
456
+ }
457
+ .hljs-light .hljs-section {
458
+ color: #005cc5;
459
+ font-weight: 700;
460
+ }
461
+ .hljs-light .hljs-bullet {
462
+ color: #735c0f;
463
+ }
464
+ .hljs-light .hljs-emphasis {
465
+ color: #24292f;
466
+ font-style: italic;
467
+ }
468
+ .hljs-light .hljs-strong {
469
+ color: #24292f;
470
+ font-weight: 700;
471
+ }
472
+
473
+ .hljs-dark {
474
+ color: #c9d1d9;
475
+ background: #292a2b !important;
476
+ }
477
+ .hljs-dark .hljs-doctag,
478
+ .hljs-dark .hljs-keyword,
479
+ .hljs-dark .hljs-meta .hljs-keyword,
480
+ .hljs-dark .hljs-template-tag,
481
+ .hljs-dark .hljs-template-variable,
482
+ .hljs-dark .hljs-type,
483
+ .hljs-dark .hljs-variable.language_ {
484
+ color: #ff7b72;
485
+ }
486
+ .hljs-dark .hljs-title,
487
+ .hljs-dark .hljs-title.class_,
488
+ .hljs-dark .hljs-title.class_.inherited__,
489
+ .hljs-dark .hljs-title.function_ {
490
+ color: #d2a8ff;
491
+ }
492
+ .hljs-dark .hljs-attr,
493
+ .hljs-dark .hljs-attribute,
494
+ .hljs-dark .hljs-literal,
495
+ .hljs-dark .hljs-meta,
496
+ .hljs-dark .hljs-number,
497
+ .hljs-dark .hljs-operator,
498
+ .hljs-dark .hljs-selector-attr,
499
+ .hljs-dark .hljs-selector-class,
500
+ .hljs-dark .hljs-selector-id,
501
+ .hljs-dark .hljs-variable {
502
+ color: #79c0ff;
503
+ }
504
+ .hljs-dark .hljs-meta .hljs-string,
505
+ .hljs-dark .hljs-regexp,
506
+ .hljs-dark .hljs-string {
507
+ color: #a5d6ff;
508
+ }
509
+ .hljs-dark .hljs-built_in,
510
+ .hljs-dark .hljs-symbol {
511
+ color: #ffa657;
512
+ }
513
+ .hljs-dark .hljs-code,
514
+ .hljs-dark .hljs-comment,
515
+ .hljs-dark .hljs-formula {
516
+ color: #8b949e;
517
+ }
518
+ .hljs-dark .hljs-name,
519
+ .hljs-dark .hljs-quote,
520
+ .hljs-dark .hljs-selector-pseudo,
521
+ .hljs-dark .hljs-selector-tag {
522
+ color: #7ee787;
523
+ }
524
+ .hljs-dark .hljs-subst {
525
+ color: #c9d1d9;
526
+ }
527
+ .hljs-dark .hljs-section {
528
+ color: #1f6feb;
529
+ font-weight: 700;
530
+ }
531
+ .hljs-dark .hljs-bullet {
532
+ color: #f2cc60;
533
+ }
534
+ .hljs-dark .hljs-emphasis {
535
+ color: #c9d1d9;
536
+ font-style: italic;
537
+ }
538
+ .hljs-dark .hljs-strong {
539
+ color: #c9d1d9;
540
+ font-weight: 700;
541
+ }
542
+ ` })
543
+ ] });
544
+ }
545
+
546
+ // src/editor/extensions/CodeBlockExtension.ts
547
+ import { InputRule } from "@tiptap/core";
548
+ import { TextSelection } from "@tiptap/pm/state";
549
+ var lowlight = createLowlight(common);
550
+ var CodeBlockExtension = CodeBlockLowlight.extend({
551
+ addAttributes() {
552
+ return {
553
+ language: {
554
+ default: "",
555
+ parseHTML: (el) => el.getAttribute("data-language") || "",
556
+ renderHTML: (attrs) => attrs.language ? { "data-language": attrs.language } : {}
557
+ }
558
+ };
559
+ },
560
+ addNodeView() {
561
+ return ReactNodeViewRenderer(CodeBlock);
562
+ },
563
+ addKeyboardShortcuts() {
564
+ return {
565
+ Backspace: () => {
566
+ const { state } = this.editor;
567
+ const { selection, schema } = state;
568
+ const { $from, empty } = selection;
569
+ if (!empty) return false;
570
+ const node = $from.node();
571
+ if (node.type !== this.type) return false;
572
+ const isEmpty = !node.textContent || node.textContent === "\u200B";
573
+ if (!isEmpty) return false;
574
+ const pos = $from.before($from.depth);
575
+ const tr = state.tr;
576
+ tr.deleteRange(pos, pos + node.nodeSize);
577
+ const paragraph = schema.nodes.paragraph.create(
578
+ null,
579
+ schema.text("\u200B")
580
+ );
581
+ tr.insert(pos, paragraph);
582
+ tr.setSelection(TextSelection.create(tr.doc, pos + 1));
583
+ this.editor.view.dispatch(tr);
584
+ return true;
585
+ }
586
+ };
587
+ },
588
+ addInputRules() {
589
+ const type = this.type;
590
+ return [
591
+ new InputRule({
592
+ find: /^```([a-z]*)$/i,
593
+ handler: ({ state, range, match }) => {
594
+ const { tr } = state;
595
+ const $start = tr.doc.resolve(range.from);
596
+ if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), type)) return;
597
+ const lang = (match[1] || "").toLowerCase();
598
+ tr.delete(range.from, range.to);
599
+ const node = type.create(
600
+ { language: lang },
601
+ state.schema.text("\u200B")
602
+ );
603
+ const pos = tr.mapping.map(range.from);
604
+ tr.insert(pos, node);
605
+ const $pos = tr.doc.resolve(pos + 1);
606
+ tr.setSelection(TextSelection.near($pos));
607
+ this.editor.view.dispatch(tr);
608
+ }
609
+ })
610
+ ];
611
+ }
612
+ }).configure({
613
+ lowlight,
614
+ HTMLAttributes: {
615
+ class: "code-block-custom"
616
+ }
617
+ });
618
+
619
+ // src/editor/extensions/TaskItemExtension.ts
620
+ import TaskItem from "@tiptap/extension-task-item";
621
+ import { ReactNodeViewRenderer as ReactNodeViewRenderer2 } from "@tiptap/react";
622
+
623
+ // src/editor/blocks/TaskBlock.tsx
624
+ import { NodeViewWrapper as NodeViewWrapper2, NodeViewContent as NodeViewContent2 } from "@tiptap/react";
625
+ import { Checkbox, useMantineTheme as useMantineTheme2, useMantineColorScheme } from "@mantine/core";
626
+ import { memo as memo2, useCallback as useCallback2 } from "react";
627
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
628
+ function useToggle(updateAttributes) {
629
+ return useCallback2(
630
+ (checked) => {
631
+ updateAttributes({ checked });
632
+ },
633
+ [updateAttributes]
634
+ );
635
+ }
636
+ var TaskBlock = memo2(({ node, updateAttributes }) => {
637
+ const theme = useMantineTheme2();
638
+ const { colorScheme } = useMantineColorScheme();
639
+ const isChecked = node.attrs.checked;
640
+ const toggle = useToggle(updateAttributes);
641
+ const textColor = isChecked ? colorScheme === "dark" ? theme.colors.green[2] : theme.colors.green[9] : colorScheme === "dark" ? theme.colors.blue[2] : theme.colors.blue[9];
642
+ return /* @__PURE__ */ jsxs2(
643
+ NodeViewWrapper2,
644
+ {
645
+ as: "li",
646
+ "data-type": "taskItem",
647
+ "data-checked": isChecked,
648
+ style: {
649
+ margin: "6px 0",
650
+ marginLeft: "-1.2em",
651
+ display: "flex",
652
+ alignItems: "center",
653
+ gap: 10
654
+ },
655
+ children: [
656
+ /* @__PURE__ */ jsx3(
657
+ Checkbox,
658
+ {
659
+ checked: isChecked,
660
+ onChange: (e) => toggle(e.currentTarget.checked),
661
+ size: "md",
662
+ styles: {
663
+ root: {
664
+ display: "flex",
665
+ alignItems: "center"
666
+ },
667
+ input: {
668
+ cursor: "pointer",
669
+ width: 22,
670
+ height: 22
671
+ }
672
+ }
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsx3(
676
+ NodeViewContent2,
677
+ {
678
+ as: "div",
679
+ style: {
680
+ flex: 1,
681
+ color: textColor,
682
+ fontWeight: 600,
683
+ opacity: isChecked ? 0.7 : 0.9,
684
+ textDecoration: isChecked ? "line-through" : "none"
685
+ }
686
+ }
687
+ ),
688
+ /* @__PURE__ */ jsx3("style", { children: `
689
+ div[data-node-view-content] p {
690
+ margin: 0;
691
+ display: inline;
692
+ line-height: 1.8;
693
+ }
694
+ ` })
695
+ ]
696
+ }
697
+ );
698
+ });
699
+ TaskBlock.displayName = "TaskBlock";
700
+
701
+ // src/editor/extensions/TaskItemExtension.ts
702
+ var TaskItemExtension = TaskItem.extend({
703
+ addNodeView() {
704
+ return ReactNodeViewRenderer2(TaskBlock);
705
+ }
706
+ });
707
+
708
+ // src/editor/extensions/NoticeExtension.ts
709
+ import { Node, mergeAttributes } from "@tiptap/core";
710
+ import { ReactNodeViewRenderer as ReactNodeViewRenderer3 } from "@tiptap/react";
711
+
712
+ // src/editor/blocks/NoticeBlock.tsx
713
+ import { NodeViewWrapper as NodeViewWrapper3, NodeViewContent as NodeViewContent3 } from "@tiptap/react";
714
+ import { Alert, ActionIcon as ActionIcon2, Menu, TextInput, Box as Box3 } from "@mantine/core";
715
+ import {
716
+ IconAlertCircle,
717
+ IconInfoCircle,
718
+ IconCheck as IconCheck2,
719
+ IconExclamationMark,
720
+ IconSettings,
721
+ IconTrash
722
+ } from "@tabler/icons-react";
723
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
724
+ var VARIANTS = [
725
+ { value: "info", icon: /* @__PURE__ */ jsx4(IconInfoCircle, { size: 18 }), color: "blue", label: "Th\xF4ng tin" },
726
+ { value: "success", icon: /* @__PURE__ */ jsx4(IconCheck2, { size: 18 }), color: "teal", label: "Th\xE0nh c\xF4ng" },
727
+ { value: "warning", icon: /* @__PURE__ */ jsx4(IconExclamationMark, { size: 18 }), color: "orange", label: "C\u1EA3nh b\xE1o" },
728
+ { value: "error", icon: /* @__PURE__ */ jsx4(IconAlertCircle, { size: 18 }), color: "red", label: "L\u1ED7i" }
729
+ ];
730
+ function NoticeBlock({ node, updateAttributes, deleteNode }) {
731
+ const { variant = "info", title = "Note" } = node.attrs || {};
732
+ const currentVariant = VARIANTS.find((v) => v.value === variant) || VARIANTS[0];
733
+ return /* @__PURE__ */ jsxs3(NodeViewWrapper3, { className: "notice-block-wrapper", children: [
734
+ /* @__PURE__ */ jsx4(BlockWrapper, { children: /* @__PURE__ */ jsxs3(Box3, { style: { position: "relative" }, children: [
735
+ /* @__PURE__ */ jsx4(
736
+ "div",
737
+ {
738
+ style: {
739
+ position: "absolute",
740
+ top: "8px",
741
+ right: "8px",
742
+ zIndex: 10,
743
+ display: "flex",
744
+ gap: "4px"
745
+ },
746
+ children: /* @__PURE__ */ jsxs3(Menu, { shadow: "md", width: 180, position: "bottom-end", withArrow: true, children: [
747
+ /* @__PURE__ */ jsx4(Menu.Target, { children: /* @__PURE__ */ jsx4(
748
+ ActionIcon2,
749
+ {
750
+ variant: "subtle",
751
+ color: "gray",
752
+ size: "sm",
753
+ className: "notice-settings-btn",
754
+ style: { opacity: 0.4 },
755
+ children: /* @__PURE__ */ jsx4(IconSettings, { size: 16 })
756
+ }
757
+ ) }),
758
+ /* @__PURE__ */ jsxs3(Menu.Dropdown, { children: [
759
+ /* @__PURE__ */ jsx4(Menu.Label, { children: "Lo\u1EA1i hi\u1EC3n th\u1ECB" }),
760
+ VARIANTS.map((v) => /* @__PURE__ */ jsx4(
761
+ Menu.Item,
762
+ {
763
+ leftSection: v.icon,
764
+ onClick: () => updateAttributes?.({ variant: v.value }),
765
+ style: { iconSize: 16 },
766
+ children: v.label
767
+ },
768
+ v.value
769
+ )),
770
+ /* @__PURE__ */ jsx4(Menu.Divider, {}),
771
+ /* @__PURE__ */ jsx4(
772
+ Menu.Item,
773
+ {
774
+ color: "red",
775
+ leftSection: /* @__PURE__ */ jsx4(IconTrash, { size: 18 }),
776
+ onClick: () => deleteNode?.(),
777
+ children: "X\xF3a kh\u1ED1i"
778
+ }
779
+ )
780
+ ] })
781
+ ] })
782
+ }
783
+ ),
784
+ /* @__PURE__ */ jsxs3(
785
+ Alert,
786
+ {
787
+ variant: "light",
788
+ color: currentVariant.color,
789
+ icon: currentVariant.icon,
790
+ radius: "md",
791
+ styles: {
792
+ root: {
793
+ padding: "4px 10px",
794
+ border: "none",
795
+ backgroundColor: `var(--mantine-color-${currentVariant.color}-light)`,
796
+ borderLeft: `4px solid var(--mantine-color-${currentVariant.color}-filled)`
797
+ },
798
+ icon: {
799
+ marginRight: "4px",
800
+ marginTop: "10px"
801
+ },
802
+ wrapper: {
803
+ gap: "4px"
804
+ }
805
+ },
806
+ children: [
807
+ /* @__PURE__ */ jsx4(
808
+ TextInput,
809
+ {
810
+ placeholder: "Nh\u1EADp ti\xEAu \u0111\u1EC1...",
811
+ variant: "unstyled",
812
+ value: title,
813
+ onChange: (e) => updateAttributes?.({ title: e.target.value }),
814
+ styles: {
815
+ input: {
816
+ fontWeight: 700,
817
+ fontSize: "1.05rem",
818
+ minHeight: "auto",
819
+ padding: 0,
820
+ height: "auto",
821
+ color: `var(--mantine-color-${currentVariant.color}-filled)`,
822
+ letterSpacing: "-0.01em"
823
+ }
824
+ }
825
+ }
826
+ ),
827
+ /* @__PURE__ */ jsx4(Box3, { style: { marginTop: "-6px", marginBottom: "12px", fontSize: "0.95rem", lineHeight: 1.6 }, children: /* @__PURE__ */ jsx4(NodeViewContent3, { className: "notice-content-area" }) })
828
+ ]
829
+ }
830
+ )
831
+ ] }) }),
832
+ /* @__PURE__ */ jsx4("style", { children: `
833
+ .notice-block-wrapper:hover .notice-settings-btn {
834
+ opacity: 1 !important;
835
+ }
836
+ .notice-content-area {
837
+ min-height: 1.2em;
838
+ }
839
+ .notice-content-area:focus {
840
+ outline: none;
841
+ }
842
+ ` })
843
+ ] });
844
+ }
845
+
846
+ // src/editor/extensions/NoticeExtension.ts
847
+ var NoticeExtension = Node.create({
848
+ name: "notice",
849
+ group: "block",
850
+ content: "block+",
851
+ defining: true,
852
+ draggable: true,
853
+ addAttributes() {
854
+ return {
855
+ variant: {
856
+ default: "info",
857
+ parseHTML: (element) => element.getAttribute("data-variant"),
858
+ renderHTML: (attributes) => ({ "data-variant": attributes.variant })
859
+ },
860
+ title: {
861
+ default: "Note",
862
+ parseHTML: (element) => element.getAttribute("data-title"),
863
+ renderHTML: (attributes) => ({ "data-title": attributes.title })
864
+ }
865
+ };
866
+ },
867
+ parseHTML() {
868
+ return [{ tag: 'div[data-type="notice"]' }];
869
+ },
870
+ renderHTML({ HTMLAttributes }) {
871
+ return ["div", mergeAttributes(HTMLAttributes, { "data-type": "notice" }), 0];
872
+ },
873
+ addNodeView() {
874
+ return ReactNodeViewRenderer3(NoticeBlock);
875
+ },
876
+ addCommands() {
877
+ return {
878
+ setNotice: (attrs) => ({ commands }) => {
879
+ return commands.insertContent({
880
+ type: this.name,
881
+ attrs,
882
+ content: [{ type: "paragraph" }]
883
+ });
884
+ },
885
+ toggleNotice: () => ({ commands }) => {
886
+ return commands.toggleNode(this.name, "paragraph");
887
+ }
888
+ };
889
+ }
890
+ });
891
+
892
+ // src/editor/extensions/TimelineExtension.ts
893
+ import { Node as Node2, mergeAttributes as mergeAttributes2 } from "@tiptap/core";
894
+ import { ReactNodeViewRenderer as ReactNodeViewRenderer4 } from "@tiptap/react";
895
+
896
+ // src/editor/blocks/TimelineBlock.tsx
897
+ import { NodeViewWrapper as NodeViewWrapper4 } from "@tiptap/react";
898
+ import {
899
+ Box as Box4,
900
+ ActionIcon as ActionIcon3,
901
+ Menu as Menu2,
902
+ TextInput as TextInput2,
903
+ Textarea,
904
+ Button,
905
+ Timeline,
906
+ Group,
907
+ Text as Text2,
908
+ Paper,
909
+ Stack,
910
+ Tooltip,
911
+ Badge,
912
+ rem,
913
+ UnstyledButton
914
+ } from "@mantine/core";
915
+ import {
916
+ IconSettings as IconSettings2,
917
+ IconTrash as IconTrash2,
918
+ IconPlus,
919
+ IconCalendar,
920
+ IconEdit,
921
+ IconEye,
922
+ IconCheck as IconCheck3
923
+ } from "@tabler/icons-react";
924
+ import { useState as useState2, useCallback as useCallback3 } from "react";
925
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
926
+ var generateId = () => Math.random().toString(36).substring(7);
927
+ function TimelineBlock({ node, updateAttributes, deleteNode }) {
928
+ const { items = [], active = 0 } = node.attrs;
929
+ const [isEditMode, setIsEditMode] = useState2(false);
930
+ const [editingItemId, setEditingItemId] = useState2(null);
931
+ const handleUpdateItems = useCallback3((newItems) => {
932
+ updateAttributes({ items: newItems });
933
+ }, [updateAttributes]);
934
+ const addTimelineItem = useCallback3(() => {
935
+ const newItem = {
936
+ id: generateId(),
937
+ title: "",
938
+ description: "",
939
+ date: (/* @__PURE__ */ new Date()).toLocaleDateString("vi-VN")
940
+ };
941
+ handleUpdateItems([...items, newItem]);
942
+ setEditingItemId(newItem.id);
943
+ setIsEditMode(true);
944
+ }, [items, handleUpdateItems]);
945
+ const updateTimelineItem = (index, updates) => {
946
+ const newItems = [...items];
947
+ newItems[index] = { ...newItems[index], ...updates };
948
+ handleUpdateItems(newItems);
949
+ };
950
+ const deleteTimelineItem = (index) => {
951
+ const newItems = items.filter((_, i) => i !== index);
952
+ handleUpdateItems(newItems);
953
+ setEditingItemId(null);
954
+ };
955
+ return /* @__PURE__ */ jsxs4(NodeViewWrapper4, { className: "timeline-block", children: [
956
+ /* @__PURE__ */ jsx5(BlockWrapper, { children: /* @__PURE__ */ jsxs4(
957
+ Paper,
958
+ {
959
+ withBorder: true,
960
+ p: "md",
961
+ radius: "md",
962
+ pos: "relative",
963
+ style: { transition: "border-color 0.2s ease" },
964
+ children: [
965
+ /* @__PURE__ */ jsxs4(
966
+ Group,
967
+ {
968
+ gap: 5,
969
+ pos: "absolute",
970
+ top: 8,
971
+ right: 8,
972
+ style: { zIndex: 10 },
973
+ children: [
974
+ /* @__PURE__ */ jsx5(Tooltip, { label: isEditMode ? "Xem tr\u01B0\u1EDBc" : "Ch\u1EC9nh s\u1EEDa", children: /* @__PURE__ */ jsx5(
975
+ ActionIcon3,
976
+ {
977
+ size: "sm",
978
+ variant: "subtle",
979
+ color: isEditMode ? "blue" : "gray",
980
+ onClick: () => {
981
+ setIsEditMode(!isEditMode);
982
+ setEditingItemId(null);
983
+ },
984
+ children: isEditMode ? /* @__PURE__ */ jsx5(IconEye, { size: 16 }) : /* @__PURE__ */ jsx5(IconEdit, { size: 16 })
985
+ }
986
+ ) }),
987
+ /* @__PURE__ */ jsxs4(Menu2, { position: "bottom-end", withinPortal: true, shadow: "md", children: [
988
+ /* @__PURE__ */ jsx5(Menu2.Target, { children: /* @__PURE__ */ jsx5(ActionIcon3, { size: "sm", variant: "subtle", color: "gray", children: /* @__PURE__ */ jsx5(IconSettings2, { size: 16 }) }) }),
989
+ /* @__PURE__ */ jsxs4(Menu2.Dropdown, { children: [
990
+ /* @__PURE__ */ jsx5(Menu2.Item, { leftSection: /* @__PURE__ */ jsx5(IconPlus, { size: 14 }), onClick: addTimelineItem, children: "Th\xEAm s\u1EF1 ki\u1EC7n" }),
991
+ /* @__PURE__ */ jsx5(Menu2.Divider, {}),
992
+ /* @__PURE__ */ jsx5(Menu2.Item, { color: "red", leftSection: /* @__PURE__ */ jsx5(IconTrash2, { size: 14 }), onClick: () => deleteNode(), children: "X\xF3a block" })
993
+ ] })
994
+ ] })
995
+ ]
996
+ }
997
+ ),
998
+ /* @__PURE__ */ jsx5(Timeline, { active, bulletSize: 28, lineWidth: 2, children: items.map((item, index) => {
999
+ const isItemEditing = editingItemId === item.id && isEditMode;
1000
+ return /* @__PURE__ */ jsx5(
1001
+ Timeline.Item,
1002
+ {
1003
+ bullet: /* @__PURE__ */ jsx5(
1004
+ UnstyledButton,
1005
+ {
1006
+ onClick: () => updateAttributes({ active: index }),
1007
+ style: {
1008
+ fontSize: rem(10),
1009
+ fontWeight: 800,
1010
+ textAlign: "center",
1011
+ width: "100%"
1012
+ },
1013
+ children: index + 1
1014
+ }
1015
+ ),
1016
+ children: /* @__PURE__ */ jsx5(
1017
+ Box4,
1018
+ {
1019
+ p: "xs",
1020
+ style: {
1021
+ borderRadius: "var(--mantine-radius-sm)",
1022
+ border: isItemEditing ? "1px solid var(--mantine-color-blue-light)" : "1px solid transparent",
1023
+ backgroundColor: isItemEditing ? "var(--mantine-color-blue-light-hover)" : "transparent",
1024
+ transition: "all 0.15s ease"
1025
+ },
1026
+ children: isItemEditing ? /* @__PURE__ */ jsxs4(Stack, { gap: "xs", children: [
1027
+ /* @__PURE__ */ jsx5(
1028
+ TextInput2,
1029
+ {
1030
+ variant: "unstyled",
1031
+ placeholder: "Ng\xE0y th\xE1ng...",
1032
+ value: item.date,
1033
+ onChange: (e) => updateTimelineItem(index, { date: e.currentTarget.value }),
1034
+ size: "xs",
1035
+ leftSection: /* @__PURE__ */ jsx5(IconCalendar, { size: 14 }),
1036
+ styles: { input: { minHeight: 0, height: rem(20) } }
1037
+ }
1038
+ ),
1039
+ /* @__PURE__ */ jsx5(
1040
+ TextInput2,
1041
+ {
1042
+ variant: "unstyled",
1043
+ placeholder: "Ti\xEAu \u0111\u1EC1...",
1044
+ value: item.title,
1045
+ onChange: (e) => updateTimelineItem(index, { title: e.currentTarget.value }),
1046
+ fw: 700,
1047
+ size: "sm",
1048
+ styles: { input: { minHeight: 0, height: rem(24), fontSize: rem(15) } }
1049
+ }
1050
+ ),
1051
+ /* @__PURE__ */ jsx5(
1052
+ Textarea,
1053
+ {
1054
+ variant: "unstyled",
1055
+ placeholder: "M\xF4 t\u1EA3...",
1056
+ value: item.description,
1057
+ onChange: (e) => updateTimelineItem(index, { description: e.currentTarget.value }),
1058
+ size: "sm",
1059
+ autosize: true,
1060
+ minRows: 1,
1061
+ styles: { input: { padding: 0, lineHeight: 1.4 } }
1062
+ }
1063
+ ),
1064
+ /* @__PURE__ */ jsxs4(Group, { justify: "flex-end", gap: 5, children: [
1065
+ /* @__PURE__ */ jsx5(ActionIcon3, { size: "sm", color: "red", variant: "subtle", onClick: () => deleteTimelineItem(index), children: /* @__PURE__ */ jsx5(IconTrash2, { size: 14 }) }),
1066
+ /* @__PURE__ */ jsx5(ActionIcon3, { size: "sm", color: "blue", variant: "light", onClick: () => setEditingItemId(null), children: /* @__PURE__ */ jsx5(IconCheck3, { size: 14 }) })
1067
+ ] })
1068
+ ] }) : /* @__PURE__ */ jsxs4(
1069
+ Box4,
1070
+ {
1071
+ onClick: () => isEditMode && setEditingItemId(item.id),
1072
+ style: { cursor: isEditMode ? "pointer" : "default" },
1073
+ children: [
1074
+ /* @__PURE__ */ jsx5(Group, { gap: 6, mb: 2, children: item.date ? /* @__PURE__ */ jsx5(Badge, { size: "xs", variant: "dot", color: "blue", children: item.date }) : isEditMode && /* @__PURE__ */ jsx5(Text2, { size: "xs", c: "dimmed", fs: "italic", children: "Th\xEAm ng\xE0y..." }) }),
1075
+ /* @__PURE__ */ jsx5(Text2, { size: "sm", fw: 700, c: item.title ? "inherit" : "dimmed", children: item.title || (isEditMode ? "Nh\u1EA5n \u0111\u1EC3 nh\u1EADp ti\xEAu \u0111\u1EC1..." : "S\u1EF1 ki\u1EC7n kh\xF4ng t\xEAn") }),
1076
+ /* @__PURE__ */ jsx5(Text2, { size: "xs", c: "dimmed", lineClamp: 2, mt: 2, children: item.description || (isEditMode ? "Th\xEAm m\xF4 t\u1EA3..." : "") })
1077
+ ]
1078
+ }
1079
+ )
1080
+ }
1081
+ )
1082
+ },
1083
+ item.id
1084
+ );
1085
+ }) }),
1086
+ isEditMode && /* @__PURE__ */ jsx5(
1087
+ Button,
1088
+ {
1089
+ variant: "subtle",
1090
+ fullWidth: true,
1091
+ mt: "sm",
1092
+ size: "xs",
1093
+ leftSection: /* @__PURE__ */ jsx5(IconPlus, { size: 14 }),
1094
+ onClick: addTimelineItem,
1095
+ color: "blue",
1096
+ children: "Th\xEAm m\u1ED1c th\u1EDDi gian"
1097
+ }
1098
+ )
1099
+ ]
1100
+ }
1101
+ ) }),
1102
+ /* @__PURE__ */ jsx5("style", { children: `
1103
+ .timeline-block:hover .timeline-toolbar {
1104
+ opacity: 1 !important;
1105
+ }
1106
+ .timeline-block input, .timeline-block textarea {
1107
+ font-family: inherit !important;
1108
+ }
1109
+ ` })
1110
+ ] });
1111
+ }
1112
+
1113
+ // src/editor/extensions/TimelineExtension.ts
1114
+ var TimelineExtension = Node2.create({
1115
+ name: "timeline",
1116
+ group: "block",
1117
+ draggable: true,
1118
+ isolating: true,
1119
+ addAttributes() {
1120
+ return {
1121
+ items: {
1122
+ default: [],
1123
+ parseHTML: (element) => {
1124
+ const json = element.getAttribute("data-items");
1125
+ return json ? JSON.parse(json) : [];
1126
+ },
1127
+ renderHTML: (attributes) => ({
1128
+ "data-items": JSON.stringify(attributes.items)
1129
+ })
1130
+ },
1131
+ active: {
1132
+ default: 0,
1133
+ parseHTML: (element) => parseInt(element.getAttribute("data-active") || "0", 10),
1134
+ renderHTML: (attributes) => ({
1135
+ "data-active": attributes.active
1136
+ })
1137
+ }
1138
+ };
1139
+ },
1140
+ parseHTML() {
1141
+ return [{ tag: 'div[data-type="timeline"]' }];
1142
+ },
1143
+ renderHTML({ HTMLAttributes }) {
1144
+ return ["div", mergeAttributes2(HTMLAttributes, { "data-type": "timeline" })];
1145
+ },
1146
+ addNodeView() {
1147
+ return ReactNodeViewRenderer4(TimelineBlock);
1148
+ },
1149
+ addCommands() {
1150
+ return {
1151
+ setTimeline: () => ({ commands }) => {
1152
+ return commands.insertContent({
1153
+ type: this.name,
1154
+ attrs: {
1155
+ items: [],
1156
+ active: 0
1157
+ }
1158
+ });
1159
+ }
1160
+ };
1161
+ }
1162
+ });
1163
+
1164
+ // src/editor/extensions/DiagramExtension.ts
1165
+ import { Node as Node3, mergeAttributes as mergeAttributes3 } from "@tiptap/core";
1166
+ import { ReactNodeViewRenderer as ReactNodeViewRenderer5 } from "@tiptap/react";
1167
+
1168
+ // src/editor/blocks/DiagramBlock.tsx
1169
+ import { NodeViewWrapper as NodeViewWrapper5 } from "@tiptap/react";
1170
+ import {
1171
+ Box as Box5,
1172
+ ActionIcon as ActionIcon4,
1173
+ Menu as Menu3,
1174
+ Textarea as Textarea2,
1175
+ Button as Button2,
1176
+ Text as Text3,
1177
+ Select as Select2,
1178
+ Group as Group2,
1179
+ Alert as Alert2,
1180
+ Paper as Paper2,
1181
+ Tooltip as Tooltip2,
1182
+ useComputedColorScheme,
1183
+ rem as rem2
1184
+ } from "@mantine/core";
1185
+ import {
1186
+ IconSettings as IconSettings3,
1187
+ IconTrash as IconTrash3,
1188
+ IconChartArea,
1189
+ IconEye as IconEye2,
1190
+ IconCode as IconCode2,
1191
+ IconAlertCircle as IconAlertCircle2,
1192
+ IconDeviceFloppy,
1193
+ IconRefresh
1194
+ } from "@tabler/icons-react";
1195
+ import { useState as useState3, useEffect, useRef, useCallback as useCallback4 } from "react";
1196
+ import mermaid from "mermaid";
1197
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1198
+ var DIAGRAM_TYPES = [
1199
+ { value: "graph", label: "Flowchart", example: "graph TD\n A[B\u1EAFt \u0111\u1EA7u] --> B{H\u1EE3p l\u1EC7?}\n B -->|C\xF3| C[X\u1EED l\xFD]\n B -->|Kh\xF4ng| D[K\u1EBFt th\xFAc]" },
1200
+ { value: "sequenceDiagram", label: "Sequence Diagram", example: "sequenceDiagram\n Client->>Server: Request\n Server-->>Client: Response" },
1201
+ { value: "classDiagram", label: "Class Diagram", example: "classDiagram\n Animal <|-- Duck\n Animal : +int age\n Animal: +isAlive()" },
1202
+ { value: "stateDiagram-v2", label: "State Diagram", example: "stateDiagram-v2\n [*] --> Idle\n Idle --> Processing\n Processing --> [*]" },
1203
+ { value: "erDiagram", label: "ER Diagram", example: "erDiagram\n USER ||--o{ POST : writes" },
1204
+ { value: "pie", label: "Pie Chart", example: 'pie title Th\u1ED1ng k\xEA\n "Data A" : 45\n "Data B" : 55' }
1205
+ ];
1206
+ function DiagramBlock({ node, updateAttributes, deleteNode }) {
1207
+ const { code = "", type = "graph" } = node.attrs;
1208
+ const [isEditing, setIsEditing] = useState3(!code);
1209
+ const [previewError, setPreviewError] = useState3(null);
1210
+ const [localCode, setLocalCode] = useState3(code);
1211
+ const mermaidRef = useRef(null);
1212
+ const computedColorScheme = useComputedColorScheme("light");
1213
+ useEffect(() => {
1214
+ mermaid.initialize({
1215
+ startOnLoad: false,
1216
+ theme: computedColorScheme === "dark" ? "dark" : "default",
1217
+ securityLevel: "loose",
1218
+ fontFamily: "inherit",
1219
+ look: "handDrawn"
1220
+ });
1221
+ }, [computedColorScheme]);
1222
+ const renderMermaid = useCallback4(async (codeToRender) => {
1223
+ if (!mermaidRef.current || !codeToRender.trim()) return;
1224
+ try {
1225
+ setPreviewError(null);
1226
+ mermaidRef.current.innerHTML = "";
1227
+ const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
1228
+ const { svg } = await mermaid.render(id, codeToRender);
1229
+ if (mermaidRef.current) {
1230
+ mermaidRef.current.innerHTML = svg;
1231
+ }
1232
+ } catch (error) {
1233
+ setPreviewError(error?.str || error?.message || "L\u1ED7i c\xFA ph\xE1p Mermaid");
1234
+ }
1235
+ }, []);
1236
+ useEffect(() => {
1237
+ if (!isEditing && localCode) {
1238
+ renderMermaid(localCode);
1239
+ }
1240
+ }, [isEditing, localCode, renderMermaid, computedColorScheme]);
1241
+ const handleSave = () => {
1242
+ updateAttributes({ code: localCode, type });
1243
+ setIsEditing(false);
1244
+ };
1245
+ const handleTypeChange = (newType) => {
1246
+ if (!newType) return;
1247
+ const selected = DIAGRAM_TYPES.find((t) => t.value === newType);
1248
+ if (selected) {
1249
+ setLocalCode(selected.example);
1250
+ updateAttributes({ code: selected.example, type: newType });
1251
+ }
1252
+ };
1253
+ return /* @__PURE__ */ jsxs5(NodeViewWrapper5, { className: "diagram-block-group", children: [
1254
+ /* @__PURE__ */ jsx6(BlockWrapper, { children: /* @__PURE__ */ jsxs5(
1255
+ Paper2,
1256
+ {
1257
+ withBorder: true,
1258
+ radius: "md",
1259
+ shadow: "sm",
1260
+ pos: "relative",
1261
+ style: { overflow: "hidden" },
1262
+ children: [
1263
+ /* @__PURE__ */ jsxs5(
1264
+ Group2,
1265
+ {
1266
+ justify: "space-between",
1267
+ p: "xs",
1268
+ bg: "var(--mantine-color-default-hover)",
1269
+ style: { borderBottom: "1px solid var(--mantine-color-default-border)" },
1270
+ children: [
1271
+ /* @__PURE__ */ jsxs5(Group2, { gap: "xs", children: [
1272
+ /* @__PURE__ */ jsx6(IconChartArea, { size: 18, color: "var(--mantine-color-blue-filled)" }),
1273
+ /* @__PURE__ */ jsx6(Text3, { size: "xs", fw: 700, tt: "uppercase", c: "dimmed", children: DIAGRAM_TYPES.find((t) => t.value === type)?.label || "Diagram" })
1274
+ ] }),
1275
+ /* @__PURE__ */ jsxs5(Group2, { gap: 4, children: [
1276
+ /* @__PURE__ */ jsx6(Tooltip2, { label: isEditing ? "Xem tr\u01B0\u1EDBc" : "S\u1EEDa code", children: /* @__PURE__ */ jsx6(
1277
+ ActionIcon4,
1278
+ {
1279
+ variant: "subtle",
1280
+ color: "gray",
1281
+ onClick: () => setIsEditing(!isEditing),
1282
+ children: isEditing ? /* @__PURE__ */ jsx6(IconEye2, { size: 18 }) : /* @__PURE__ */ jsx6(IconCode2, { size: 18 })
1283
+ }
1284
+ ) }),
1285
+ /* @__PURE__ */ jsxs5(Menu3, { position: "bottom-end", withArrow: true, shadow: "md", children: [
1286
+ /* @__PURE__ */ jsx6(Menu3.Target, { children: /* @__PURE__ */ jsx6(ActionIcon4, { variant: "subtle", color: "gray", children: /* @__PURE__ */ jsx6(IconSettings3, { size: 18 }) }) }),
1287
+ /* @__PURE__ */ jsxs5(Menu3.Dropdown, { children: [
1288
+ /* @__PURE__ */ jsx6(Menu3.Label, { children: "Lo\u1EA1i bi\u1EC3u \u0111\u1ED3" }),
1289
+ DIAGRAM_TYPES.map((t) => /* @__PURE__ */ jsx6(Menu3.Item, { onClick: () => handleTypeChange(t.value), children: t.label }, t.value)),
1290
+ /* @__PURE__ */ jsx6(Menu3.Divider, {}),
1291
+ /* @__PURE__ */ jsx6(
1292
+ Menu3.Item,
1293
+ {
1294
+ color: "red",
1295
+ leftSection: /* @__PURE__ */ jsx6(IconTrash3, { size: 16 }),
1296
+ onClick: () => deleteNode(),
1297
+ children: "X\xF3a bi\u1EC3u \u0111\u1ED3"
1298
+ }
1299
+ )
1300
+ ] })
1301
+ ] })
1302
+ ] })
1303
+ ]
1304
+ }
1305
+ ),
1306
+ /* @__PURE__ */ jsx6(Box5, { p: "md", children: isEditing ? /* @__PURE__ */ jsxs5(Box5, { children: [
1307
+ /* @__PURE__ */ jsx6(
1308
+ Select2,
1309
+ {
1310
+ data: DIAGRAM_TYPES,
1311
+ value: type,
1312
+ onChange: handleTypeChange,
1313
+ label: "Ki\u1EC3u bi\u1EC3u \u0111\u1ED3",
1314
+ size: "xs",
1315
+ mb: "sm"
1316
+ }
1317
+ ),
1318
+ /* @__PURE__ */ jsx6(
1319
+ Textarea2,
1320
+ {
1321
+ placeholder: "Nh\u1EADp code Mermaid t\u1EA1i \u0111\xE2y...",
1322
+ value: localCode,
1323
+ onChange: (e) => setLocalCode(e.currentTarget.value),
1324
+ minRows: 6,
1325
+ maxRows: 20,
1326
+ autosize: true,
1327
+ styles: {
1328
+ input: {
1329
+ fontFamily: "var(--mantine-font-family-monospace)",
1330
+ fontSize: rem2(13),
1331
+ backgroundColor: "var(--mantine-color-body)"
1332
+ }
1333
+ }
1334
+ }
1335
+ ),
1336
+ /* @__PURE__ */ jsxs5(Group2, { justify: "flex-end", mt: "md", children: [
1337
+ /* @__PURE__ */ jsx6(
1338
+ Button2,
1339
+ {
1340
+ variant: "light",
1341
+ size: "xs",
1342
+ leftSection: /* @__PURE__ */ jsx6(IconRefresh, { size: 14 }),
1343
+ onClick: () => setLocalCode(code),
1344
+ children: "Ho\xE0n t\xE1c"
1345
+ }
1346
+ ),
1347
+ /* @__PURE__ */ jsx6(
1348
+ Button2,
1349
+ {
1350
+ size: "xs",
1351
+ leftSection: /* @__PURE__ */ jsx6(IconDeviceFloppy, { size: 14 }),
1352
+ onClick: handleSave,
1353
+ children: "L\u01B0u & Hi\u1EC3n th\u1ECB"
1354
+ }
1355
+ )
1356
+ ] })
1357
+ ] }) : /* @__PURE__ */ jsx6(Box5, { children: previewError ? /* @__PURE__ */ jsxs5(Alert2, { icon: /* @__PURE__ */ jsx6(IconAlertCircle2, { size: 16 }), title: "L\u1ED7i c\xFA ph\xE1p", color: "red", children: [
1358
+ /* @__PURE__ */ jsx6(Text3, { size: "xs", style: { whiteSpace: "pre-wrap" }, children: previewError }),
1359
+ /* @__PURE__ */ jsx6(Button2, { size: "compact-xs", color: "red", variant: "light", mt: "sm", onClick: () => setIsEditing(true), children: "S\u1EEDa ngay" })
1360
+ ] }) : /* @__PURE__ */ jsx6(
1361
+ Box5,
1362
+ {
1363
+ ref: mermaidRef,
1364
+ className: "mermaid-render-container",
1365
+ style: {
1366
+ display: "flex",
1367
+ justifyContent: "center",
1368
+ overflowX: "auto",
1369
+ padding: "1rem",
1370
+ borderRadius: "var(--mantine-radius-sm)",
1371
+ backgroundColor: computedColorScheme === "dark" ? "transparent" : "#fff"
1372
+ }
1373
+ }
1374
+ ) }) })
1375
+ ]
1376
+ }
1377
+ ) }),
1378
+ /* @__PURE__ */ jsx6("style", { children: `
1379
+ .mermaid-render-container svg {
1380
+ max-width: 100%;
1381
+ height: auto;
1382
+ }
1383
+ .diagram-block-group {
1384
+ transition: all 0.2s ease;
1385
+ }
1386
+ ` })
1387
+ ] });
1388
+ }
1389
+
1390
+ // src/editor/extensions/DiagramExtension.ts
1391
+ var DiagramExtension = Node3.create({
1392
+ name: "diagram",
1393
+ group: "block",
1394
+ draggable: true,
1395
+ isolating: true,
1396
+ addAttributes() {
1397
+ return {
1398
+ code: {
1399
+ default: "",
1400
+ parseHTML: (element) => element.getAttribute("data-code") || "",
1401
+ renderHTML: (attributes) => ({
1402
+ "data-code": attributes.code || ""
1403
+ })
1404
+ },
1405
+ type: {
1406
+ default: "graph",
1407
+ parseHTML: (element) => element.getAttribute("data-type") || "graph",
1408
+ renderHTML: (attributes) => ({
1409
+ "data-type": attributes.type || "graph"
1410
+ })
1411
+ }
1412
+ };
1413
+ },
1414
+ parseHTML() {
1415
+ return [{ tag: 'div[data-type="diagram"]' }];
1416
+ },
1417
+ renderHTML({ HTMLAttributes }) {
1418
+ return ["div", mergeAttributes3(HTMLAttributes, { "data-type": "diagram" })];
1419
+ },
1420
+ addNodeView() {
1421
+ return ReactNodeViewRenderer5(DiagramBlock);
1422
+ },
1423
+ addCommands() {
1424
+ return {
1425
+ setDiagram: () => ({ commands }) => {
1426
+ return commands.insertContent({
1427
+ type: this.name,
1428
+ attrs: {
1429
+ code: "",
1430
+ type: "graph"
1431
+ }
1432
+ });
1433
+ }
1434
+ };
1435
+ }
1436
+ });
1437
+
1438
+ // src/editor/hooks/useEditor.ts
1439
+ import { Color } from "@tiptap/extension-color";
1440
+ import { TextStyle } from "@tiptap/extension-text-style";
1441
+
1442
+ // src/editor/extensions/BackgroundColor.ts
1443
+ import { Mark, mergeAttributes as mergeAttributes4 } from "@tiptap/core";
1444
+ var BackgroundColorMark = Mark.create({
1445
+ name: "backgroundColor",
1446
+ addOptions() {
1447
+ return {
1448
+ HTMLAttributes: {}
1449
+ };
1450
+ },
1451
+ addAttributes() {
1452
+ return {
1453
+ color: {
1454
+ default: null,
1455
+ parseHTML: (element) => element.getAttribute("data-bg-color") || null,
1456
+ renderHTML: (attributes) => {
1457
+ if (!attributes.color) {
1458
+ return {};
1459
+ }
1460
+ return {
1461
+ "data-bg-color": attributes.color
1462
+ };
1463
+ }
1464
+ }
1465
+ };
1466
+ },
1467
+ parseHTML() {
1468
+ return [
1469
+ {
1470
+ tag: "span[data-bg-color]"
1471
+ }
1472
+ ];
1473
+ },
1474
+ renderHTML({ HTMLAttributes, mark }) {
1475
+ return [
1476
+ "span",
1477
+ mergeAttributes4(HTMLAttributes, { "data-bg-color": mark.attrs.color }),
1478
+ 0
1479
+ ];
1480
+ },
1481
+ addCommands() {
1482
+ return {
1483
+ setBackgroundColor: (color) => ({ commands }) => {
1484
+ return commands.setMark(this.name, { color });
1485
+ },
1486
+ unsetBackgroundColor: () => ({ commands }) => {
1487
+ return commands.unsetMark(this.name);
1488
+ }
1489
+ };
1490
+ }
1491
+ });
1492
+
1493
+ // src/editor/hooks/useEditor.ts
1494
+ import TaskList from "@tiptap/extension-task-list";
1495
+ import { Markdown } from "tiptap-markdown";
1496
+ import { Table, TableRow, TableHeader, TableCell } from "@tiptap/extension-table";
1497
+
1498
+ // src/editor/extensions/ImageExtension.ts
1499
+ import Image from "@tiptap/extension-image";
1500
+ import { ReactNodeViewRenderer as ReactNodeViewRenderer6 } from "@tiptap/react";
1501
+
1502
+ // src/editor/blocks/ImageBlock.tsx
1503
+ import { useState as useState4, memo as memo3, useCallback as useCallback5, useRef as useRef2, useEffect as useEffect2 } from "react";
1504
+ import { NodeViewWrapper as NodeViewWrapper6 } from "@tiptap/react";
1505
+ import { ActionIcon as ActionIcon5, Box as Box6, useMantineColorScheme as useMantineColorScheme2, useMantineTheme as useMantineTheme3, Text as Text4 } from "@mantine/core";
1506
+ import { IconCheck as IconCheck4, IconCopy as IconCopy2, IconArrowsMaximize } from "@tabler/icons-react";
1507
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1508
+ var RESIZE_HANDLE_SIZE = 12;
1509
+ var ImageBlock = memo3(function ImageBlock2({ node, selected, updateAttributes }) {
1510
+ const [copied, setCopied] = useState4(false);
1511
+ const [isResizing, setIsResizing] = useState4(false);
1512
+ const [resizeStart, setResizeStart] = useState4({ x: 0, y: 0, width: 0 });
1513
+ const imgRef = useRef2(null);
1514
+ const { colorScheme } = useMantineColorScheme2();
1515
+ const theme = useMantineTheme3();
1516
+ const isDark = colorScheme === "dark";
1517
+ const src = node.attrs?.src || "";
1518
+ const alt = node.attrs?.alt || "";
1519
+ const width = node.attrs?.width || null;
1520
+ useEffect2(() => {
1521
+ const handleMouseMove = (e) => {
1522
+ if (!isResizing || !imgRef.current) return;
1523
+ const deltaX = e.clientX - resizeStart.x;
1524
+ const newWidth = Math.max(100, resizeStart.width + deltaX);
1525
+ imgRef.current.style.width = `${newWidth}px`;
1526
+ };
1527
+ const handleMouseUp = () => {
1528
+ if (!isResizing || !imgRef.current) return;
1529
+ const currentWidth = imgRef.current.offsetWidth;
1530
+ updateAttributes?.({ width: currentWidth });
1531
+ setIsResizing(false);
1532
+ };
1533
+ if (isResizing) {
1534
+ document.addEventListener("mousemove", handleMouseMove);
1535
+ document.addEventListener("mouseup", handleMouseUp);
1536
+ document.body.style.cursor = "ew-resize";
1537
+ document.body.style.userSelect = "none";
1538
+ }
1539
+ return () => {
1540
+ document.removeEventListener("mousemove", handleMouseMove);
1541
+ document.removeEventListener("mouseup", handleMouseUp);
1542
+ document.body.style.cursor = "";
1543
+ document.body.style.userSelect = "";
1544
+ };
1545
+ }, [isResizing, resizeStart, updateAttributes]);
1546
+ const handleResizeStart = useCallback5((e) => {
1547
+ e.preventDefault();
1548
+ e.stopPropagation();
1549
+ if (!imgRef.current) return;
1550
+ setIsResizing(true);
1551
+ setResizeStart({
1552
+ x: e.clientX,
1553
+ y: e.clientY,
1554
+ width: imgRef.current.offsetWidth
1555
+ });
1556
+ }, []);
1557
+ const handleCopy = useCallback5(async () => {
1558
+ if (!src) return;
1559
+ try {
1560
+ const success = await copyToClipboard(src);
1561
+ if (success) {
1562
+ setCopied(true);
1563
+ setTimeout(() => setCopied(false), 1500);
1564
+ }
1565
+ } catch (err) {
1566
+ console.error("Copy failed:", err);
1567
+ }
1568
+ }, [src]);
1569
+ const handleResetSize = useCallback5(() => {
1570
+ if (!imgRef.current) return;
1571
+ imgRef.current.style.width = "100%";
1572
+ updateAttributes?.({ width: null });
1573
+ }, [updateAttributes]);
1574
+ return /* @__PURE__ */ jsx7(NodeViewWrapper6, { className: "image-block-wrapper", children: /* @__PURE__ */ jsxs6(
1575
+ Box6,
1576
+ {
1577
+ pos: "relative",
1578
+ display: "inline-block",
1579
+ style: {
1580
+ maxWidth: "100%",
1581
+ borderRadius: 8,
1582
+ overflow: "visible"
1583
+ },
1584
+ children: [
1585
+ selected && /* @__PURE__ */ jsxs6(Fragment, { children: [
1586
+ /* @__PURE__ */ jsxs6(
1587
+ Box6,
1588
+ {
1589
+ pos: "absolute",
1590
+ top: 8,
1591
+ right: 8,
1592
+ style: {
1593
+ zIndex: 2,
1594
+ display: "flex",
1595
+ gap: 4
1596
+ },
1597
+ children: [
1598
+ /* @__PURE__ */ jsx7(
1599
+ ActionIcon5,
1600
+ {
1601
+ color: isDark ? "dark" : "gray",
1602
+ variant: "filled",
1603
+ radius: "md",
1604
+ onClick: handleCopy,
1605
+ title: copied ? "Copied!" : "Copy image URL",
1606
+ bg: isDark ? theme.colors.dark[7] : theme.white,
1607
+ style: {
1608
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)"
1609
+ },
1610
+ children: copied ? /* @__PURE__ */ jsx7(IconCheck4, { color: "green", size: 16 }) : /* @__PURE__ */ jsx7(IconCopy2, { size: 16 })
1611
+ }
1612
+ ),
1613
+ /* @__PURE__ */ jsx7(
1614
+ ActionIcon5,
1615
+ {
1616
+ color: isDark ? "dark" : "gray",
1617
+ variant: "filled",
1618
+ radius: "md",
1619
+ onClick: handleResetSize,
1620
+ title: "Reset to original size",
1621
+ bg: isDark ? theme.colors.dark[7] : theme.white,
1622
+ style: {
1623
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)"
1624
+ },
1625
+ children: /* @__PURE__ */ jsx7(IconArrowsMaximize, { size: 16 })
1626
+ }
1627
+ )
1628
+ ]
1629
+ }
1630
+ ),
1631
+ /* @__PURE__ */ jsx7(
1632
+ Box6,
1633
+ {
1634
+ pos: "absolute",
1635
+ bottom: 0,
1636
+ right: 0,
1637
+ style: {
1638
+ zIndex: 2,
1639
+ width: RESIZE_HANDLE_SIZE,
1640
+ height: RESIZE_HANDLE_SIZE,
1641
+ cursor: "ew-resize",
1642
+ transform: "translate(50%, 50%)"
1643
+ },
1644
+ onMouseDown: handleResizeStart,
1645
+ children: /* @__PURE__ */ jsx7(
1646
+ Box6,
1647
+ {
1648
+ w: "100%",
1649
+ h: "100%",
1650
+ bg: theme.colors.blue[6],
1651
+ style: {
1652
+ borderRadius: "50%",
1653
+ border: `2px solid ${theme.white}`,
1654
+ boxShadow: "0 1px 4px rgba(0, 0, 0, 0.3)"
1655
+ }
1656
+ }
1657
+ )
1658
+ }
1659
+ )
1660
+ ] }),
1661
+ /* @__PURE__ */ jsx7(
1662
+ "img",
1663
+ {
1664
+ ref: imgRef,
1665
+ src,
1666
+ alt,
1667
+ style: {
1668
+ display: "block",
1669
+ maxWidth: "100%",
1670
+ width: width ? `${width}px` : "100%",
1671
+ height: "auto",
1672
+ borderRadius: 8,
1673
+ border: selected ? `1px solid ${theme.colors.blue[6]}` : "none",
1674
+ boxShadow: selected ? `0 0 0 1px ${theme.colors.blue[6]}` : "none",
1675
+ cursor: isResizing ? "ew-resize" : "default",
1676
+ transition: isResizing ? "none" : "border 0.2s, box-shadow 0.2s"
1677
+ }
1678
+ }
1679
+ ),
1680
+ alt && /* @__PURE__ */ jsx7(
1681
+ Text4,
1682
+ {
1683
+ size: "xs",
1684
+ c: isDark ? theme.colors.dark[2] : theme.colors.gray[6],
1685
+ ta: "center",
1686
+ mt: 4,
1687
+ style: {
1688
+ fontStyle: "italic"
1689
+ },
1690
+ children: alt
1691
+ }
1692
+ )
1693
+ ]
1694
+ }
1695
+ ) });
1696
+ });
1697
+ ImageBlock.displayName = "ImageBlock";
1698
+
1699
+ // src/editor/extensions/ImageExtension.ts
1700
+ var ImageExtension = Image.extend({
1701
+ addNodeView() {
1702
+ return ReactNodeViewRenderer6(ImageBlock);
1703
+ },
1704
+ addAttributes() {
1705
+ return {
1706
+ src: {
1707
+ default: null
1708
+ },
1709
+ alt: {
1710
+ default: ""
1711
+ },
1712
+ title: {
1713
+ default: ""
1714
+ },
1715
+ width: {
1716
+ default: null,
1717
+ parseHTML: (el) => el.getAttribute("width") || null,
1718
+ renderHTML: (attrs) => {
1719
+ if (!attrs.width) {
1720
+ return {};
1721
+ }
1722
+ return {
1723
+ width: attrs.width
1724
+ };
1725
+ }
1726
+ }
1727
+ };
1728
+ }
1729
+ });
1730
+
1731
+ // src/editor/hooks/useEditor.ts
1732
+ import BulletList from "@tiptap/extension-bullet-list";
1733
+ import OrderedList from "@tiptap/extension-ordered-list";
1734
+ import ListItem from "@tiptap/extension-list-item";
1735
+ var useEditorWithStorage = ({
1736
+ initialContent,
1737
+ onContentChange,
1738
+ debounceDelay = 500,
1739
+ outputFormat = "html",
1740
+ extensions = [],
1741
+ ...editorOptions
1742
+ }) => {
1743
+ const debounceRef = useRef3(null);
1744
+ const initialContentRef = useRef3(initialContent);
1745
+ const hasSetInitialContent = useRef3(false);
1746
+ const isUpdatingRef = useRef3(false);
1747
+ const lastEmittedContentRef = useRef3("");
1748
+ const getContent = (editor2) => {
1749
+ if (outputFormat === "json") {
1750
+ return JSON.stringify(editor2.getJSON());
1751
+ }
1752
+ const content = outputFormat === "markdown" ? serializeToMarkdown(editor2) : editor2.getHTML();
1753
+ return content;
1754
+ };
1755
+ const editor = useEditor({
1756
+ ...editorOptions,
1757
+ extensions: [
1758
+ StarterKit.configure({
1759
+ codeBlock: false,
1760
+ bulletList: false,
1761
+ orderedList: false,
1762
+ listItem: false
1763
+ }),
1764
+ Color,
1765
+ TextStyle,
1766
+ BackgroundColorMark,
1767
+ CodeBlockExtension,
1768
+ BulletList.configure({
1769
+ itemTypeName: "listItem"
1770
+ }),
1771
+ OrderedList.configure({
1772
+ itemTypeName: "listItem"
1773
+ }),
1774
+ ListItem,
1775
+ TaskList.configure({
1776
+ itemTypeName: "taskItem"
1777
+ }),
1778
+ TaskItemExtension.configure({
1779
+ nested: true
1780
+ }),
1781
+ NoticeExtension,
1782
+ TimelineExtension,
1783
+ DiagramExtension,
1784
+ Markdown,
1785
+ Table.configure({
1786
+ resizable: true
1787
+ }),
1788
+ TableRow,
1789
+ TableHeader,
1790
+ TableCell,
1791
+ ImageExtension,
1792
+ ...extensions
1793
+ ],
1794
+ content: (() => {
1795
+ if (initialContent) {
1796
+ try {
1797
+ const parsed = JSON.parse(initialContent);
1798
+ if (parsed.type === "doc") {
1799
+ return parsed;
1800
+ }
1801
+ } catch {
1802
+ }
1803
+ }
1804
+ return initialContent;
1805
+ })(),
1806
+ onCreate: ({ editor: editor2 }) => {
1807
+ if (outputFormat === "json") {
1808
+ lastEmittedContentRef.current = JSON.stringify(editor2.getJSON());
1809
+ } else if (outputFormat === "markdown") {
1810
+ lastEmittedContentRef.current = serializeToMarkdown(editor2);
1811
+ } else {
1812
+ lastEmittedContentRef.current = editor2.getHTML();
1813
+ }
1814
+ },
1815
+ onUpdate: ({ editor: editor2 }) => {
1816
+ if (!onContentChange || isUpdatingRef.current || !hasSetInitialContent.current) return;
1817
+ if (debounceRef.current) {
1818
+ clearTimeout(debounceRef.current);
1819
+ }
1820
+ debounceRef.current = setTimeout(() => {
1821
+ const newContent = getContent(editor2);
1822
+ if (newContent === lastEmittedContentRef.current) {
1823
+ debug.log("[useEditorWithStorage] Content unchanged, skipping");
1824
+ return;
1825
+ }
1826
+ lastEmittedContentRef.current = newContent;
1827
+ onContentChange(newContent);
1828
+ }, debounceDelay);
1829
+ }
1830
+ });
1831
+ useEffect3(() => {
1832
+ if (!editor) return;
1833
+ if (hasSetInitialContent.current) return;
1834
+ hasSetInitialContent.current = true;
1835
+ lastEmittedContentRef.current = initialContent || "";
1836
+ }, [editor]);
1837
+ useEffect3(() => {
1838
+ if (!editor || !initialContent) return;
1839
+ if (hasSetInitialContent.current) return;
1840
+ const currentContent = outputFormat === "markdown" ? serializeToMarkdown(editor) : editor.getHTML();
1841
+ if (currentContent.trim() !== initialContent.trim()) {
1842
+ try {
1843
+ const jsonContent = JSON.parse(initialContent);
1844
+ editor.commands.setContent(jsonContent);
1845
+ } catch {
1846
+ editor.commands.setContent(initialContent);
1847
+ }
1848
+ hasSetInitialContent.current = true;
1849
+ lastEmittedContentRef.current = initialContent;
1850
+ }
1851
+ }, [editor]);
1852
+ useEffect3(() => {
1853
+ if (!editor || !initialContent) return;
1854
+ if (initialContent === lastEmittedContentRef.current) return;
1855
+ if (debounceRef.current) {
1856
+ clearTimeout(debounceRef.current);
1857
+ debounceRef.current = null;
1858
+ }
1859
+ isUpdatingRef.current = true;
1860
+ try {
1861
+ const jsonContent = JSON.parse(initialContent);
1862
+ editor.commands.setContent(jsonContent);
1863
+ } catch {
1864
+ editor.commands.setContent(initialContent);
1865
+ }
1866
+ isUpdatingRef.current = false;
1867
+ initialContentRef.current = initialContent;
1868
+ lastEmittedContentRef.current = initialContent;
1869
+ }, [initialContent, editor]);
1870
+ useEffect3(() => {
1871
+ return () => {
1872
+ if (debounceRef.current) {
1873
+ clearTimeout(debounceRef.current);
1874
+ }
1875
+ };
1876
+ }, []);
1877
+ return editor;
1878
+ };
1879
+
1880
+ // src/editor/components/TableOfContentsExtension.ts
1881
+ import { Extension } from "@tiptap/react";
1882
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
1883
+ var generateId2 = (text, index) => {
1884
+ const base = text.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
1885
+ return base ? `heading-${base}-${index}` : `heading-${index}`;
1886
+ };
1887
+ var TableOfContents = Extension.create({
1888
+ name: "tableOfContents",
1889
+ addOptions() {
1890
+ return {
1891
+ onUpdate: void 0
1892
+ };
1893
+ },
1894
+ onBeforeCreate() {
1895
+ setTimeout(() => {
1896
+ const headings = [];
1897
+ const { doc } = this.editor.state;
1898
+ const headingCounters = {};
1899
+ doc.descendants((node, pos) => {
1900
+ if (node.type.name === "heading") {
1901
+ const level = node.attrs.level || 1;
1902
+ if (!headingCounters[level]) {
1903
+ headingCounters[level] = 0;
1904
+ }
1905
+ headingCounters[level]++;
1906
+ const existingId = node.attrs.id;
1907
+ const id = existingId || generateId2(node.textContent, headingCounters[level]);
1908
+ headings.push({
1909
+ id,
1910
+ text: node.textContent,
1911
+ level,
1912
+ pos
1913
+ });
1914
+ }
1915
+ });
1916
+ if (this.options.onUpdate && headings.length > 0) {
1917
+ this.options.onUpdate(headings);
1918
+ }
1919
+ }, 0);
1920
+ },
1921
+ addProseMirrorPlugins() {
1922
+ const { editor } = this;
1923
+ const getHeadings = () => {
1924
+ const headings = [];
1925
+ const { doc } = editor.state;
1926
+ const headingCounters = {};
1927
+ doc.descendants((node, pos) => {
1928
+ if (node.type.name === "heading") {
1929
+ const level = node.attrs.level || 1;
1930
+ if (!headingCounters[level]) {
1931
+ headingCounters[level] = 0;
1932
+ }
1933
+ headingCounters[level]++;
1934
+ const existingId = node.attrs.id;
1935
+ const id = existingId || generateId2(node.textContent, headingCounters[level]);
1936
+ headings.push({
1937
+ id,
1938
+ text: node.textContent,
1939
+ level,
1940
+ pos
1941
+ });
1942
+ }
1943
+ });
1944
+ return headings;
1945
+ };
1946
+ const updateHeadings = () => {
1947
+ const headings = getHeadings();
1948
+ if (this.options.onUpdate) {
1949
+ this.options.onUpdate(headings);
1950
+ }
1951
+ };
1952
+ return [
1953
+ new Plugin({
1954
+ key: new PluginKey("tableOfContents"),
1955
+ onTransaction({ transaction }) {
1956
+ if (transaction.docChanged || transaction.selectionSet) {
1957
+ setTimeout(updateHeadings, 0);
1958
+ }
1959
+ }
1960
+ })
1961
+ ];
1962
+ }
1963
+ });
1964
+ var TableOfContentsExtension_default = TableOfContents;
1965
+
1966
+ // src/editor/components/TableOfContents.tsx
1967
+ import { memo as memo5, useState as useState6, useEffect as useEffect5, useRef as useRef5, useCallback as useCallback7 } from "react";
1968
+ import { Box as Box7, Text as Text5, ScrollArea, Popover, useMantineTheme as useMantineTheme5 } from "@mantine/core";
1969
+ import { IconList } from "@tabler/icons-react";
1970
+
1971
+ // src/editor/components/ReadingIndicator.tsx
1972
+ import {
1973
+ memo as memo4,
1974
+ useState as useState5,
1975
+ useRef as useRef4,
1976
+ useCallback as useCallback6,
1977
+ useEffect as useEffect4
1978
+ } from "react";
1979
+ import { useMantineTheme as useMantineTheme4 } from "@mantine/core";
1980
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1981
+ var INDICATOR_RIGHT = 22;
1982
+ var INDICATOR_TOP = 420;
1983
+ var INDICATOR_MAX_H = "44vh";
1984
+ var BAR_WIDTH = 38;
1985
+ var BAR_HEIGHT = 5;
1986
+ var BAR_RADIUS = 999;
1987
+ var BAR_GAP = 6;
1988
+ var TOOLTIP_GAP = 10;
1989
+ var TOOLTIP_MAX_WIDTH = 240;
1990
+ var HeadingBar = memo4(function HeadingBar2({
1991
+ heading,
1992
+ isActive,
1993
+ isDark,
1994
+ onNavigate,
1995
+ onHoverEnter,
1996
+ onHoverLeave,
1997
+ barRef
1998
+ }) {
1999
+ const wrapperRef = useRef4(null);
2000
+ const activeColor = "#D4A017";
2001
+ const inactiveColor = isDark ? "rgba(255,255,255,0.18)" : "rgba(0,0,0,0.14)";
2002
+ const handleMouseEnter = useCallback6(() => {
2003
+ if (wrapperRef.current) onHoverEnter(heading, wrapperRef.current);
2004
+ }, [heading, onHoverEnter]);
2005
+ const handleFocus = useCallback6(() => {
2006
+ if (wrapperRef.current) onHoverEnter(heading, wrapperRef.current);
2007
+ }, [heading, onHoverEnter]);
2008
+ const handleTouchStart = useCallback6((e) => {
2009
+ e.preventDefault();
2010
+ if (wrapperRef.current) onHoverEnter(heading, wrapperRef.current);
2011
+ }, [heading, onHoverEnter]);
2012
+ const handleKeyDown = useCallback6((e) => {
2013
+ if (e.key === "Enter" || e.key === " ") {
2014
+ e.preventDefault();
2015
+ onNavigate(heading);
2016
+ }
2017
+ if (e.key === "Escape") {
2018
+ onHoverLeave();
2019
+ e.currentTarget.blur();
2020
+ }
2021
+ }, [heading, onNavigate, onHoverLeave]);
2022
+ return /* @__PURE__ */ jsx8(
2023
+ "div",
2024
+ {
2025
+ ref: (el) => {
2026
+ wrapperRef.current = el;
2027
+ barRef?.(el);
2028
+ },
2029
+ role: "menuitem",
2030
+ tabIndex: 0,
2031
+ "aria-label": heading.text || "(Empty heading)",
2032
+ "aria-current": isActive ? "location" : void 0,
2033
+ onMouseEnter: handleMouseEnter,
2034
+ onMouseLeave: onHoverLeave,
2035
+ onFocus: handleFocus,
2036
+ onBlur: onHoverLeave,
2037
+ onTouchStart: handleTouchStart,
2038
+ onClick: () => onNavigate(heading),
2039
+ onKeyDown: handleKeyDown,
2040
+ style: {
2041
+ display: "flex",
2042
+ justifyContent: "center",
2043
+ alignItems: "center",
2044
+ width: "100%",
2045
+ padding: "4px 0",
2046
+ cursor: "pointer",
2047
+ outline: "none",
2048
+ WebkitTapHighlightColor: "transparent",
2049
+ boxSizing: "border-box"
2050
+ },
2051
+ children: /* @__PURE__ */ jsx8(
2052
+ "div",
2053
+ {
2054
+ style: {
2055
+ width: BAR_WIDTH,
2056
+ height: BAR_HEIGHT,
2057
+ borderRadius: BAR_RADIUS,
2058
+ overflow: "hidden",
2059
+ pointerEvents: "none",
2060
+ boxShadow: isActive ? isDark ? "0 0 6px rgba(255,140,0,0.45), 0 1px 3px rgba(255,140,0,0.25)" : "0 0 5px rgba(255,120,0,0.35), 0 1px 2px rgba(255,120,0,0.20)" : "none"
2061
+ },
2062
+ children: /* @__PURE__ */ jsx8(
2063
+ "div",
2064
+ {
2065
+ style: {
2066
+ width: "100%",
2067
+ height: "100%",
2068
+ borderRadius: BAR_RADIUS,
2069
+ backgroundColor: isActive ? activeColor : inactiveColor,
2070
+ opacity: isActive ? 1 : 0.38,
2071
+ transition: "background-color 200ms ease, opacity 200ms ease, transform 200ms ease",
2072
+ transform: isActive ? "scaleX(1.2)" : "scaleX(1)",
2073
+ transformOrigin: "center center",
2074
+ willChange: "transform"
2075
+ }
2076
+ }
2077
+ )
2078
+ }
2079
+ )
2080
+ }
2081
+ );
2082
+ });
2083
+ HeadingBar.displayName = "HeadingBar";
2084
+ var ReadingIndicator = memo4(function ReadingIndicator2({
2085
+ headings,
2086
+ activeIndex,
2087
+ isDark,
2088
+ onNavigate
2089
+ }) {
2090
+ const theme = useMantineTheme4();
2091
+ const navRef = useRef4(null);
2092
+ const barRefs = useRef4(/* @__PURE__ */ new Map());
2093
+ const [tooltip, setTooltip] = useState5({ visible: false, text: "", y: 0 });
2094
+ const hideTimer = useRef4(null);
2095
+ const handleHoverEnter = useCallback6((heading, el) => {
2096
+ if (hideTimer.current) {
2097
+ clearTimeout(hideTimer.current);
2098
+ hideTimer.current = null;
2099
+ }
2100
+ const rect = el.getBoundingClientRect();
2101
+ setTooltip({
2102
+ visible: true,
2103
+ text: heading.text || "(Empty heading)",
2104
+ y: rect.top + rect.height / 2
2105
+ });
2106
+ }, []);
2107
+ const handleHoverLeave = useCallback6(() => {
2108
+ hideTimer.current = setTimeout(() => setTooltip((t) => ({ ...t, visible: false })), 160);
2109
+ }, []);
2110
+ useEffect4(() => () => {
2111
+ if (hideTimer.current) clearTimeout(hideTimer.current);
2112
+ }, []);
2113
+ const setBarRef = useCallback6((index) => (el) => {
2114
+ if (el) barRefs.current.set(index, el);
2115
+ else barRefs.current.delete(index);
2116
+ }, []);
2117
+ useEffect4(() => {
2118
+ const nav = navRef.current;
2119
+ const el = barRefs.current.get(activeIndex);
2120
+ if (!nav || !el || activeIndex < 0) return;
2121
+ const navRect = nav.getBoundingClientRect();
2122
+ const elRect = el.getBoundingClientRect();
2123
+ const isAbove = elRect.top < navRect.top;
2124
+ const isBelow = elRect.bottom > navRect.bottom;
2125
+ if (!isAbove && !isBelow) return;
2126
+ const targetScrollTop = nav.scrollTop + (elRect.top - navRect.top) - navRect.height / 2 + elRect.height / 2;
2127
+ nav.scrollTo({ top: targetScrollTop, behavior: "smooth" });
2128
+ }, [activeIndex]);
2129
+ const handleContainerKeyDown = useCallback6((e) => {
2130
+ if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
2131
+ e.preventDefault();
2132
+ const focused = document.activeElement;
2133
+ const bars = navRef.current?.querySelectorAll('[role="menuitem"]');
2134
+ if (!bars) return;
2135
+ const arr = Array.from(bars);
2136
+ const idx = arr.indexOf(focused);
2137
+ if (idx === -1) return;
2138
+ const next = e.key === "ArrowDown" ? arr[Math.min(idx + 1, arr.length - 1)] : arr[Math.max(idx - 1, 0)];
2139
+ next?.focus();
2140
+ }, []);
2141
+ useEffect4(() => {
2142
+ const handleOutside = (e) => {
2143
+ if (navRef.current?.contains(e.target)) return;
2144
+ setTooltip((t) => ({ ...t, visible: false }));
2145
+ const focused = document.activeElement;
2146
+ if (navRef.current?.contains(focused)) focused.blur();
2147
+ };
2148
+ document.addEventListener("mousedown", handleOutside, { passive: true });
2149
+ document.addEventListener("touchstart", handleOutside, { passive: true });
2150
+ return () => {
2151
+ document.removeEventListener("mousedown", handleOutside);
2152
+ document.removeEventListener("touchstart", handleOutside);
2153
+ };
2154
+ }, []);
2155
+ if (headings.length === 0) return null;
2156
+ const tooltipBg = isDark ? theme.colors.dark[6] : "#ffffff";
2157
+ const tooltipBorder = isDark ? theme.colors.dark[4] : theme.colors.gray[3];
2158
+ const tooltipText = isDark ? theme.colors.gray[1] : theme.colors.dark[7];
2159
+ const tooltipRight = INDICATOR_RIGHT + BAR_WIDTH + TOOLTIP_GAP;
2160
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
2161
+ /* @__PURE__ */ jsxs7(
2162
+ "div",
2163
+ {
2164
+ style: {
2165
+ position: "fixed",
2166
+ right: INDICATOR_RIGHT,
2167
+ top: INDICATOR_TOP,
2168
+ width: BAR_WIDTH,
2169
+ zIndex: 10,
2170
+ display: "flex",
2171
+ flexDirection: "column",
2172
+ pointerEvents: "none"
2173
+ },
2174
+ children: [
2175
+ /* @__PURE__ */ jsx8("style", { children: `nav.ri-nav::-webkit-scrollbar { display: none; }` }),
2176
+ /* @__PURE__ */ jsx8(
2177
+ "nav",
2178
+ {
2179
+ ref: navRef,
2180
+ role: "navigation",
2181
+ "aria-label": "Document section navigator",
2182
+ className: "ri-nav",
2183
+ onKeyDown: handleContainerKeyDown,
2184
+ style: {
2185
+ width: BAR_WIDTH,
2186
+ maxHeight: INDICATOR_MAX_H,
2187
+ overflowY: "auto",
2188
+ overflowX: "visible",
2189
+ overscrollBehavior: "contain",
2190
+ pointerEvents: "auto",
2191
+ scrollbarWidth: "none",
2192
+ msOverflowStyle: "none",
2193
+ display: "flex",
2194
+ flexDirection: "column",
2195
+ alignItems: "flex-end",
2196
+ gap: BAR_GAP,
2197
+ paddingTop: 2,
2198
+ paddingBottom: 2
2199
+ },
2200
+ children: headings.map((heading, index) => /* @__PURE__ */ jsx8(
2201
+ HeadingBar,
2202
+ {
2203
+ heading,
2204
+ isActive: index === activeIndex,
2205
+ isDark,
2206
+ onNavigate,
2207
+ onHoverEnter: handleHoverEnter,
2208
+ onHoverLeave: handleHoverLeave,
2209
+ barRef: setBarRef(index)
2210
+ },
2211
+ `${heading.id}-${index}`
2212
+ ))
2213
+ }
2214
+ )
2215
+ ]
2216
+ }
2217
+ ),
2218
+ /* @__PURE__ */ jsx8(
2219
+ "div",
2220
+ {
2221
+ role: "tooltip",
2222
+ "aria-live": "polite",
2223
+ style: {
2224
+ position: "fixed",
2225
+ right: tooltipRight,
2226
+ top: tooltip.y,
2227
+ transform: tooltip.visible ? "translateY(-50%)" : "translateY(-50%) translateX(6px)",
2228
+ zIndex: 400,
2229
+ pointerEvents: "none",
2230
+ opacity: tooltip.visible ? 1 : 0,
2231
+ transition: "opacity 160ms ease, transform 160ms ease",
2232
+ willChange: "opacity, transform",
2233
+ maxWidth: TOOLTIP_MAX_WIDTH,
2234
+ whiteSpace: "nowrap"
2235
+ },
2236
+ children: /* @__PURE__ */ jsx8(
2237
+ "div",
2238
+ {
2239
+ style: {
2240
+ background: tooltipBg,
2241
+ border: `1px solid ${tooltipBorder}`,
2242
+ borderRadius: 7,
2243
+ padding: "4px 10px",
2244
+ boxShadow: isDark ? "0 4px 16px rgba(0,0,0,0.5), 0 1px 4px rgba(0,0,0,0.3)" : "0 4px 16px rgba(0,0,0,0.10), 0 1px 4px rgba(0,0,0,0.07)",
2245
+ color: tooltipText,
2246
+ fontSize: 12,
2247
+ fontWeight: 500,
2248
+ lineHeight: 1.4,
2249
+ maxWidth: TOOLTIP_MAX_WIDTH,
2250
+ overflow: "hidden",
2251
+ textOverflow: "ellipsis",
2252
+ letterSpacing: "0.01em"
2253
+ },
2254
+ children: tooltip.text
2255
+ }
2256
+ )
2257
+ }
2258
+ )
2259
+ ] });
2260
+ });
2261
+ ReadingIndicator.displayName = "ReadingIndicator";
2262
+
2263
+ // src/editor/components/TableOfContents.tsx
2264
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2265
+ var TableOfContentsPanel = memo5(({ headings, editor }) => {
2266
+ const theme = useMantineTheme5();
2267
+ const { currentTheme } = useTheme();
2268
+ const isDark = currentTheme === "dark";
2269
+ const [opened, setOpened] = useState6(false);
2270
+ const [activeId, setActiveId] = useState6(null);
2271
+ const targetRef = useRef5(null);
2272
+ const isProgrammaticScrollRef = useRef5(false);
2273
+ const ignoreBottomDetectionRef = useRef5(false);
2274
+ const lastClickedHeadingIdRef = useRef5(null);
2275
+ const computeActiveHeading = useCallback7(() => {
2276
+ if (isProgrammaticScrollRef.current) return;
2277
+ if (!editor?.view) return;
2278
+ const editorDom = editor.view.dom;
2279
+ const headingEls = editorDom.querySelectorAll("h1,h2,h3,h4,h5,h6");
2280
+ if (headingEls.length === 0 || headings.length === 0) return;
2281
+ const THRESHOLD = 80;
2282
+ const tops = Array.from(headingEls).map(
2283
+ (el) => el.getBoundingClientRect().top
2284
+ );
2285
+ let isAtBottom = false;
2286
+ let parent = editorDom.parentElement;
2287
+ while (parent) {
2288
+ const style = window.getComputedStyle(parent);
2289
+ const overflowY = style.overflowY;
2290
+ const isScrollable = overflowY === "auto" || overflowY === "scroll";
2291
+ if (isScrollable && parent.scrollHeight > parent.clientHeight + 10) {
2292
+ if (Math.ceil(parent.scrollTop + parent.clientHeight) >= parent.scrollHeight - 10) {
2293
+ isAtBottom = true;
2294
+ }
2295
+ break;
2296
+ }
2297
+ parent = parent.parentElement;
2298
+ }
2299
+ if (!isAtBottom || ignoreBottomDetectionRef.current) {
2300
+ const docEl = document.documentElement;
2301
+ if (docEl.scrollHeight > window.innerHeight + 10) {
2302
+ if (Math.ceil(window.innerHeight + window.scrollY) >= docEl.scrollHeight - 10) {
2303
+ isAtBottom = true;
2304
+ }
2305
+ }
2306
+ }
2307
+ let bestIdx = -1;
2308
+ if (isAtBottom) {
2309
+ if (lastClickedHeadingIdRef.current) {
2310
+ const clickedIdx = headings.findIndex((h) => h.id === lastClickedHeadingIdRef.current);
2311
+ if (clickedIdx !== -1) {
2312
+ bestIdx = clickedIdx;
2313
+ } else {
2314
+ bestIdx = headings.length - 1;
2315
+ }
2316
+ } else {
2317
+ bestIdx = headings.length - 1;
2318
+ }
2319
+ } else {
2320
+ for (let i = tops.length - 1; i >= 0; i--) {
2321
+ if (tops[i] <= THRESHOLD) {
2322
+ bestIdx = i;
2323
+ break;
2324
+ }
2325
+ }
2326
+ if (bestIdx === -1) {
2327
+ const viewportH = window.innerHeight;
2328
+ let closestTop = Infinity;
2329
+ for (let i = 0; i < tops.length; i++) {
2330
+ if (tops[i] < viewportH && tops[i] < closestTop) {
2331
+ closestTop = tops[i];
2332
+ bestIdx = i;
2333
+ }
2334
+ }
2335
+ if (bestIdx === -1) bestIdx = 0;
2336
+ }
2337
+ }
2338
+ const matched = headings[bestIdx];
2339
+ if (matched) setActiveId(matched.id);
2340
+ }, [editor, headings]);
2341
+ const computeActiveHeadingByCursor = useCallback7(() => {
2342
+ if (isProgrammaticScrollRef.current) return;
2343
+ if (!editor?.view) return;
2344
+ const { state } = editor;
2345
+ const cursorPos = state.selection.anchor;
2346
+ let bestIdx = -1;
2347
+ for (let i = headings.length - 1; i >= 0; i--) {
2348
+ if (headings[i].pos <= cursorPos) {
2349
+ bestIdx = i;
2350
+ break;
2351
+ }
2352
+ }
2353
+ if (bestIdx >= 0 && headings[bestIdx]) {
2354
+ setActiveId(headings[bestIdx].id);
2355
+ lastClickedHeadingIdRef.current = headings[bestIdx].id;
2356
+ }
2357
+ }, [editor, headings]);
2358
+ const computeActiveHeadingRef = useRef5(computeActiveHeading);
2359
+ const computeActiveHeadingByCursorRef = useRef5(computeActiveHeadingByCursor);
2360
+ useEffect5(() => {
2361
+ computeActiveHeadingRef.current = computeActiveHeading;
2362
+ computeActiveHeadingByCursorRef.current = computeActiveHeadingByCursor;
2363
+ });
2364
+ useEffect5(() => {
2365
+ if (!editor?.view) return;
2366
+ let rafId = 0;
2367
+ const onScroll = () => {
2368
+ if (rafId) return;
2369
+ rafId = requestAnimationFrame(() => {
2370
+ computeActiveHeadingRef.current();
2371
+ rafId = 0;
2372
+ });
2373
+ };
2374
+ window.addEventListener("scroll", onScroll, { capture: true, passive: true });
2375
+ window.addEventListener("resize", onScroll, { passive: true });
2376
+ const initId = setTimeout(() => {
2377
+ computeActiveHeadingRef.current();
2378
+ }, 200);
2379
+ return () => {
2380
+ clearTimeout(initId);
2381
+ if (rafId) cancelAnimationFrame(rafId);
2382
+ window.removeEventListener("scroll", onScroll, { capture: true });
2383
+ window.removeEventListener("resize", onScroll);
2384
+ };
2385
+ }, [editor]);
2386
+ useEffect5(() => {
2387
+ computeActiveHeadingRef.current();
2388
+ }, [headings]);
2389
+ useEffect5(() => {
2390
+ if (!editor) return;
2391
+ const handler = () => {
2392
+ if (!isProgrammaticScrollRef.current) {
2393
+ computeActiveHeadingByCursorRef.current();
2394
+ }
2395
+ };
2396
+ editor.on("selectionUpdate", handler);
2397
+ return () => editor.off("selectionUpdate", handler);
2398
+ }, [editor]);
2399
+ useEffect5(() => {
2400
+ const onKey = (e) => {
2401
+ if (e.key === "Escape" && opened) setOpened(false);
2402
+ };
2403
+ document.addEventListener("keydown", onKey);
2404
+ return () => document.removeEventListener("keydown", onKey);
2405
+ }, [opened]);
2406
+ useEffect5(() => {
2407
+ const clearLastClicked = () => {
2408
+ lastClickedHeadingIdRef.current = null;
2409
+ };
2410
+ window.addEventListener("wheel", clearLastClicked, { passive: true });
2411
+ window.addEventListener("touchmove", clearLastClicked, { passive: true });
2412
+ window.addEventListener("keydown", clearLastClicked, { passive: true });
2413
+ window.addEventListener("mousedown", clearLastClicked, { passive: true });
2414
+ return () => {
2415
+ window.removeEventListener("wheel", clearLastClicked);
2416
+ window.removeEventListener("touchmove", clearLastClicked);
2417
+ window.removeEventListener("keydown", clearLastClicked);
2418
+ window.removeEventListener("mousedown", clearLastClicked);
2419
+ };
2420
+ }, []);
2421
+ const handleClick = useCallback7((heading) => {
2422
+ if (!editor) return;
2423
+ const { pos } = heading;
2424
+ setActiveId(heading.id);
2425
+ lastClickedHeadingIdRef.current = heading.id;
2426
+ isProgrammaticScrollRef.current = true;
2427
+ ignoreBottomDetectionRef.current = true;
2428
+ editor.chain().focus().setTextSelection(pos).run();
2429
+ setTimeout(() => {
2430
+ const node = editor.view.nodeDOM(pos);
2431
+ if (node) node.scrollIntoView({ behavior: "smooth", block: "start" });
2432
+ setTimeout(() => {
2433
+ isProgrammaticScrollRef.current = false;
2434
+ ignoreBottomDetectionRef.current = false;
2435
+ computeActiveHeadingRef.current();
2436
+ }, 650);
2437
+ }, 50);
2438
+ }, [editor]);
2439
+ const getIndent = (level) => level === 1 ? 5 : (level - 1) * 12;
2440
+ const getHeadingPrefix = (level, index) => {
2441
+ const previousHeadings = headings.slice(0, index);
2442
+ let parentIndex = -1;
2443
+ for (let i = previousHeadings.length - 1; i >= 0; i--) {
2444
+ if (previousHeadings[i].level < level) {
2445
+ parentIndex = i;
2446
+ break;
2447
+ }
2448
+ }
2449
+ const sameLevelAfterParent = previousHeadings.slice(parentIndex + 1).filter((h) => h.level === level).length + 1;
2450
+ switch (level) {
2451
+ case 1:
2452
+ return " ";
2453
+ case 2: {
2454
+ const roman = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"];
2455
+ return sameLevelAfterParent <= 10 ? `${roman[sameLevelAfterParent - 1]}.` : `${sameLevelAfterParent}.`;
2456
+ }
2457
+ case 3:
2458
+ return `${sameLevelAfterParent}.`;
2459
+ case 4:
2460
+ return ">";
2461
+ case 5:
2462
+ return "-";
2463
+ case 6:
2464
+ return "*";
2465
+ default:
2466
+ return `${sameLevelAfterParent}.`;
2467
+ }
2468
+ };
2469
+ const getHeadingColor = (level) => {
2470
+ const colors = {
2471
+ 1: "#cca415",
2472
+ 2: "#2989d8",
2473
+ 3: "#c7701f",
2474
+ 4: "#188831",
2475
+ 5: "#d31f1f",
2476
+ 6: "#149cb1"
2477
+ };
2478
+ return colors[level] || "inherit";
2479
+ };
2480
+ const buttonBackground = isDark ? theme.colors.dark[9] : theme.colors.blue[0];
2481
+ const buttonBackgroundOpened = isDark ? theme.colors.blue[8] : theme.colors.blue[1];
2482
+ const borderColor = isDark ? theme.colors.dark[4] : theme.colors.gray[3];
2483
+ const iconColor = isDark ? theme.colors.blue[3] : theme.colors.dark[9];
2484
+ const iconColorOpened = isDark ? theme.colors.blue[2] : theme.colors.blue[7];
2485
+ const dropdownBackground = isDark ? "#1d1d1d" : "#ffffff";
2486
+ const activeItemBackground = isDark ? theme.colors.dark[5] : theme.colors.blue[0];
2487
+ const hoverItemBackground = isDark ? theme.colors.dark[6] : theme.colors.gray[1];
2488
+ const activeIndex = headings.findIndex((h) => h.id === activeId);
2489
+ if (headings.length === 0) return null;
2490
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2491
+ /* @__PURE__ */ jsx9(
2492
+ ReadingIndicator,
2493
+ {
2494
+ headings,
2495
+ activeIndex,
2496
+ isDark,
2497
+ onNavigate: handleClick
2498
+ }
2499
+ ),
2500
+ /* @__PURE__ */ jsxs8(
2501
+ Popover,
2502
+ {
2503
+ opened,
2504
+ onChange: setOpened,
2505
+ position: "left-start",
2506
+ offset: 10,
2507
+ withArrow: true,
2508
+ shadow: "md",
2509
+ withinPortal: true,
2510
+ closeOnClickOutside: true,
2511
+ styles: {
2512
+ dropdown: {
2513
+ padding: 0,
2514
+ overflow: "hidden",
2515
+ borderRadius: theme.radius.lg,
2516
+ border: `1px solid ${borderColor}`
2517
+ }
2518
+ },
2519
+ children: [
2520
+ /* @__PURE__ */ jsx9(Popover.Target, { children: /* @__PURE__ */ jsx9(
2521
+ Box7,
2522
+ {
2523
+ ref: targetRef,
2524
+ onClick: (e) => {
2525
+ e.stopPropagation();
2526
+ setOpened((o) => !o);
2527
+ },
2528
+ style: {
2529
+ position: "fixed",
2530
+ right: 18,
2531
+ top: 65,
2532
+ width: 42,
2533
+ height: 42,
2534
+ borderRadius: "22px",
2535
+ background: opened ? buttonBackgroundOpened : buttonBackground,
2536
+ border: `1px solid ${borderColor}`,
2537
+ display: "flex",
2538
+ alignItems: "center",
2539
+ justifyContent: "center",
2540
+ cursor: "pointer",
2541
+ zIndex: 100,
2542
+ transition: "all 0.2s ease",
2543
+ boxShadow: opened ? `0 2px 8px ${isDark ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.15)"}` : `0 1px 3px ${isDark ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.1)"}`
2544
+ },
2545
+ title: "Table of contents",
2546
+ children: /* @__PURE__ */ jsx9(
2547
+ IconList,
2548
+ {
2549
+ size: 20,
2550
+ color: opened ? iconColorOpened : iconColor,
2551
+ stroke: 1.5
2552
+ }
2553
+ )
2554
+ }
2555
+ ) }),
2556
+ /* @__PURE__ */ jsx9(Popover.Dropdown, { bg: dropdownBackground, children: /* @__PURE__ */ jsx9(
2557
+ Box7,
2558
+ {
2559
+ style: {
2560
+ width: "auto",
2561
+ minWidth: 400,
2562
+ display: "flex",
2563
+ flexDirection: "column",
2564
+ backgroundColor: dropdownBackground
2565
+ },
2566
+ children: /* @__PURE__ */ jsx9(
2567
+ ScrollArea.Autosize,
2568
+ {
2569
+ mah: 520,
2570
+ scrollbarSize: 4,
2571
+ offsetScrollbars: false,
2572
+ type: "hover",
2573
+ styles: {
2574
+ scrollbar: {
2575
+ '&[data-orientation="vertical"]': { padding: 0, right: 2 }
2576
+ },
2577
+ thumb: {
2578
+ backgroundColor: isDark ? theme.colors.dark[3] : theme.colors.gray[5]
2579
+ }
2580
+ },
2581
+ children: /* @__PURE__ */ jsx9(Box7, { px: 12, py: 15, children: headings.map((heading, index) => /* @__PURE__ */ jsx9(
2582
+ Box7,
2583
+ {
2584
+ style: {
2585
+ padding: "5px 5px",
2586
+ paddingLeft: `${getIndent(heading.level)}px`,
2587
+ borderRadius: theme.radius.md,
2588
+ cursor: "pointer",
2589
+ background: activeId === heading.id ? activeItemBackground : "transparent",
2590
+ borderLeft: activeId === heading.id ? `3px solid ${theme.colors.blue[6]}` : "3px solid transparent",
2591
+ transition: "all 0.15s ease",
2592
+ marginBottom: 2
2593
+ },
2594
+ onClick: () => handleClick(heading),
2595
+ onMouseEnter: (e) => {
2596
+ if (activeId !== heading.id) e.currentTarget.style.background = hoverItemBackground;
2597
+ },
2598
+ onMouseLeave: (e) => {
2599
+ if (activeId !== heading.id) e.currentTarget.style.background = "transparent";
2600
+ },
2601
+ children: /* @__PURE__ */ jsxs8(
2602
+ Text5,
2603
+ {
2604
+ size: "sm",
2605
+ truncate: true,
2606
+ style: {
2607
+ color: getHeadingColor(heading.level),
2608
+ fontWeight: heading.level <= 2 ? 600 : 400,
2609
+ fontSize: heading.level <= 2 ? "15px" : "13px",
2610
+ display: "flex",
2611
+ alignItems: "center",
2612
+ gap: "4px"
2613
+ },
2614
+ children: [
2615
+ getHeadingPrefix(heading.level, index) && /* @__PURE__ */ jsx9(
2616
+ Text5,
2617
+ {
2618
+ component: "span",
2619
+ size: "xs",
2620
+ style: {
2621
+ color: getHeadingColor(heading.level),
2622
+ fontWeight: 500,
2623
+ whiteSpace: "nowrap"
2624
+ },
2625
+ children: getHeadingPrefix(heading.level, index)
2626
+ }
2627
+ ),
2628
+ heading.text || "(Empty heading)"
2629
+ ]
2630
+ }
2631
+ )
2632
+ },
2633
+ `${heading.id}-${index}`
2634
+ )) })
2635
+ }
2636
+ )
2637
+ }
2638
+ ) })
2639
+ ]
2640
+ }
2641
+ )
2642
+ ] });
2643
+ });
2644
+ TableOfContentsPanel.displayName = "TableOfContentsPanel";
2645
+
2646
+ // src/editor/hooks/pasteHandler.ts
2647
+ import TurndownService from "turndown";
2648
+ var turndown = new TurndownService({
2649
+ headingStyle: "atx",
2650
+ codeBlockStyle: "fenced",
2651
+ hr: "---"
2652
+ });
2653
+ turndown.addRule("taskListCheckbox", {
2654
+ filter: (node) => {
2655
+ const hasCheckbox = node.querySelector('input[type="checkbox"]') !== null;
2656
+ return hasCheckbox;
2657
+ },
2658
+ replacement: (content, node) => {
2659
+ const checkbox = node.querySelector('input[type="checkbox"]');
2660
+ const checked = checkbox?.checked || checkbox?.getAttribute("checked") !== null;
2661
+ const text = content.replace(/^\s*\[[ xX]\]\s*/, "").trim();
2662
+ const contentText = text || "\u200B";
2663
+ return `- [${checked ? "x" : " "}] ${contentText}
2664
+ `;
2665
+ }
2666
+ });
2667
+ turndown.addRule("taskListText", {
2668
+ filter: (node) => {
2669
+ if (node.nodeName !== "LI") return false;
2670
+ const text = node.textContent || "";
2671
+ return /^\s*\[[ xX]\]/.test(text);
2672
+ },
2673
+ replacement: (_content, node) => {
2674
+ const text = node.textContent || "";
2675
+ const match = text.match(/^\s*\[([ xX])\]\s*(.*)/);
2676
+ if (!match) return "";
2677
+ const checked = match[1].toLowerCase() === "x";
2678
+ const cleanText = match[2].trim();
2679
+ const content = cleanText || "\u200B";
2680
+ return `- [${checked ? "x" : " "}] ${content}
2681
+ `;
2682
+ }
2683
+ });
2684
+ turndown.addRule("listItem", {
2685
+ filter: (node) => {
2686
+ if (node.nodeName !== "LI") return false;
2687
+ const text = node.textContent || "";
2688
+ if (/^\s*\[[ xX]\]/.test(text)) return false;
2689
+ if (node.querySelector('input[type="checkbox"]')) return false;
2690
+ return true;
2691
+ },
2692
+ replacement: (content, node) => {
2693
+ const parent = node.parentNode;
2694
+ if (parent.nodeName === "OL") {
2695
+ const startAttr = parent.getAttribute("start");
2696
+ let number;
2697
+ if (startAttr) {
2698
+ number = parseInt(startAttr, 10);
2699
+ } else {
2700
+ const items = Array.from(parent.children).filter((child) => child.nodeName === "LI");
2701
+ number = items.indexOf(node) + 1;
2702
+ }
2703
+ return `${number}. ${content.trim()}
2704
+ `;
2705
+ }
2706
+ return "- " + content.trim() + "\n";
2707
+ }
2708
+ });
2709
+ turndown.addRule("table", {
2710
+ filter: "table",
2711
+ replacement: (_content, node) => {
2712
+ const rows = node.querySelectorAll("tr");
2713
+ if (!rows.length) return "\n\n";
2714
+ const markdownRows = [];
2715
+ let hasTh = false;
2716
+ for (const row of rows) {
2717
+ const cells = row.querySelectorAll("th, td");
2718
+ if (!cells.length) continue;
2719
+ const isHeader = !hasTh && row.querySelector("th") !== null;
2720
+ if (isHeader) hasTh = true;
2721
+ const cellTexts = Array.from(
2722
+ cells,
2723
+ (cell) => (cell.textContent || "").trim().replace(/\|/g, "\\|")
2724
+ );
2725
+ markdownRows.push("| " + cellTexts.join(" | ") + " |");
2726
+ if (isHeader) {
2727
+ markdownRows.push("| " + cellTexts.map(() => "---").join(" | ") + " |");
2728
+ }
2729
+ }
2730
+ if (!hasTh && markdownRows.length > 0) {
2731
+ const colCount = rows[0].querySelectorAll("th, td").length;
2732
+ markdownRows.splice(1, 0, "| " + Array.from({ length: colCount }, () => "---").join(" | ") + " |");
2733
+ }
2734
+ return "\n" + markdownRows.join("\n") + "\n";
2735
+ }
2736
+ });
2737
+ ["TH", "TD"].forEach(
2738
+ (tag) => turndown.addRule(`tableCell_${tag}`, {
2739
+ filter: (node) => node.nodeName === tag,
2740
+ replacement: (content) => content
2741
+ })
2742
+ );
2743
+ ["THEAD", "TBODY", "TFOOT", "TR"].forEach(
2744
+ (tag) => turndown.addRule(`tableSection_${tag}`, {
2745
+ filter: (node) => node.nodeName === tag,
2746
+ replacement: (content) => content
2747
+ })
2748
+ );
2749
+ turndown.addRule("colorSpan", {
2750
+ filter: (node) => node.nodeName === "SPAN" && !!node.style.color,
2751
+ replacement: (content, node) => {
2752
+ return `<span style="color: ${node.style.color}">${content}</span>`;
2753
+ }
2754
+ });
2755
+ var handlePaste = (editor, event) => {
2756
+ const clipboard = event.clipboardData;
2757
+ if (!clipboard) return false;
2758
+ const html = clipboard.getData("text/html");
2759
+ const text = clipboard.getData("text/plain");
2760
+ for (const item of clipboard.items) {
2761
+ if (item.type.startsWith("image")) {
2762
+ const file = item.getAsFile();
2763
+ if (file && editor) {
2764
+ const reader = new FileReader();
2765
+ reader.onload = (e) => {
2766
+ editor.chain().focus().setImage({ src: e.target?.result }).run();
2767
+ };
2768
+ reader.readAsDataURL(file);
2769
+ event.preventDefault();
2770
+ return true;
2771
+ }
2772
+ }
2773
+ }
2774
+ if (html && editor) {
2775
+ const markdown = turndown.turndown(html).replace(/\n{3,}/g, "\n\n");
2776
+ editor.chain().focus().insertContent(markdown).run();
2777
+ event.preventDefault();
2778
+ return true;
2779
+ }
2780
+ if (text && editor) {
2781
+ editor.chain().focus().insertContent(text).run();
2782
+ event.preventDefault();
2783
+ return true;
2784
+ }
2785
+ return false;
2786
+ };
2787
+
2788
+ // src/editor/MarkdownEditor.tsx
2789
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2790
+ var MarkdownEditor = memo6(function MarkdownEditor2({
2791
+ initialContent = "<p></p>",
2792
+ onChange,
2793
+ outputFormat = "json",
2794
+ showTableOfContents = true,
2795
+ showHeadingDividers = true,
2796
+ minHeight = 200,
2797
+ editable = true
2798
+ }) {
2799
+ const [headings, setHeadings] = useState7([]);
2800
+ const handleHeadingsUpdate = useCallback8((newHeadings) => {
2801
+ setHeadings(newHeadings);
2802
+ }, []);
2803
+ const slashCommand = useMemo2(() => {
2804
+ return createSlashCommand({
2805
+ char: "/",
2806
+ items: ({ editor: editor2, query }) => {
2807
+ const blocks = getDefaultBlockItems(editor2);
2808
+ if (!query) return blocks;
2809
+ return blocks.filter(
2810
+ (item) => item.title.toLowerCase().includes(query.toLowerCase()) || item.description?.toLowerCase().includes(query.toLowerCase()) || item.keywords?.some((k) => k.toLowerCase().includes(query.toLowerCase()))
2811
+ );
2812
+ }
2813
+ });
2814
+ }, []);
2815
+ const editor = useEditorWithStorage({
2816
+ initialContent,
2817
+ onContentChange: onChange,
2818
+ outputFormat,
2819
+ extensions: [
2820
+ ...showHeadingDividers ? [] : [],
2821
+ ...showTableOfContents ? [TableOfContentsExtension_default.configure({ onUpdate: handleHeadingsUpdate })] : [],
2822
+ slashCommand
2823
+ ],
2824
+ editable,
2825
+ editorProps: {
2826
+ attributes: {
2827
+ spellCheck: "false",
2828
+ style: "outline:none;border:none;font-size: 15px;"
2829
+ },
2830
+ handleDOMEvents: {
2831
+ paste: (_view, event) => {
2832
+ return handlePaste(editor, event);
2833
+ }
2834
+ }
2835
+ }
2836
+ });
2837
+ useEffect6(() => {
2838
+ if (editor) {
2839
+ editor.setEditable(editable);
2840
+ }
2841
+ }, [editor, editable]);
2842
+ const { colorScheme: rawColorScheme } = useMantineColorScheme3();
2843
+ const theme = useMantineTheme6();
2844
+ const colorScheme = resolveColorScheme(rawColorScheme);
2845
+ const bgColor = colorScheme === "dark" ? theme.colors.dark[8] : theme.colors.gray[0];
2846
+ if (!editor) return null;
2847
+ return /* @__PURE__ */ jsxs9(Card, { shadow: "sm", padding: "md", radius: "18px", bg: bgColor, withBorder: true, style: { position: "relative" }, children: [
2848
+ showTableOfContents && /* @__PURE__ */ jsx10(TableOfContentsPanel, { headings, editor }),
2849
+ /* @__PURE__ */ jsx10(EditorContent, { editor, minHeight }),
2850
+ /* @__PURE__ */ jsx10(BubbleMenu, { editor })
2851
+ ] });
2852
+ });
2853
+ MarkdownEditor.displayName = "MarkdownEditor";
2854
+ var MarkdownEditor_default = MarkdownEditor;
2855
+
2856
+ // src/modals/DocumentEditor.tsx
2857
+ import { useState as useState8, useEffect as useEffect7 } from "react";
2858
+ import {
2859
+ Group as Group3,
2860
+ Text as Text6,
2861
+ Box as Box8,
2862
+ Modal,
2863
+ Button as Button3,
2864
+ Select as Select3,
2865
+ Stack as Stack2,
2866
+ Textarea as Textarea3,
2867
+ Alert as Alert3,
2868
+ useMantineTheme as useMantineTheme7,
2869
+ useMantineColorScheme as useMantineColorScheme4
2870
+ } from "@mantine/core";
2871
+ import { IconSparkles, IconAlertCircle as IconAlertCircle3 } from "@tabler/icons-react";
2872
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2873
+ function DocumentEditor({ editor, opened, onClose }) {
2874
+ const theme = useMantineTheme7();
2875
+ const { colorScheme } = useMantineColorScheme4();
2876
+ const isDark = colorScheme === "dark";
2877
+ const [aiPrompt, setAiPrompt] = useState8("");
2878
+ const [selectedTemplate, setSelectedTemplate] = useState8(null);
2879
+ const [aiLoading, setAiLoading] = useState8(false);
2880
+ const [aiError, setAiError] = useState8(null);
2881
+ const [selectedText, setSelectedText] = useState8("");
2882
+ const [instructions, setInstructions] = useState8([]);
2883
+ const [templatesLoading, setTemplatesLoading] = useState8(false);
2884
+ useEffect7(() => {
2885
+ if (opened) {
2886
+ const { from, to } = editor.state.selection;
2887
+ const markdownText = serializeSelectionToMarkdown(editor, from, to);
2888
+ setSelectedText(markdownText);
2889
+ const savedTemplate = localStorage.getItem("ai-rewrite-last-template");
2890
+ if (savedTemplate) {
2891
+ setSelectedTemplate(savedTemplate);
2892
+ } else if (instructions.length > 0) {
2893
+ const firstTemplate = instructions[0].name.toLowerCase();
2894
+ setSelectedTemplate(firstTemplate);
2895
+ }
2896
+ }
2897
+ }, [opened, editor, instructions]);
2898
+ useEffect7(() => {
2899
+ if (!opened) return;
2900
+ const fetchInstructions = async () => {
2901
+ try {
2902
+ setTemplatesLoading(true);
2903
+ const response = await instructionsApi.getAll();
2904
+ if (response.success && "data" in response) {
2905
+ setInstructions(response.data);
2906
+ }
2907
+ } catch (error) {
2908
+ console.error("Failed to fetch instructions:", error);
2909
+ } finally {
2910
+ setTemplatesLoading(false);
2911
+ }
2912
+ };
2913
+ fetchInstructions();
2914
+ }, [opened]);
2915
+ useEffect7(() => {
2916
+ if (selectedTemplate !== null) {
2917
+ localStorage.setItem("ai-rewrite-last-template", selectedTemplate);
2918
+ }
2919
+ }, [selectedTemplate]);
2920
+ const getTemplateOptions = () => {
2921
+ return instructions.map((instruction) => ({
2922
+ value: instruction.name.toLowerCase(),
2923
+ label: instruction.name.charAt(0).toUpperCase() + instruction.name.slice(1)
2924
+ }));
2925
+ };
2926
+ const handleAISubmit = async () => {
2927
+ const { from, to } = editor.state.selection;
2928
+ const markdownText = serializeSelectionToMarkdown(editor, from, to);
2929
+ if (!markdownText.trim()) {
2930
+ setAiError("No text selected. Please select some text to rewrite.");
2931
+ return;
2932
+ }
2933
+ if (!aiPrompt.trim() && !selectedTemplate) {
2934
+ setAiError("Please enter a prompt or select a template.");
2935
+ return;
2936
+ }
2937
+ setAiLoading(true);
2938
+ setAiError(null);
2939
+ try {
2940
+ let instructionContent = aiPrompt;
2941
+ if (selectedTemplate) {
2942
+ const instruction = instructions.find(
2943
+ (inst) => inst.name.toLowerCase() === selectedTemplate.toLowerCase()
2944
+ );
2945
+ if (instruction) {
2946
+ instructionContent = instruction.content;
2947
+ }
2948
+ }
2949
+ const response = await llmApi.generate({
2950
+ type: selectedTemplate || "custom",
2951
+ prompt: instructionContent,
2952
+ content: markdownText
2953
+ });
2954
+ if (response.success && "data" in response) {
2955
+ const rewrittenMarkdown = response.data.response;
2956
+ console.log("Rewritten markdown:", rewrittenMarkdown);
2957
+ editor.chain().focus().deleteRange({ from, to }).insertContent(rewrittenMarkdown || "").run();
2958
+ onClose();
2959
+ setAiPrompt("");
2960
+ setSelectedTemplate(null);
2961
+ NotificationCenter_default.success({
2962
+ title: "Rewrote text with AI",
2963
+ message: "Text rewritten successfully"
2964
+ });
2965
+ }
2966
+ } catch (error) {
2967
+ console.error("AI rewrite failed:", error);
2968
+ setAiError(error.message || "Failed to generate AI response");
2969
+ NotificationCenter_default.error({
2970
+ title: "AI rewrite failed",
2971
+ message: error.message || "Failed to generate AI response"
2972
+ });
2973
+ } finally {
2974
+ setAiLoading(false);
2975
+ }
2976
+ };
2977
+ const handleClose = () => {
2978
+ onClose();
2979
+ setAiPrompt("");
2980
+ setSelectedTemplate(null);
2981
+ setAiError(null);
2982
+ };
2983
+ return /* @__PURE__ */ jsx11(
2984
+ Modal,
2985
+ {
2986
+ opened,
2987
+ onClose: handleClose,
2988
+ title: /* @__PURE__ */ jsxs10(Group3, { gap: "xs", children: [
2989
+ /* @__PURE__ */ jsx11(IconSparkles, { size: 20 }),
2990
+ "AI Rewrite"
2991
+ ] }),
2992
+ size: "lg",
2993
+ radius: "lg",
2994
+ centered: true,
2995
+ children: /* @__PURE__ */ jsxs10(Stack2, { gap: "md", children: [
2996
+ selectedText && /* @__PURE__ */ jsxs10(Box8, { children: [
2997
+ /* @__PURE__ */ jsx11(Text6, { size: "sm", fw: 500, mb: "xs", children: "Selected Text" }),
2998
+ /* @__PURE__ */ jsx11(
2999
+ Box8,
3000
+ {
3001
+ p: "sm",
3002
+ bg: isDark ? "dark.5" : "gray.0",
3003
+ style: {
3004
+ borderRadius: theme.radius.md,
3005
+ maxHeight: 250,
3006
+ overflow: "auto",
3007
+ borderLeft: "3px solid var(--mantine-color-gray-6)"
3008
+ },
3009
+ children: /* @__PURE__ */ jsx11(Text6, { size: "sm", c: "blue", style: { whiteSpace: "pre-wrap" }, children: selectedText.length > 200 ? `${selectedText.substring(0, 200)}...` : selectedText })
3010
+ }
3011
+ )
3012
+ ] }),
3013
+ /* @__PURE__ */ jsx11(
3014
+ Select3,
3015
+ {
3016
+ label: "Quick Templates",
3017
+ radius: "lg",
3018
+ placeholder: "Select a template or enter custom prompt",
3019
+ value: selectedTemplate,
3020
+ onChange: setSelectedTemplate,
3021
+ data: getTemplateOptions(),
3022
+ clearable: true,
3023
+ disabled: templatesLoading,
3024
+ styles: {
3025
+ dropdown: {
3026
+ borderRadius: "12px"
3027
+ }
3028
+ }
3029
+ }
3030
+ ),
3031
+ /* @__PURE__ */ jsx11(
3032
+ Textarea3,
3033
+ {
3034
+ label: "Custom Prompt (Optional)",
3035
+ radius: "lg",
3036
+ placeholder: "e.g., Make it more professional, Add more details, etc.",
3037
+ value: aiPrompt,
3038
+ onChange: (e) => setAiPrompt(e.currentTarget.value),
3039
+ minRows: 3
3040
+ }
3041
+ ),
3042
+ aiError && /* @__PURE__ */ jsx11(Alert3, { icon: /* @__PURE__ */ jsx11(IconAlertCircle3, { size: 16 }), title: "Error", color: "red", children: aiError }),
3043
+ /* @__PURE__ */ jsxs10(Group3, { justify: "flex-end", mt: "md", children: [
3044
+ /* @__PURE__ */ jsx11(
3045
+ Button3,
3046
+ {
3047
+ variant: "light",
3048
+ color: "gray",
3049
+ radius: "lg",
3050
+ onClick: handleClose,
3051
+ children: "Cancel"
3052
+ }
3053
+ ),
3054
+ /* @__PURE__ */ jsx11(
3055
+ Button3,
3056
+ {
3057
+ onClick: handleAISubmit,
3058
+ loading: aiLoading,
3059
+ radius: "lg",
3060
+ leftSection: !aiLoading && /* @__PURE__ */ jsx11(IconSparkles, { size: 16 }),
3061
+ color: "green.8",
3062
+ children: aiLoading ? "Generating..." : "Generate"
3063
+ }
3064
+ )
3065
+ ] })
3066
+ ] })
3067
+ }
3068
+ );
3069
+ }
3070
+
3071
+ // src/editor/components/EditorContent.tsx
3072
+ import { EditorContent as TiptapEditorContent } from "@tiptap/react";
3073
+ import { useMantineColorScheme as useMantineColorScheme5, useMantineTheme as useMantineTheme8 } from "@mantine/core";
3074
+
3075
+ // src/editor/components/TableControls.tsx
3076
+ import { useEffect as useEffect8, useState as useState9, useCallback as useCallback9, useRef as useRef6 } from "react";
3077
+ import { Paper as Paper3, Text as Text7, Group as Group4 } from "@mantine/core";
3078
+ import { IconDotsVertical, IconDots, IconCopy as IconCopy3, IconTrash as IconTrash4, IconArrowsRightLeft, IconArrowBadgeDown, IconArrowBadgeUp } from "@tabler/icons-react";
3079
+ import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
3080
+ function TableControls({ editor }) {
3081
+ const [isTableActive, setIsTableActive] = useState9(false);
3082
+ const [colHeaders, setColHeaders] = useState9([]);
3083
+ const [rowHeaders, setRowHeaders] = useState9([]);
3084
+ const [activeColMenu, setActiveColMenu] = useState9(null);
3085
+ const [activeRowMenu, setActiveRowMenu] = useState9(null);
3086
+ const colMenuRef = useRef6(null);
3087
+ const rowMenuRef = useRef6(null);
3088
+ const updatePositions = useCallback9(() => {
3089
+ if (!editor.isActive("table")) {
3090
+ setIsTableActive(false);
3091
+ setColHeaders([]);
3092
+ setRowHeaders([]);
3093
+ return;
3094
+ }
3095
+ const container = document.querySelector(".ProseMirror");
3096
+ if (!container) return;
3097
+ const containerRect = container.getBoundingClientRect();
3098
+ let tableNode = null;
3099
+ const { state } = editor;
3100
+ const { selection } = state;
3101
+ const resolvedPos = state.doc.resolve(selection.from);
3102
+ for (let depth = resolvedPos.depth; depth > 0; depth--) {
3103
+ const node = resolvedPos.node(depth);
3104
+ if (node.type.name === "table") {
3105
+ const tableStartPos = resolvedPos.start(depth);
3106
+ const tables = document.querySelectorAll(".ProseMirror table");
3107
+ for (const table of tables) {
3108
+ try {
3109
+ const domPos = editor.view.posAtDOM(table, 0);
3110
+ if (domPos === tableStartPos) {
3111
+ tableNode = table;
3112
+ break;
3113
+ }
3114
+ } catch {
3115
+ }
3116
+ }
3117
+ break;
3118
+ }
3119
+ }
3120
+ if (!tableNode) {
3121
+ const sel = window.getSelection();
3122
+ if (sel && sel.rangeCount > 0) {
3123
+ const range = sel.getRangeAt(0);
3124
+ const container2 = range.startContainer.parentElement?.closest(".ProseMirror table");
3125
+ if (container2) {
3126
+ tableNode = container2;
3127
+ }
3128
+ }
3129
+ }
3130
+ if (!tableNode) {
3131
+ tableNode = document.querySelector(".ProseMirror table");
3132
+ }
3133
+ if (!tableNode) {
3134
+ setIsTableActive(false);
3135
+ setColHeaders([]);
3136
+ setRowHeaders([]);
3137
+ return;
3138
+ }
3139
+ setIsTableActive(true);
3140
+ const firstRow = tableNode.querySelector("tr");
3141
+ if (firstRow) {
3142
+ const cells = firstRow.querySelectorAll("th, td");
3143
+ const positions = [];
3144
+ cells.forEach((cell) => {
3145
+ const cellRect = cell.getBoundingClientRect();
3146
+ positions.push({
3147
+ top: cellRect.top - containerRect.top,
3148
+ left: cellRect.left - containerRect.left,
3149
+ width: cellRect.width
3150
+ });
3151
+ });
3152
+ setColHeaders(positions);
3153
+ }
3154
+ const rows = tableNode.querySelectorAll("tr");
3155
+ if (rows.length > 0) {
3156
+ const positions = [];
3157
+ rows.forEach((row) => {
3158
+ const rowRect = row.getBoundingClientRect();
3159
+ positions.push({
3160
+ top: rowRect.top - containerRect.top,
3161
+ left: rowRect.left - containerRect.left,
3162
+ height: rowRect.height
3163
+ });
3164
+ });
3165
+ setRowHeaders(positions);
3166
+ }
3167
+ }, [editor]);
3168
+ useEffect8(() => {
3169
+ updatePositions();
3170
+ const handler = () => updatePositions();
3171
+ editor.on("selectionUpdate", handler);
3172
+ editor.on("transaction", handler);
3173
+ const observer = new MutationObserver(handler);
3174
+ const tableNode = document.querySelector(".ProseMirror table");
3175
+ if (tableNode) {
3176
+ observer.observe(tableNode.parentElement, { childList: true, subtree: true });
3177
+ }
3178
+ const handleClickOutside = (e) => {
3179
+ if (colMenuRef.current && !colMenuRef.current.contains(e.target)) {
3180
+ setActiveColMenu(null);
3181
+ }
3182
+ if (rowMenuRef.current && !rowMenuRef.current.contains(e.target)) {
3183
+ setActiveRowMenu(null);
3184
+ }
3185
+ };
3186
+ if (activeColMenu || activeRowMenu) {
3187
+ document.addEventListener("mousedown", handleClickOutside);
3188
+ }
3189
+ return () => {
3190
+ editor.off("selectionUpdate", handler);
3191
+ editor.off("transaction", handler);
3192
+ observer.disconnect();
3193
+ document.removeEventListener("mousedown", handleClickOutside);
3194
+ };
3195
+ }, [editor, updatePositions, activeColMenu, activeRowMenu]);
3196
+ const handleDuplicateColumn = () => {
3197
+ ;
3198
+ editor.chain().focus().addColumnAfter().run();
3199
+ setTimeout(updatePositions, 50);
3200
+ setActiveColMenu(null);
3201
+ };
3202
+ const handleDeleteColumn = () => {
3203
+ ;
3204
+ editor.chain().focus().deleteColumn().run();
3205
+ setActiveColMenu(null);
3206
+ };
3207
+ const handleInsertLeft = () => {
3208
+ ;
3209
+ editor.chain().focus().addColumnBefore().run();
3210
+ setTimeout(updatePositions, 50);
3211
+ setActiveColMenu(null);
3212
+ };
3213
+ const handleInsertRight = () => {
3214
+ ;
3215
+ editor.chain().focus().addColumnAfter().run();
3216
+ setTimeout(updatePositions, 50);
3217
+ setActiveColMenu(null);
3218
+ };
3219
+ const handleDuplicateRow = () => {
3220
+ ;
3221
+ editor.chain().focus().addRowAfter().run();
3222
+ setTimeout(updatePositions, 50);
3223
+ setActiveRowMenu(null);
3224
+ };
3225
+ const handleDeleteRow = () => {
3226
+ ;
3227
+ editor.chain().focus().deleteRow().run();
3228
+ setActiveRowMenu(null);
3229
+ };
3230
+ const handleInsertAbove = () => {
3231
+ ;
3232
+ editor.chain().focus().addRowBefore().run();
3233
+ setTimeout(updatePositions, 50);
3234
+ setActiveRowMenu(null);
3235
+ };
3236
+ const handleInsertBelow = () => {
3237
+ ;
3238
+ editor.chain().focus().addRowAfter().run();
3239
+ setTimeout(updatePositions, 50);
3240
+ setActiveRowMenu(null);
3241
+ };
3242
+ if (!isTableActive) return null;
3243
+ if (!editor.isEditable) return null;
3244
+ const menuButtonStyle = {
3245
+ position: "absolute",
3246
+ zIndex: 999,
3247
+ display: "flex",
3248
+ alignItems: "center",
3249
+ justifyContent: "center",
3250
+ width: 24,
3251
+ height: 24,
3252
+ borderRadius: 4,
3253
+ backgroundColor: "var(--mantine-color-body)",
3254
+ border: "1px solid var(--mantine-color-default-border)",
3255
+ opacity: 0.5,
3256
+ transition: "all 0.15s ease",
3257
+ cursor: "pointer"
3258
+ };
3259
+ return /* @__PURE__ */ jsxs11(Fragment4, { children: [
3260
+ rowHeaders.map((pos, index) => /* @__PURE__ */ jsx12(
3261
+ "div",
3262
+ {
3263
+ style: {
3264
+ ...menuButtonStyle,
3265
+ top: `${pos.top + pos.height / 2 - 12}px`,
3266
+ left: `${pos.left - 12}px`
3267
+ },
3268
+ onMouseEnter: (e) => {
3269
+ e.currentTarget.style.opacity = "1";
3270
+ },
3271
+ onMouseLeave: (e) => {
3272
+ e.currentTarget.style.opacity = "0.5";
3273
+ },
3274
+ onClick: (e) => {
3275
+ e.stopPropagation();
3276
+ setActiveRowMenu({
3277
+ index,
3278
+ top: pos.top + pos.height / 2 - 100,
3279
+ left: pos.left + 30
3280
+ });
3281
+ },
3282
+ children: /* @__PURE__ */ jsx12(IconDotsVertical, { size: 14 })
3283
+ },
3284
+ index
3285
+ )),
3286
+ colHeaders.map((pos, index) => /* @__PURE__ */ jsx12(
3287
+ "div",
3288
+ {
3289
+ style: {
3290
+ ...menuButtonStyle,
3291
+ top: `${pos.top - 12}px`,
3292
+ left: `${pos.left + pos.width / 2 - 12}px`
3293
+ },
3294
+ onMouseEnter: (e) => {
3295
+ e.currentTarget.style.opacity = "1";
3296
+ },
3297
+ onMouseLeave: (e) => {
3298
+ e.currentTarget.style.opacity = "0.5";
3299
+ },
3300
+ onClick: (e) => {
3301
+ e.stopPropagation();
3302
+ setActiveColMenu({
3303
+ index,
3304
+ top: pos.top + 30,
3305
+ left: pos.left + pos.width / 2 - 100
3306
+ });
3307
+ },
3308
+ children: /* @__PURE__ */ jsx12(IconDots, { size: 14 })
3309
+ },
3310
+ index
3311
+ )),
3312
+ activeColMenu && /* @__PURE__ */ jsx12(
3313
+ "div",
3314
+ {
3315
+ ref: colMenuRef,
3316
+ style: {
3317
+ position: "absolute",
3318
+ top: `${activeColMenu.top}px`,
3319
+ left: `${activeColMenu.left}px`,
3320
+ zIndex: 1001
3321
+ },
3322
+ children: /* @__PURE__ */ jsxs11(
3323
+ Paper3,
3324
+ {
3325
+ shadow: "xl",
3326
+ p: "xs",
3327
+ radius: "md",
3328
+ withBorder: true,
3329
+ style: {
3330
+ minWidth: 200,
3331
+ backgroundColor: "var(--mantine-color-body)"
3332
+ },
3333
+ children: [
3334
+ /* @__PURE__ */ jsx12(Group4, { gap: 4, wrap: "nowrap", children: /* @__PURE__ */ jsx12(Text7, { size: "xs", fw: 500, style: { flex: 1 }, children: "Column" }) }),
3335
+ /* @__PURE__ */ jsxs11("div", { style: { marginTop: 8 }, children: [
3336
+ /* @__PURE__ */ jsx12(
3337
+ MenuItem,
3338
+ {
3339
+ icon: /* @__PURE__ */ jsx12(IconArrowsRightLeft, { size: 16 }),
3340
+ label: "Insert Left",
3341
+ onClick: handleInsertLeft
3342
+ }
3343
+ ),
3344
+ /* @__PURE__ */ jsx12(
3345
+ MenuItem,
3346
+ {
3347
+ icon: /* @__PURE__ */ jsx12(IconArrowBadgeDown, { size: 16, style: { transform: "rotate(-90deg)" } }),
3348
+ label: "Insert Right",
3349
+ onClick: handleInsertRight
3350
+ }
3351
+ ),
3352
+ /* @__PURE__ */ jsx12(
3353
+ MenuItem,
3354
+ {
3355
+ icon: /* @__PURE__ */ jsx12(IconCopy3, { size: 16 }),
3356
+ label: "Duplicate",
3357
+ onClick: handleDuplicateColumn
3358
+ }
3359
+ ),
3360
+ /* @__PURE__ */ jsx12("div", { style: { height: 1, background: "var(--mantine-color-default-border)", margin: "4px 0" } }),
3361
+ /* @__PURE__ */ jsx12(
3362
+ MenuItem,
3363
+ {
3364
+ icon: /* @__PURE__ */ jsx12(IconTrash4, { size: 16 }),
3365
+ label: "Delete",
3366
+ onClick: handleDeleteColumn,
3367
+ color: "red"
3368
+ }
3369
+ )
3370
+ ] })
3371
+ ]
3372
+ }
3373
+ )
3374
+ }
3375
+ ),
3376
+ activeRowMenu && /* @__PURE__ */ jsx12(
3377
+ "div",
3378
+ {
3379
+ ref: rowMenuRef,
3380
+ style: {
3381
+ position: "absolute",
3382
+ top: `${activeRowMenu.top}px`,
3383
+ left: `${activeRowMenu.left}px`,
3384
+ zIndex: 1001
3385
+ },
3386
+ children: /* @__PURE__ */ jsxs11(
3387
+ Paper3,
3388
+ {
3389
+ shadow: "xl",
3390
+ p: "xs",
3391
+ radius: "md",
3392
+ withBorder: true,
3393
+ style: {
3394
+ minWidth: 200,
3395
+ backgroundColor: "var(--mantine-color-body)"
3396
+ },
3397
+ children: [
3398
+ /* @__PURE__ */ jsx12(Group4, { gap: 4, wrap: "nowrap", children: /* @__PURE__ */ jsx12(Text7, { size: "xs", fw: 500, style: { flex: 1 }, children: "Row" }) }),
3399
+ /* @__PURE__ */ jsxs11("div", { style: { marginTop: 8 }, children: [
3400
+ /* @__PURE__ */ jsx12(
3401
+ MenuItem,
3402
+ {
3403
+ icon: /* @__PURE__ */ jsx12(IconArrowBadgeUp, { size: 16 }),
3404
+ label: "Insert Above",
3405
+ onClick: handleInsertAbove
3406
+ }
3407
+ ),
3408
+ /* @__PURE__ */ jsx12(
3409
+ MenuItem,
3410
+ {
3411
+ icon: /* @__PURE__ */ jsx12(IconArrowBadgeDown, { size: 16 }),
3412
+ label: "Insert Below",
3413
+ onClick: handleInsertBelow
3414
+ }
3415
+ ),
3416
+ /* @__PURE__ */ jsx12(
3417
+ MenuItem,
3418
+ {
3419
+ icon: /* @__PURE__ */ jsx12(IconCopy3, { size: 16 }),
3420
+ label: "Duplicate",
3421
+ onClick: handleDuplicateRow
3422
+ }
3423
+ ),
3424
+ /* @__PURE__ */ jsx12("div", { style: { height: 1, background: "var(--mantine-color-default-border)", margin: "4px 0" } }),
3425
+ /* @__PURE__ */ jsx12(
3426
+ MenuItem,
3427
+ {
3428
+ icon: /* @__PURE__ */ jsx12(IconTrash4, { size: 16 }),
3429
+ label: "Delete",
3430
+ onClick: handleDeleteRow,
3431
+ color: "red"
3432
+ }
3433
+ )
3434
+ ] })
3435
+ ]
3436
+ }
3437
+ )
3438
+ }
3439
+ )
3440
+ ] });
3441
+ }
3442
+ var MenuItem = ({ icon, label, onClick, color }) => /* @__PURE__ */ jsxs11(
3443
+ "div",
3444
+ {
3445
+ onClick,
3446
+ style: {
3447
+ display: "flex",
3448
+ alignItems: "center",
3449
+ gap: 8,
3450
+ padding: "6px 8px",
3451
+ borderRadius: 4,
3452
+ cursor: "pointer",
3453
+ color: color || "inherit",
3454
+ transition: "background-color 0.1s ease"
3455
+ },
3456
+ onMouseEnter: (e) => {
3457
+ e.currentTarget.style.backgroundColor = "var(--mantine-color-default-hover)";
3458
+ },
3459
+ onMouseLeave: (e) => {
3460
+ e.currentTarget.style.backgroundColor = "transparent";
3461
+ },
3462
+ children: [
3463
+ icon,
3464
+ /* @__PURE__ */ jsx12(Text7, { size: "sm", children: label })
3465
+ ]
3466
+ }
3467
+ );
3468
+
3469
+ // src/editor/components/EditorContent.tsx
3470
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
3471
+ function EditorContent({ editor, minHeight = 200 }) {
3472
+ const { colorScheme: rawColorScheme } = useMantineColorScheme5();
3473
+ const theme = useMantineTheme8();
3474
+ const colorScheme = resolveColorScheme(rawColorScheme);
3475
+ const tableBorderColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[3];
3476
+ const tableHeaderBg = colorScheme === "dark" ? theme.colors.dark[5] : theme.colors.gray[1];
3477
+ const bgColors = {
3478
+ red: colorScheme === "dark" ? theme.colors.red[8] : theme.colors.red[1],
3479
+ orange: colorScheme === "dark" ? theme.colors.orange[8] : theme.colors.orange[1],
3480
+ yellow: colorScheme === "dark" ? theme.colors.yellow[8] : theme.colors.yellow[1],
3481
+ green: colorScheme === "dark" ? theme.colors.green[8] : theme.colors.green[1],
3482
+ blue: colorScheme === "dark" ? theme.colors.blue[8] : theme.colors.blue[1],
3483
+ violet: colorScheme === "dark" ? theme.colors.violet[8] : theme.colors.violet[1],
3484
+ pink: colorScheme === "dark" ? theme.colors.pink[8] : theme.colors.pink[1],
3485
+ gray: colorScheme === "dark" ? theme.colors.gray[8] : theme.colors.gray[1]
3486
+ };
3487
+ const h1Color = colorScheme === "dark" ? theme.colors.yellow[5] : theme.colors.yellow[8];
3488
+ const h2Color = colorScheme === "dark" ? theme.colors.blue[5] : theme.colors.blue[8];
3489
+ const h3Color = colorScheme === "dark" ? theme.colors.orange[5] : theme.colors.orange[8];
3490
+ const h4Color = colorScheme === "dark" ? theme.colors.green[5] : theme.colors.green[8];
3491
+ const h5Color = colorScheme === "dark" ? theme.colors.red[5] : theme.colors.red[8];
3492
+ const h6Color = colorScheme === "dark" ? theme.colors.cyan[5] : theme.colors.cyan[8];
3493
+ return /* @__PURE__ */ jsx13("div", { style: { position: "relative" }, children: /* @__PURE__ */ jsxs12(
3494
+ TiptapEditorContent,
3495
+ {
3496
+ editor,
3497
+ style: { minHeight, lineHeight: "1.3", padding: "0 12px" },
3498
+ children: [
3499
+ /* @__PURE__ */ jsx13("style", { children: `
3500
+ .ProseMirror {
3501
+ --bg-red: ${bgColors.red};
3502
+ --bg-orange: ${bgColors.orange};
3503
+ --bg-yellow: ${bgColors.yellow};
3504
+ --bg-green: ${bgColors.green};
3505
+ --bg-blue: ${bgColors.blue};
3506
+ --bg-violet: ${bgColors.violet};
3507
+ --bg-pink: ${bgColors.pink};
3508
+ --bg-gray: ${bgColors.gray};
3509
+ font-family: 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
3510
+ }
3511
+ .ProseMirror ul, .ProseMirror ol { line-height: 1.5; padding-left: 1.6em; margin: 0.6em 0 0.2em 0; }
3512
+ .ProseMirror ol li::marker { font-weight: 700; color: ${colorScheme === "dark" ? theme.colors.blue[4] : theme.colors.blue[7]}; }
3513
+ .ProseMirror ol li { color: ${colorScheme === "dark" ? theme.colors.blue[4] : theme.colors.blue[8]}; }
3514
+ .ProseMirror h1 { color: ${h1Color}; font-size: 1.4em; margin-top: 0em; margin-bottom: 0em; font-weight: 700; }
3515
+ .ProseMirror h2 { color: ${h2Color}; font-size: 1.3em; margin-top: 1.4em; margin-bottom: 0.3em; font-weight: 600; }
3516
+ .ProseMirror h3 { color: ${h3Color}; font-size: 1.2em; margin-top: 1.0em; margin-bottom: 0.2em; font-weight: 600; }
3517
+ .ProseMirror h4 { color: ${h4Color}; font-size: 1.1em; margin-top: 0.6em; margin-bottom: 0.2em; font-weight: 500; }
3518
+ .ProseMirror h5 { color: ${h5Color}; font-size: 1.0em; margin-top: 0.5em; margin-bottom: 0.1em; font-weight: 500; }
3519
+ .ProseMirror h6 { color: ${h6Color}; font-size: 1.0em; margin-top: 0.5em; margin-bottom: 0.1em; font-weight: 500; }
3520
+ .ProseMirror p { margin-top: 0.1em; margin-bottom: 0.5em; }
3521
+ .ProseMirror blockquote {
3522
+ margin: 12px 0;
3523
+ padding: 8px 16px;
3524
+ border-left: 6px solid ${colorScheme === "dark" ? theme.colors.gray[5] : theme.colors.gray[3]};
3525
+ background-color: ${colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]};
3526
+ border-radius: 4px;
3527
+ font-style: normal;
3528
+ color: ${colorScheme === "dark" ? theme.colors.gray[3] : theme.colors.gray[7]};
3529
+ }
3530
+ .ProseMirror blockquote p {
3531
+ margin: 4px 0;
3532
+ }
3533
+ .ProseMirror blockquote p:first-child {
3534
+ margin-top: 0;
3535
+ }
3536
+ .ProseMirror blockquote p:last-child {
3537
+ margin-bottom: 0;
3538
+ }
3539
+ .ProseMirror span[data-bg-color] {
3540
+ padding: 0.1em 0.3em;
3541
+ border-radius: 3px;
3542
+ box-decoration-break: clone;
3543
+ }
3544
+ .ProseMirror span[data-bg-color="red"] { background-color: var(--bg-red); }
3545
+ .ProseMirror span[data-bg-color="orange"] { background-color: var(--bg-orange); }
3546
+ .ProseMirror span[data-bg-color="yellow"] { background-color: var(--bg-yellow); }
3547
+ .ProseMirror span[data-bg-color="green"] { background-color: var(--bg-green); }
3548
+ .ProseMirror span[data-bg-color="blue"] { background-color: var(--bg-blue); }
3549
+ .ProseMirror span[data-bg-color="violet"] { background-color: var(--bg-violet); }
3550
+ .ProseMirror span[data-bg-color="pink"] { background-color: var(--bg-pink); }
3551
+ .ProseMirror span[data-bg-color="gray"] { background-color: var(--bg-gray); }
3552
+ .ProseMirror p code,
3553
+ .ProseMirror h1 code,
3554
+ .ProseMirror h2 code,
3555
+ .ProseMirror h3 code,
3556
+ .ProseMirror h4 code,
3557
+ .ProseMirror h5 code,
3558
+ .ProseMirror h6 code,
3559
+ .ProseMirror li code,
3560
+ .ProseMirror td code,
3561
+ .ProseMirror th code,
3562
+ .ProseMirror blockquote code {
3563
+ font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;
3564
+ font-size: 0.875em;
3565
+ padding: 0.2em 0.4em;
3566
+ border-radius: 6px;
3567
+ background-color: ${colorScheme === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.06)"};
3568
+ color: ${colorScheme === "dark" ? theme.colors.orange[4] : theme.colors.red[7]};
3569
+ border: 1px solid ${colorScheme === "dark" ? "rgba(255, 255, 255, 0.06)" : "rgba(0, 0, 0, 0.08)"};
3570
+ box-decoration-break: clone;
3571
+ white-space: pre-wrap;
3572
+ word-break: break-word;
3573
+ }
3574
+ .ProseMirror table {
3575
+ border-collapse: separate;
3576
+ border-spacing: 0;
3577
+ width: 100%;
3578
+ margin: 8px 0;
3579
+ font-size: 14px;
3580
+ border: 1px solid ${tableBorderColor};
3581
+ border-radius: 8px;
3582
+ overflow: hidden;
3583
+ }
3584
+ .ProseMirror th, .ProseMirror td {
3585
+ border: 1px solid ${tableBorderColor};
3586
+ padding: 8px 12px;
3587
+ vertical-align: top;
3588
+ }
3589
+ .ProseMirror th {
3590
+ background-color: ${tableHeaderBg};
3591
+ font-weight: 600;
3592
+ text-align: left;
3593
+ }
3594
+ .ProseMirror td {
3595
+ background-color: transparent;
3596
+ }
3597
+ .ProseMirror .selectedCell {
3598
+ border: 2px solid ${theme.colors.blue[6]};
3599
+ }
3600
+ .ProseMirror hr {
3601
+ border: none;
3602
+ border-top: 1px solid ${colorScheme === "dark" ? theme.colors.dark[3] : theme.colors.gray[4]};
3603
+ opacity: 0.4;
3604
+ margin: 16px 0;
3605
+ cursor: pointer;
3606
+ }
3607
+ .ProseMirror hr.ProseMirror-selectednode {
3608
+ opacity: 1;
3609
+ border-top: 2px solid ${theme.colors.blue[6]};
3610
+ outline: 2px solid ${theme.colors.blue[3]};
3611
+ outline-offset: 2px;
3612
+ }
3613
+ .ProseMirror img.ticket-image {
3614
+ max-width: 100%;
3615
+ height: auto;
3616
+ border-radius: 4px;
3617
+ margin: 8px 0;
3618
+ cursor: pointer;
3619
+ }
3620
+ .ProseMirror img.ticket-image.ProseMirror-selectednode {
3621
+ outline: 2px solid ${theme.colors.blue[6]};
3622
+ outline-offset: 2px;
3623
+ }
3624
+ .code-block-wrapper .ProseMirror-trailingBreak {
3625
+ display: none;
3626
+ }
3627
+ ` }),
3628
+ /* @__PURE__ */ jsx13(TableControls, { editor })
3629
+ ]
3630
+ }
3631
+ ) });
3632
+ }
3633
+
3634
+ // src/editor/components/BubbleMenu.tsx
3635
+ import { BubbleMenu as TiptapBubbleMenu } from "@tiptap/react/menus";
3636
+ import { useEditorState } from "@tiptap/react";
3637
+ import {
3638
+ Group as Group5,
3639
+ ActionIcon as ActionIcon6,
3640
+ Text as Text8,
3641
+ Box as Box9,
3642
+ Menu as Menu4,
3643
+ useMantineTheme as useMantineTheme9,
3644
+ useMantineColorScheme as useMantineColorScheme6
3645
+ } from "@mantine/core";
3646
+ import {
3647
+ IconBold,
3648
+ IconItalic,
3649
+ IconCode as IconCode3,
3650
+ IconColorPicker,
3651
+ IconQuote,
3652
+ IconCodeDots,
3653
+ IconHeading,
3654
+ IconCheck as IconCheck5,
3655
+ IconListCheck,
3656
+ IconRefreshDot,
3657
+ IconPalette,
3658
+ IconSparkles as IconSparkles2
3659
+ } from "@tabler/icons-react";
3660
+ import { useState as useState10 } from "react";
3661
+ import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
3662
+ function BubbleMenu({ editor }) {
3663
+ const theme = useMantineTheme9();
3664
+ const { colorScheme } = useMantineColorScheme6();
3665
+ const isDark = colorScheme === "dark";
3666
+ const [aiModalOpen, setAiModalOpen] = useState10(false);
3667
+ const editorState = useEditorState({
3668
+ editor,
3669
+ selector: ({ editor: editor2 }) => ({
3670
+ textAttrs: editor2.getAttributes("textStyle"),
3671
+ bgAttrs: editor2.getAttributes("backgroundColor"),
3672
+ isBold: editor2.isActive("bold"),
3673
+ isItalic: editor2.isActive("italic"),
3674
+ isCode: editor2.isActive("code"),
3675
+ isCodeBlock: editor2.isActive("codeBlock"),
3676
+ isQuote: editor2.isActive("blockquote"),
3677
+ isTaskList: editor2.isActive("taskList"),
3678
+ currentHeadingLevel: (() => {
3679
+ for (let level = 1; level <= 6; level++) {
3680
+ if (editor2.isActive("heading", { level })) {
3681
+ return level;
3682
+ }
3683
+ }
3684
+ return null;
3685
+ })()
3686
+ }),
3687
+ equalityFn: (a, b) => JSON.stringify(a) === JSON.stringify(b)
3688
+ });
3689
+ const handleHeadingChange = (level) => {
3690
+ editor.chain().focus().toggleHeading({ level }).run();
3691
+ };
3692
+ const handleAIRewrite = () => {
3693
+ const { from, to } = editor.state.selection;
3694
+ const markdownText = serializeSelectionToMarkdown(editor, from, to);
3695
+ if (!markdownText.trim()) {
3696
+ NotificationCenter_default.warn({
3697
+ title: "No text selected",
3698
+ message: "Please select some text to rewrite with AI"
3699
+ });
3700
+ return;
3701
+ }
3702
+ setAiModalOpen(true);
3703
+ };
3704
+ return /* @__PURE__ */ jsxs13(Fragment5, { children: [
3705
+ /* @__PURE__ */ jsx14(
3706
+ TiptapBubbleMenu,
3707
+ {
3708
+ editor,
3709
+ shouldShow: ({ editor: editor2 }) => {
3710
+ const { from, to } = editor2.state.selection;
3711
+ const hasSelection = from !== to;
3712
+ if (editor2.isActive("codeBlock")) {
3713
+ return false;
3714
+ }
3715
+ if (editor2.isActive("image")) {
3716
+ return false;
3717
+ }
3718
+ if (!editor2.isEditable) {
3719
+ return false;
3720
+ }
3721
+ return hasSelection;
3722
+ },
3723
+ children: /* @__PURE__ */ jsxs13(
3724
+ Group5,
3725
+ {
3726
+ gap: 4,
3727
+ p: "xs",
3728
+ bg: "var(--mantine-color-body)",
3729
+ style: bubbleStyle,
3730
+ children: [
3731
+ /* @__PURE__ */ jsx14(
3732
+ ToolbarButton,
3733
+ {
3734
+ onClick: handleAIRewrite,
3735
+ icon: /* @__PURE__ */ jsx14(IconSparkles2, { size: 12 }),
3736
+ color: "green.8"
3737
+ }
3738
+ ),
3739
+ /* @__PURE__ */ jsx14(Divider, {}),
3740
+ /* @__PURE__ */ jsx14(
3741
+ ToolbarButton,
3742
+ {
3743
+ active: editorState.isBold,
3744
+ onClick: () => editor.chain().focus().toggleBold().run(),
3745
+ icon: /* @__PURE__ */ jsx14(IconBold, { size: 12 })
3746
+ }
3747
+ ),
3748
+ /* @__PURE__ */ jsx14(
3749
+ ToolbarButton,
3750
+ {
3751
+ active: editorState.isItalic,
3752
+ onClick: () => editor.chain().focus().toggleItalic().run(),
3753
+ icon: /* @__PURE__ */ jsx14(IconItalic, { size: 12 })
3754
+ }
3755
+ ),
3756
+ /* @__PURE__ */ jsx14(
3757
+ ToolbarButton,
3758
+ {
3759
+ active: editorState.isCode,
3760
+ onClick: () => editor.chain().focus().toggleCode().run(),
3761
+ icon: /* @__PURE__ */ jsx14(IconCode3, { size: 12 })
3762
+ }
3763
+ ),
3764
+ /* @__PURE__ */ jsx14(Divider, {}),
3765
+ /* @__PURE__ */ jsxs13(Menu4, { shadow: "md", width: 200, withinPortal: true, children: [
3766
+ /* @__PURE__ */ jsx14(Menu4.Target, { children: /* @__PURE__ */ jsx14(ActionIcon6, { size: "sm", variant: editorState.currentHeadingLevel ? "filled" : "light", style: { minWidth: 40 }, children: editorState.currentHeadingLevel ? /* @__PURE__ */ jsxs13(Text8, { size: "xs", fw: 600, children: [
3767
+ "H",
3768
+ editorState.currentHeadingLevel
3769
+ ] }) : /* @__PURE__ */ jsx14(IconHeading, { size: 12 }) }) }),
3770
+ /* @__PURE__ */ jsxs13(Menu4.Dropdown, { children: [
3771
+ /* @__PURE__ */ jsx14(Menu4.Label, { children: "Headings" }),
3772
+ /* @__PURE__ */ jsx14(Menu4.Divider, {}),
3773
+ [1, 2, 3, 4, 5, 6].map((level) => /* @__PURE__ */ jsxs13(
3774
+ Menu4.Item,
3775
+ {
3776
+ component: "button",
3777
+ onClick: () => handleHeadingChange(level),
3778
+ leftSection: editorState.currentHeadingLevel === level ? /* @__PURE__ */ jsx14(IconCheck5, { size: 14 }) : null,
3779
+ children: [
3780
+ "Heading ",
3781
+ level
3782
+ ]
3783
+ },
3784
+ level
3785
+ ))
3786
+ ] })
3787
+ ] }),
3788
+ /* @__PURE__ */ jsx14(
3789
+ ToolbarButton,
3790
+ {
3791
+ active: editorState.isCodeBlock,
3792
+ onClick: () => editor.chain().focus().toggleCodeBlock().run(),
3793
+ icon: /* @__PURE__ */ jsx14(IconCodeDots, { size: 12 })
3794
+ }
3795
+ ),
3796
+ /* @__PURE__ */ jsx14(
3797
+ ToolbarButton,
3798
+ {
3799
+ active: editorState.isQuote,
3800
+ onClick: () => editor.chain().focus().toggleBlockquote().run(),
3801
+ icon: /* @__PURE__ */ jsx14(IconQuote, { size: 12 })
3802
+ }
3803
+ ),
3804
+ /* @__PURE__ */ jsx14(
3805
+ ToolbarButton,
3806
+ {
3807
+ active: editorState.isTaskList,
3808
+ onClick: () => editor.chain().focus().toggleTaskList().run(),
3809
+ icon: /* @__PURE__ */ jsx14(IconListCheck, { size: 12 })
3810
+ }
3811
+ ),
3812
+ /* @__PURE__ */ jsx14(Divider, {}),
3813
+ /* @__PURE__ */ jsxs13(Menu4, { shadow: "md", width: 280, withinPortal: true, children: [
3814
+ /* @__PURE__ */ jsx14(Menu4.Target, { children: /* @__PURE__ */ jsx14(
3815
+ ActionIcon6,
3816
+ {
3817
+ size: "sm",
3818
+ variant: editorState.textAttrs.color ? "filled" : "light",
3819
+ style: { minWidth: 40 },
3820
+ title: "Text Color",
3821
+ children: /* @__PURE__ */ jsx14(IconColorPicker, { size: 12 })
3822
+ }
3823
+ ) }),
3824
+ /* @__PURE__ */ jsxs13(Menu4.Dropdown, { children: [
3825
+ /* @__PURE__ */ jsxs13(Group5, { justify: "space-between", align: "center", pt: "5", pr: "10", gap: "0", children: [
3826
+ /* @__PURE__ */ jsx14(Menu4.Label, { mb: 0, children: "Text Color" }),
3827
+ /* @__PURE__ */ jsx14(
3828
+ ActionIcon6,
3829
+ {
3830
+ size: "sm",
3831
+ variant: "subtle",
3832
+ onClick: () => editor.chain().focus().unsetColor().run(),
3833
+ title: "Default",
3834
+ children: /* @__PURE__ */ jsx14(IconRefreshDot, { size: 16 })
3835
+ }
3836
+ )
3837
+ ] }),
3838
+ /* @__PURE__ */ jsx14(Menu4.Divider, {}),
3839
+ /* @__PURE__ */ jsx14(Group5, { gap: "xs", wrap: "wrap", p: "xs", children: ["red", "orange", "yellow", "green", "blue", "violet", "pink", "gray"].map((colorName) => /* @__PURE__ */ jsx14(
3840
+ ActionIcon6,
3841
+ {
3842
+ size: "sm",
3843
+ variant: editorState.textAttrs.color === theme.colors[colorName][6] ? "filled" : "light",
3844
+ onClick: () => editor.chain().focus().setColor(theme.colors[colorName][6]).run(),
3845
+ color: colorName,
3846
+ title: colorName,
3847
+ children: /* @__PURE__ */ jsx14(IconCheck5, { size: 12 })
3848
+ },
3849
+ colorName
3850
+ )) })
3851
+ ] })
3852
+ ] }),
3853
+ /* @__PURE__ */ jsxs13(Menu4, { shadow: "md", width: 280, withinPortal: true, children: [
3854
+ /* @__PURE__ */ jsx14(Menu4.Target, { children: /* @__PURE__ */ jsx14(
3855
+ ActionIcon6,
3856
+ {
3857
+ size: "sm",
3858
+ variant: editorState.bgAttrs.color ? "filled" : "light",
3859
+ style: { minWidth: 40 },
3860
+ title: "Highlight",
3861
+ children: /* @__PURE__ */ jsx14(IconPalette, { size: 12 })
3862
+ }
3863
+ ) }),
3864
+ /* @__PURE__ */ jsxs13(Menu4.Dropdown, { children: [
3865
+ /* @__PURE__ */ jsxs13(Group5, { justify: "space-between", align: "center", pt: "5", pr: "10", gap: "0", children: [
3866
+ /* @__PURE__ */ jsx14(Menu4.Label, { mb: 0, children: "Background Color" }),
3867
+ /* @__PURE__ */ jsx14(
3868
+ ActionIcon6,
3869
+ {
3870
+ size: "sm",
3871
+ variant: "subtle",
3872
+ onClick: () => editor.chain().focus().unsetBackgroundColor().run(),
3873
+ title: "Default",
3874
+ children: /* @__PURE__ */ jsx14(IconRefreshDot, { size: 16 })
3875
+ }
3876
+ )
3877
+ ] }),
3878
+ /* @__PURE__ */ jsx14(Menu4.Divider, {}),
3879
+ /* @__PURE__ */ jsx14(Group5, { gap: "xs", wrap: "wrap", p: "xs", children: ["red", "orange", "yellow", "green", "blue", "violet", "pink", "gray"].map((colorName) => /* @__PURE__ */ jsx14(
3880
+ ActionIcon6,
3881
+ {
3882
+ size: "sm",
3883
+ variant: editorState.bgAttrs.color === colorName ? "filled" : "light",
3884
+ onClick: () => editor.chain().focus().setBackgroundColor(colorName).run(),
3885
+ color: colorName,
3886
+ title: colorName,
3887
+ style: editorState.bgAttrs.color !== colorName ? { backgroundColor: isDark ? theme.colors[colorName][8] : theme.colors[colorName][1] } : void 0,
3888
+ children: /* @__PURE__ */ jsx14(IconCheck5, { size: 12, color: editorState.bgAttrs.color === colorName ? isDark ? theme.white : "inherit" : isDark ? theme.white : theme.colors[colorName][9] })
3889
+ },
3890
+ colorName
3891
+ )) })
3892
+ ] })
3893
+ ] })
3894
+ ]
3895
+ }
3896
+ )
3897
+ }
3898
+ ),
3899
+ /* @__PURE__ */ jsx14(
3900
+ DocumentEditor,
3901
+ {
3902
+ editor,
3903
+ opened: aiModalOpen,
3904
+ onClose: () => setAiModalOpen(false)
3905
+ }
3906
+ )
3907
+ ] });
3908
+ }
3909
+ var ToolbarButton = ({ active, onClick, icon, color }) => /* @__PURE__ */ jsx14(
3910
+ ActionIcon6,
3911
+ {
3912
+ size: "sm",
3913
+ variant: active ? "filled" : "subtle",
3914
+ onClick,
3915
+ color,
3916
+ children: icon
3917
+ }
3918
+ );
3919
+ var Divider = () => /* @__PURE__ */ jsx14(Box9, { w: 1, h: 16, bg: "var(--mantine-color-default-border)", mx: 4 });
3920
+ var bubbleStyle = {
3921
+ borderRadius: 4,
3922
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)"
3923
+ };
3924
+
3925
+ // src/editor/components/SlashCommand.tsx
3926
+ import { Extension as Extension2 } from "@tiptap/core";
3927
+ import Suggestion from "@tiptap/suggestion";
3928
+ import { ReactRenderer } from "@tiptap/react";
3929
+ import tippy from "tippy.js";
3930
+ import { forwardRef, useImperativeHandle, useState as useState11, useEffect as useEffect9, useRef as useRef7 } from "react";
3931
+ import { Box as Box10, Text as Text9, UnstyledButton as UnstyledButton2 } from "@mantine/core";
3932
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
3933
+ var SlashCommandList = forwardRef((props, ref) => {
3934
+ const [selectedIndex, setSelectedIndex] = useState11(0);
3935
+ const scrollRef = useRef7(null);
3936
+ useEffect9(() => setSelectedIndex(0), [props.items]);
3937
+ const selectItem = (index) => {
3938
+ const item = props.items[index];
3939
+ if (item) {
3940
+ props.command(item);
3941
+ }
3942
+ };
3943
+ useImperativeHandle(ref, () => ({
3944
+ onKeyDown: ({ event }) => {
3945
+ if (event.key === "ArrowUp") {
3946
+ setSelectedIndex((prev) => (prev - 1 + props.items.length) % props.items.length);
3947
+ return true;
3948
+ }
3949
+ if (event.key === "ArrowDown") {
3950
+ setSelectedIndex((prev) => (prev + 1) % props.items.length);
3951
+ return true;
3952
+ }
3953
+ if (event.key === "Enter" || event.key === "Tab") {
3954
+ selectItem(selectedIndex);
3955
+ return true;
3956
+ }
3957
+ return false;
3958
+ }
3959
+ }));
3960
+ useEffect9(() => {
3961
+ const activeItem = scrollRef.current?.querySelector(`[data-index="${selectedIndex}"]`);
3962
+ if (activeItem) {
3963
+ activeItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
3964
+ }
3965
+ }, [selectedIndex]);
3966
+ if (props.items.length === 0) {
3967
+ return /* @__PURE__ */ jsx15(Box10, { p: "xs", bg: "var(--mantine-color-body)", style: { borderRadius: 8, border: "1px solid var(--mantine-color-default-border)" }, children: /* @__PURE__ */ jsx15(Text9, { size: "xs", c: "dimmed", children: "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3..." }) });
3968
+ }
3969
+ return /* @__PURE__ */ jsxs14(
3970
+ Box10,
3971
+ {
3972
+ ref: scrollRef,
3973
+ style: {
3974
+ background: "var(--mantine-color-body)",
3975
+ backdropFilter: "blur(12px)",
3976
+ border: "1px solid var(--mantine-color-default-border)",
3977
+ borderRadius: "12px",
3978
+ boxShadow: "var(--mantine-shadow-xl)",
3979
+ padding: "4px",
3980
+ maxHeight: "320px",
3981
+ overflowY: "auto",
3982
+ width: "300px",
3983
+ animation: "slash-in 0.1s ease-out"
3984
+ },
3985
+ children: [
3986
+ /* @__PURE__ */ jsx15("style", { children: `
3987
+ @keyframes slash-in {
3988
+ from { opacity: 0; transform: translateY(5px); }
3989
+ to { opacity: 1; transform: translateY(0); }
3990
+ }
3991
+ ` }),
3992
+ props.items.map((item, index) => {
3993
+ const isSelected = index === selectedIndex;
3994
+ return /* @__PURE__ */ jsxs14(
3995
+ UnstyledButton2,
3996
+ {
3997
+ "data-index": index,
3998
+ onClick: () => selectItem(index),
3999
+ onMouseEnter: () => setSelectedIndex(index),
4000
+ style: {
4001
+ display: "flex",
4002
+ width: "100%",
4003
+ padding: "8px 12px",
4004
+ borderRadius: "8px",
4005
+ gap: "12px",
4006
+ alignItems: "center",
4007
+ backgroundColor: isSelected ? "var(--mantine-color-blue-light)" : "transparent",
4008
+ transition: "background-color 0.1s ease"
4009
+ },
4010
+ children: [
4011
+ /* @__PURE__ */ jsx15(
4012
+ Box10,
4013
+ {
4014
+ style: {
4015
+ width: "32px",
4016
+ height: "32px",
4017
+ borderRadius: "6px",
4018
+ display: "flex",
4019
+ alignItems: "center",
4020
+ justifyContent: "center",
4021
+ backgroundColor: isSelected ? "var(--mantine-color-default-hover)" : "var(--mantine-color-default)",
4022
+ boxShadow: isSelected ? "var(--mantine-shadow-xs)" : "none",
4023
+ fontSize: "16px",
4024
+ color: isSelected ? "var(--mantine-color-blue-filled)" : "var(--mantine-color-default-color)"
4025
+ },
4026
+ children: item.icon
4027
+ }
4028
+ ),
4029
+ /* @__PURE__ */ jsxs14(Box10, { style: { flex: 1 }, children: [
4030
+ /* @__PURE__ */ jsx15(Text9, { size: "sm", fw: isSelected ? 600 : 500, lh: 1.2, c: isSelected ? "var(--mantine-color-blue-filled)" : "inherit", children: item.title }),
4031
+ /* @__PURE__ */ jsx15(Text9, { size: "xs", c: "dimmed", lh: 1.2, mt: 2, children: item.description })
4032
+ ] })
4033
+ ]
4034
+ },
4035
+ item.title
4036
+ );
4037
+ })
4038
+ ]
4039
+ }
4040
+ );
4041
+ });
4042
+ SlashCommandList.displayName = "SlashCommandList";
4043
+ var createSlashCommandRenderer = () => {
4044
+ let component = null;
4045
+ let popup = null;
4046
+ return {
4047
+ onStart: (props) => {
4048
+ if (popup) {
4049
+ popup.destroy();
4050
+ popup = null;
4051
+ }
4052
+ if (component) {
4053
+ component.destroy();
4054
+ component = null;
4055
+ }
4056
+ component = new ReactRenderer(SlashCommandList, {
4057
+ props,
4058
+ editor: props.editor
4059
+ });
4060
+ if (!props.clientRect) return;
4061
+ popup = tippy("body", {
4062
+ getReferenceClientRect: props.clientRect,
4063
+ appendTo: () => document.body,
4064
+ content: component.element,
4065
+ showOnCreate: true,
4066
+ interactive: true,
4067
+ trigger: "manual",
4068
+ placement: "bottom-start",
4069
+ duration: [150, 0]
4070
+ })[0];
4071
+ },
4072
+ onUpdate(props) {
4073
+ component?.updateProps(props);
4074
+ if (props.clientRect && popup) {
4075
+ popup.setProps({
4076
+ getReferenceClientRect: props.clientRect
4077
+ });
4078
+ }
4079
+ },
4080
+ onKeyDown(props) {
4081
+ if (props.event.key === "Escape") {
4082
+ popup?.hide();
4083
+ return true;
4084
+ }
4085
+ return component?.ref?.onKeyDown?.(props);
4086
+ },
4087
+ onExit() {
4088
+ popup?.destroy();
4089
+ component?.destroy();
4090
+ popup = null;
4091
+ component = null;
4092
+ }
4093
+ };
4094
+ };
4095
+ function createSlashCommand(options) {
4096
+ return Extension2.create({
4097
+ name: "slashCommand",
4098
+ addProseMirrorPlugins() {
4099
+ return [
4100
+ Suggestion({
4101
+ editor: this.editor,
4102
+ char: options.char,
4103
+ items: options.items,
4104
+ render: createSlashCommandRenderer,
4105
+ command: ({ editor, range, props }) => {
4106
+ editor.chain().focus().deleteRange(range).run();
4107
+ props.command({ editor, range });
4108
+ }
4109
+ })
4110
+ ];
4111
+ },
4112
+ onDestroy() {
4113
+ }
4114
+ });
4115
+ }
4116
+
4117
+ // src/editor/components/BlockRenderer.tsx
4118
+ import { memo as memo7 } from "react";
4119
+
4120
+ // src/editor/hooks/blockRegistry.tsx
4121
+ import {
4122
+ IconH1,
4123
+ IconH2,
4124
+ IconH3,
4125
+ IconList as IconList2,
4126
+ IconListNumbers,
4127
+ IconListCheck as IconListCheck2,
4128
+ IconCode as IconCode4,
4129
+ IconQuote as IconQuote2,
4130
+ IconMinus,
4131
+ IconTable,
4132
+ IconPhoto,
4133
+ IconBold as IconBold2,
4134
+ IconItalic as IconItalic2,
4135
+ IconNote,
4136
+ IconTimelineEvent,
4137
+ IconChartArea as IconChartArea2
4138
+ } from "@tabler/icons-react";
4139
+ import { jsx as jsx16 } from "react/jsx-runtime";
4140
+ var BLOCKS = [];
4141
+ function registerBlock(block) {
4142
+ const existingIndex = BLOCKS.findIndex((b) => b.type === block.type);
4143
+ if (existingIndex !== -1) {
4144
+ BLOCKS[existingIndex] = block;
4145
+ } else {
4146
+ BLOCKS.push(block);
4147
+ }
4148
+ }
4149
+ function getBlockByType(type) {
4150
+ return BLOCKS.find((b) => b.type === type);
4151
+ }
4152
+ function filterBlocks(query) {
4153
+ const lowerQuery = query.toLowerCase();
4154
+ return BLOCKS.filter(
4155
+ (block) => block.label.toLowerCase().includes(lowerQuery) || block.description?.toLowerCase().includes(lowerQuery) || block.keywords?.some((keyword) => keyword.toLowerCase().includes(lowerQuery))
4156
+ );
4157
+ }
4158
+ function getDefaultBlockItems(editor) {
4159
+ return [
4160
+ {
4161
+ title: "Heading 1",
4162
+ description: "Large section heading",
4163
+ icon: /* @__PURE__ */ jsx16(IconH1, { size: 20 }),
4164
+ keywords: ["heading", "title", "h1"],
4165
+ command: () => editor.chain().focus().toggleHeading({ level: 1 }).run()
4166
+ },
4167
+ {
4168
+ title: "Heading 2",
4169
+ description: "Medium section heading",
4170
+ icon: /* @__PURE__ */ jsx16(IconH2, { size: 20 }),
4171
+ keywords: ["heading", "subtitle", "h2"],
4172
+ command: () => editor.chain().focus().toggleHeading({ level: 2 }).run()
4173
+ },
4174
+ {
4175
+ title: "Heading 3",
4176
+ description: "Small section heading",
4177
+ icon: /* @__PURE__ */ jsx16(IconH3, { size: 20 }),
4178
+ keywords: ["heading", "subtitle", "h3"],
4179
+ command: () => editor.chain().focus().toggleHeading({ level: 3 }).run()
4180
+ },
4181
+ {
4182
+ title: "Bullet List",
4183
+ description: "Create a simple bullet list",
4184
+ icon: /* @__PURE__ */ jsx16(IconList2, { size: 20 }),
4185
+ keywords: ["bullet", "list", "unordered"],
4186
+ command: () => editor.chain().focus().toggleBulletList().run()
4187
+ },
4188
+ {
4189
+ title: "Numbered List",
4190
+ description: "Create a numbered list",
4191
+ icon: /* @__PURE__ */ jsx16(IconListNumbers, { size: 20 }),
4192
+ keywords: ["number", "ordered", "list"],
4193
+ command: () => editor.chain().focus().toggleOrderedList().run()
4194
+ },
4195
+ {
4196
+ title: "Task List",
4197
+ description: "Create a checklist with checkboxes",
4198
+ icon: /* @__PURE__ */ jsx16(IconListCheck2, { size: 20 }),
4199
+ keywords: ["task", "checkbox", "todo", "checklist"],
4200
+ command: () => editor.chain().focus().toggleTaskList().run()
4201
+ },
4202
+ {
4203
+ title: "Code Block",
4204
+ description: "Insert a code block with syntax highlighting",
4205
+ icon: /* @__PURE__ */ jsx16(IconCode4, { size: 20 }),
4206
+ keywords: ["code", "block", "pre"],
4207
+ command: () => editor.chain().focus().toggleCodeBlock().run()
4208
+ },
4209
+ {
4210
+ title: "Quote",
4211
+ description: "Insert a blockquote",
4212
+ icon: /* @__PURE__ */ jsx16(IconQuote2, { size: 20 }),
4213
+ keywords: ["quote", "blockquote", "citation"],
4214
+ command: () => editor.chain().focus().toggleBlockquote().run()
4215
+ },
4216
+ {
4217
+ title: "Horizontal Rule",
4218
+ description: "Insert a horizontal divider",
4219
+ icon: /* @__PURE__ */ jsx16(IconMinus, { size: 20 }),
4220
+ keywords: ["horizontal", "rule", "divider", "line"],
4221
+ command: () => editor.chain().focus().setHorizontalRule().run()
4222
+ },
4223
+ {
4224
+ title: "Table",
4225
+ description: "Insert a table",
4226
+ icon: /* @__PURE__ */ jsx16(IconTable, { size: 20 }),
4227
+ keywords: ["table", "grid"],
4228
+ command: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
4229
+ },
4230
+ {
4231
+ title: "Image",
4232
+ description: "Insert an image from URL or file",
4233
+ icon: /* @__PURE__ */ jsx16(IconPhoto, { size: 20 }),
4234
+ keywords: ["image", "picture", "photo"],
4235
+ command: ({ editor: editor2 }) => {
4236
+ const choice = window.confirm(
4237
+ "Click OK to select an image file, or Cancel to enter a URL."
4238
+ );
4239
+ if (choice) {
4240
+ const input = document.createElement("input");
4241
+ input.type = "file";
4242
+ input.accept = "image/*";
4243
+ input.onchange = (e) => {
4244
+ const file = e.target.files?.[0];
4245
+ if (file) {
4246
+ const reader = new FileReader();
4247
+ reader.onload = (event) => {
4248
+ const src = event.target?.result;
4249
+ editor2.chain().focus().setImage({ src, alt: file.name }).run();
4250
+ };
4251
+ reader.readAsDataURL(file);
4252
+ }
4253
+ };
4254
+ input.click();
4255
+ } else {
4256
+ const url = prompt("Enter image URL:");
4257
+ if (url) {
4258
+ editor2.chain().focus().setImage({ src: url }).run();
4259
+ }
4260
+ }
4261
+ }
4262
+ },
4263
+ {
4264
+ title: "Bold",
4265
+ description: "Make text bold",
4266
+ icon: /* @__PURE__ */ jsx16(IconBold2, { size: 20 }),
4267
+ keywords: ["bold", "strong"],
4268
+ command: () => editor.chain().focus().toggleBold().run()
4269
+ },
4270
+ {
4271
+ title: "Italic",
4272
+ description: "Make text italic",
4273
+ icon: /* @__PURE__ */ jsx16(IconItalic2, { size: 20 }),
4274
+ keywords: ["italic", "em"],
4275
+ command: () => editor.chain().focus().toggleItalic().run()
4276
+ },
4277
+ {
4278
+ title: "Notice",
4279
+ description: "Insert a notice callout box",
4280
+ icon: /* @__PURE__ */ jsx16(IconNote, { size: 20 }),
4281
+ keywords: ["info", "warning", "callout", "notice"],
4282
+ command: () => {
4283
+ editor.chain().focus().setNotice({ variant: "warning", title: "Notice" }).run();
4284
+ }
4285
+ },
4286
+ {
4287
+ title: "Timeline",
4288
+ description: "Create a visual timeline of events",
4289
+ icon: /* @__PURE__ */ jsx16(IconTimelineEvent, { size: 20 }),
4290
+ keywords: ["timeline", "events", "history", "chronology"],
4291
+ command: () => {
4292
+ editor.chain().focus().setTimeline().run();
4293
+ }
4294
+ },
4295
+ {
4296
+ title: "Diagram",
4297
+ description: "Insert a Mermaid diagram (flowchart, sequence, etc.)",
4298
+ icon: /* @__PURE__ */ jsx16(IconChartArea2, { size: 20 }),
4299
+ keywords: ["diagram", "mermaid", "flowchart", "sequence", "chart", "graph"],
4300
+ command: () => {
4301
+ editor.chain().focus().setDiagram().run();
4302
+ }
4303
+ }
4304
+ ];
4305
+ }
4306
+
4307
+ // src/editor/components/BlockRenderer.tsx
4308
+ import { jsx as jsx17 } from "react/jsx-runtime";
4309
+ var BlockRenderer = memo7(function BlockRenderer2({
4310
+ node,
4311
+ editor,
4312
+ updateAttributes,
4313
+ deleteNode
4314
+ }) {
4315
+ const block = getBlockByType(node.type.name);
4316
+ if (!block) {
4317
+ return /* @__PURE__ */ jsx17("div", { children: node.textContent });
4318
+ }
4319
+ const Component = block.render;
4320
+ return /* @__PURE__ */ jsx17(
4321
+ Component,
4322
+ {
4323
+ node,
4324
+ editor,
4325
+ updateAttributes,
4326
+ deleteNode
4327
+ }
4328
+ );
4329
+ });
4330
+
4331
+ // src/editor/extensions/createBlockNode.ts
4332
+ import { Node as Node4, mergeAttributes as mergeAttributes5 } from "@tiptap/core";
4333
+ import { ReactNodeViewRenderer as ReactNodeViewRenderer7 } from "@tiptap/react";
4334
+ function createBlockNode(options) {
4335
+ const {
4336
+ name,
4337
+ group = "block",
4338
+ content = "inline*",
4339
+ draggable = false,
4340
+ defining = true,
4341
+ component
4342
+ } = options;
4343
+ return Node4.create({
4344
+ name,
4345
+ group,
4346
+ content,
4347
+ draggable,
4348
+ defining,
4349
+ parseHTML() {
4350
+ return [{ tag: `div[data-type="${name}"]` }];
4351
+ },
4352
+ renderHTML({ HTMLAttributes }) {
4353
+ return ["div", mergeAttributes5(HTMLAttributes, { "data-type": name }), 0];
4354
+ },
4355
+ addNodeView() {
4356
+ if (!component) {
4357
+ return null;
4358
+ }
4359
+ return ReactNodeViewRenderer7(component);
4360
+ },
4361
+ addAttributes() {
4362
+ return {
4363
+ variant: {
4364
+ default: null,
4365
+ parseHTML: (element) => element.getAttribute("data-variant"),
4366
+ renderHTML: (attributes) => ({ "data-variant": attributes.variant })
4367
+ },
4368
+ title: {
4369
+ default: null,
4370
+ parseHTML: (element) => element.getAttribute("data-title"),
4371
+ renderHTML: (attributes) => ({ "data-title": attributes.title })
4372
+ },
4373
+ checked: {
4374
+ default: false,
4375
+ parseHTML: (element) => element.getAttribute("data-checked") === "true",
4376
+ renderHTML: (attributes) => ({ "data-checked": attributes.checked })
4377
+ },
4378
+ language: {
4379
+ default: null,
4380
+ parseHTML: (element) => element.getAttribute("data-language"),
4381
+ renderHTML: (attributes) => ({ "data-language": attributes.language })
4382
+ }
4383
+ };
4384
+ },
4385
+ addCommands() {
4386
+ return {
4387
+ [`set${name.charAt(0).toUpperCase() + name.slice(1)}`]: (attrs) => ({ commands }) => {
4388
+ return commands.insertContent({
4389
+ type: this.name,
4390
+ attrs,
4391
+ content: content ? [{ type: "paragraph" }] : []
4392
+ });
4393
+ }
4394
+ };
4395
+ }
4396
+ });
4397
+ }
4398
+
4399
+ // src/editor/utils/contentMigration.ts
4400
+ import MarkdownIt from "markdown-it";
4401
+ var md = new MarkdownIt({
4402
+ html: true,
4403
+ breaks: false,
4404
+ linkify: true
4405
+ });
4406
+ md.block.ruler.before("paragraph", "table", (state, startLine, endLine, silent) => {
4407
+ const startPos = state.bMarks[startLine] + state.tShift[startLine];
4408
+ const endPos = state.eMarks[startLine];
4409
+ const firstLine = state.src.slice(startPos, endPos).trim();
4410
+ if (!firstLine.startsWith("|") && !firstLine.startsWith("|-")) {
4411
+ return false;
4412
+ }
4413
+ let nextLine = startLine;
4414
+ const rows = [];
4415
+ while (nextLine < endLine) {
4416
+ const rowStart = state.bMarks[nextLine] + state.tShift[nextLine];
4417
+ const rowEnd = state.eMarks[nextLine];
4418
+ const rowText = state.src.slice(rowStart, rowEnd).trim();
4419
+ if (!rowText.startsWith("|")) {
4420
+ break;
4421
+ }
4422
+ rows.push(rowText);
4423
+ nextLine++;
4424
+ }
4425
+ if (rows.length < 2) {
4426
+ return false;
4427
+ }
4428
+ const separatorRow = rows[1].trim();
4429
+ if (!/^[\s|:\-]+$/.test(separatorRow)) {
4430
+ return false;
4431
+ }
4432
+ if (silent) {
4433
+ return true;
4434
+ }
4435
+ const parseCells = (row, cellType) => {
4436
+ const cells = row.split("|").filter((c) => c.trim() !== "");
4437
+ cells.forEach((cellText) => {
4438
+ state.push(cellType + "_open", cellType, 1);
4439
+ const inline = state.push("inline", "", 0);
4440
+ inline.content = cellText.trim();
4441
+ inline.children = [];
4442
+ state.push(cellType + "_close", cellType, -1);
4443
+ });
4444
+ };
4445
+ const tableOpen = state.push("table_open", "table", 1);
4446
+ tableOpen.map = [startLine, nextLine];
4447
+ state.push("thead_open", "thead", 1);
4448
+ state.push("tr_open", "tr", 1);
4449
+ parseCells(rows[0], "th");
4450
+ state.push("tr_close", "tr", -1);
4451
+ state.push("thead_close", "thead", -1);
4452
+ state.push("tbody_open", "tbody", 1);
4453
+ for (let i = 2; i < rows.length; i++) {
4454
+ state.push("tr_open", "tr", 1);
4455
+ parseCells(rows[i], "td");
4456
+ state.push("tr_close", "tr", -1);
4457
+ }
4458
+ state.push("tbody_close", "tbody", -1);
4459
+ state.push("table_close", "table", -1);
4460
+ state.line = nextLine;
4461
+ return true;
4462
+ }, { alt: ["paragraph", "reference", "blockquote"] });
4463
+ var markdownToTiptapJSON = (markdown) => {
4464
+ if (!markdown || !markdown.trim()) {
4465
+ return JSON.stringify({ type: "doc", content: [] });
4466
+ }
4467
+ try {
4468
+ const parsed = JSON.parse(markdown);
4469
+ if (parsed.type === "doc") {
4470
+ return markdown;
4471
+ }
4472
+ } catch {
4473
+ }
4474
+ const tokens = md.parse(markdown, {});
4475
+ const content = convertTokensToTiptap(tokens);
4476
+ return JSON.stringify({ type: "doc", content });
4477
+ };
4478
+ var convertTokensToTiptap = (tokens) => {
4479
+ const content = [];
4480
+ let i = 0;
4481
+ while (i < tokens.length) {
4482
+ const token = tokens[i];
4483
+ switch (token.type) {
4484
+ case "heading_open": {
4485
+ const level = parseInt(token.tag.replace("h", ""));
4486
+ const inline = tokens[i + 1];
4487
+ const text = inline?.content || "";
4488
+ content.push({
4489
+ type: "heading",
4490
+ attrs: { level },
4491
+ content: text ? parseInline(text) : []
4492
+ });
4493
+ i += 3;
4494
+ break;
4495
+ }
4496
+ case "paragraph_open": {
4497
+ const inline = tokens[i + 1];
4498
+ if (inline?.type === "inline" && inline.content?.trim()) {
4499
+ content.push({
4500
+ type: "paragraph",
4501
+ content: parseInline(inline.content)
4502
+ });
4503
+ }
4504
+ i += 3;
4505
+ break;
4506
+ }
4507
+ case "table_open": {
4508
+ const tableContent = [];
4509
+ let j = i + 1;
4510
+ while (j < tokens.length && tokens[j].type !== "table_close") {
4511
+ const t = tokens[j];
4512
+ if (t.type === "tr_open") {
4513
+ const rowCells = [];
4514
+ let k = j + 1;
4515
+ while (k < tokens.length && tokens[k].type !== "tr_close") {
4516
+ const cell = tokens[k];
4517
+ if (cell.type === "th_open" || cell.type === "td_open") {
4518
+ const cellType = cell.type === "th_open" ? "tableHeader" : "tableCell";
4519
+ const inline = tokens[k + 1];
4520
+ const cellContent = inline?.content ? [{ type: "paragraph", content: parseInline(inline.content) }] : [{ type: "paragraph", content: [] }];
4521
+ rowCells.push({
4522
+ type: cellType,
4523
+ attrs: { colspan: 1, rowspan: 1, colwidth: null },
4524
+ content: cellContent
4525
+ });
4526
+ k += 3;
4527
+ } else {
4528
+ k++;
4529
+ }
4530
+ }
4531
+ tableContent.push({
4532
+ type: "tableRow",
4533
+ content: rowCells
4534
+ });
4535
+ j = k + 1;
4536
+ } else {
4537
+ j++;
4538
+ }
4539
+ }
4540
+ content.push({
4541
+ type: "table",
4542
+ content: tableContent
4543
+ });
4544
+ i = j + 1;
4545
+ break;
4546
+ }
4547
+ case "bullet_list_open": {
4548
+ const items = [];
4549
+ let j = i + 1;
4550
+ let isTaskList = false;
4551
+ let peekJ = j;
4552
+ while (peekJ < tokens.length && tokens[peekJ].type !== "bullet_list_close") {
4553
+ if (tokens[peekJ].type === "list_item_open" && tokens[peekJ + 1]?.type === "paragraph_open") {
4554
+ const inline = tokens[peekJ + 2];
4555
+ if (inline?.type === "inline" && inline.content?.match(/^\[[ xX]\]/)) {
4556
+ isTaskList = true;
4557
+ break;
4558
+ }
4559
+ }
4560
+ peekJ++;
4561
+ }
4562
+ while (j < tokens.length && tokens[j].type !== "bullet_list_close") {
4563
+ if (tokens[j].type === "list_item_open") {
4564
+ const itemContent = [];
4565
+ let k = j + 1;
4566
+ while (k < tokens.length && tokens[k].type !== "list_item_close") {
4567
+ if (tokens[k].type === "paragraph_open" && tokens[k + 1]?.type === "inline") {
4568
+ const text = tokens[k + 1].content;
4569
+ if (text?.trim()) {
4570
+ const taskMatch = text.match(/^\[([ xX])\]\s*(.*)/);
4571
+ if (isTaskList && taskMatch) {
4572
+ const checked = taskMatch[1].toLowerCase() === "x";
4573
+ const remainingText = taskMatch[2];
4574
+ itemContent.push({
4575
+ type: "taskItem",
4576
+ attrs: { checked },
4577
+ content: remainingText.trim() ? [{ type: "paragraph", content: parseInline(remainingText) }] : [{ type: "paragraph", content: [] }]
4578
+ });
4579
+ } else {
4580
+ itemContent.push({
4581
+ type: "listItem",
4582
+ content: [{
4583
+ type: "paragraph",
4584
+ content: parseInline(text)
4585
+ }]
4586
+ });
4587
+ }
4588
+ }
4589
+ k += 3;
4590
+ } else {
4591
+ k++;
4592
+ }
4593
+ }
4594
+ items.push(...itemContent);
4595
+ j = k + 1;
4596
+ } else {
4597
+ j++;
4598
+ }
4599
+ }
4600
+ if (isTaskList) {
4601
+ content.push({
4602
+ type: "taskList",
4603
+ content: items
4604
+ });
4605
+ } else {
4606
+ content.push({
4607
+ type: "bulletList",
4608
+ attrs: { tight: true },
4609
+ content: items
4610
+ });
4611
+ }
4612
+ i = j + 1;
4613
+ break;
4614
+ }
4615
+ case "ordered_list_open": {
4616
+ const items = [];
4617
+ let j = i + 1;
4618
+ while (j < tokens.length && tokens[j].type !== "ordered_list_close") {
4619
+ if (tokens[j].type === "list_item_open") {
4620
+ const itemContent = [];
4621
+ let k = j + 1;
4622
+ while (k < tokens.length && tokens[k].type !== "list_item_close") {
4623
+ if (tokens[k].type === "paragraph_open" && tokens[k + 1]?.type === "inline") {
4624
+ const text = tokens[k + 1].content;
4625
+ if (text?.trim()) {
4626
+ itemContent.push({
4627
+ type: "paragraph",
4628
+ content: parseInline(text)
4629
+ });
4630
+ }
4631
+ k += 3;
4632
+ } else {
4633
+ k++;
4634
+ }
4635
+ }
4636
+ items.push({
4637
+ type: "listItem",
4638
+ content: itemContent.length ? itemContent : [{ type: "paragraph", content: [] }]
4639
+ });
4640
+ j = k + 1;
4641
+ } else {
4642
+ j++;
4643
+ }
4644
+ }
4645
+ content.push({
4646
+ type: "orderedList",
4647
+ attrs: { start: 1 },
4648
+ content: items
4649
+ });
4650
+ i = j + 1;
4651
+ break;
4652
+ }
4653
+ case "blockquote_open": {
4654
+ const quoteContent = [];
4655
+ let j = i + 1;
4656
+ while (j < tokens.length && tokens[j].type !== "blockquote_close") {
4657
+ if (tokens[j].type === "paragraph_open" && tokens[j + 1]?.type === "inline") {
4658
+ const text = tokens[j + 1].content;
4659
+ quoteContent.push({
4660
+ type: "paragraph",
4661
+ content: parseInline(text)
4662
+ });
4663
+ j += 3;
4664
+ } else {
4665
+ j++;
4666
+ }
4667
+ }
4668
+ content.push({
4669
+ type: "blockquote",
4670
+ content: quoteContent
4671
+ });
4672
+ i = j + 1;
4673
+ break;
4674
+ }
4675
+ case "fence":
4676
+ case "code_block": {
4677
+ const language = token.info?.trim() || "";
4678
+ content.push({
4679
+ type: "codeBlock",
4680
+ attrs: { language },
4681
+ content: [{ type: "text", text: token.content }]
4682
+ });
4683
+ i++;
4684
+ break;
4685
+ }
4686
+ case "hr":
4687
+ content.push({ type: "horizontalRule" });
4688
+ i++;
4689
+ break;
4690
+ default:
4691
+ i++;
4692
+ }
4693
+ }
4694
+ return content;
4695
+ };
4696
+ var parseInline = (text) => {
4697
+ const nodes = [];
4698
+ let remaining = text;
4699
+ while (remaining.length > 0) {
4700
+ const codeMatch = remaining.match(/^`([^`]+)`/);
4701
+ if (codeMatch) {
4702
+ nodes.push({ type: "text", text: codeMatch[1], marks: [{ type: "code" }] });
4703
+ remaining = remaining.slice(codeMatch[0].length);
4704
+ continue;
4705
+ }
4706
+ const boldItalicMatch = remaining.match(/^\*\*\*([^*]+)\*\*\*/);
4707
+ if (boldItalicMatch) {
4708
+ nodes.push({
4709
+ type: "text",
4710
+ text: boldItalicMatch[1],
4711
+ marks: [{ type: "bold" }, { type: "italic" }]
4712
+ });
4713
+ remaining = remaining.slice(boldItalicMatch[0].length);
4714
+ continue;
4715
+ }
4716
+ const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/);
4717
+ if (boldMatch) {
4718
+ nodes.push({ type: "text", text: boldMatch[1], marks: [{ type: "bold" }] });
4719
+ remaining = remaining.slice(boldMatch[0].length);
4720
+ continue;
4721
+ }
4722
+ const italicMatch = remaining.match(/^\*([^*]+)\*/);
4723
+ if (italicMatch) {
4724
+ nodes.push({ type: "text", text: italicMatch[1], marks: [{ type: "italic" }] });
4725
+ remaining = remaining.slice(italicMatch[0].length);
4726
+ continue;
4727
+ }
4728
+ const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
4729
+ if (linkMatch) {
4730
+ nodes.push({
4731
+ type: "text",
4732
+ text: linkMatch[1],
4733
+ marks: [{ type: "link", attrs: { href: linkMatch[2] } }]
4734
+ });
4735
+ remaining = remaining.slice(linkMatch[0].length);
4736
+ continue;
4737
+ }
4738
+ const nextSpecial = remaining.match(/^[^`*\[\\]+/);
4739
+ if (nextSpecial) {
4740
+ nodes.push({ type: "text", text: nextSpecial[0] });
4741
+ remaining = remaining.slice(nextSpecial[0].length);
4742
+ } else {
4743
+ nodes.push({ type: "text", text: remaining[0] });
4744
+ remaining = remaining.slice(1);
4745
+ }
4746
+ }
4747
+ return nodes.length ? nodes : [{ type: "text", text }];
4748
+ };
4749
+ var isTiptapJSON = (content) => {
4750
+ if (!content) return false;
4751
+ try {
4752
+ const parsed = JSON.parse(content);
4753
+ return parsed.type === "doc";
4754
+ } catch {
4755
+ return false;
4756
+ }
4757
+ };
4758
+ var migrateToTiptapJSON = (content) => {
4759
+ if (isTiptapJSON(content)) {
4760
+ return content;
4761
+ }
4762
+ return markdownToTiptapJSON(content);
4763
+ };
4764
+ export {
4765
+ BACKEND_URL,
4766
+ BLOCKS,
4767
+ BlockRenderer,
4768
+ BlockWrapper,
4769
+ BubbleMenu,
4770
+ CodeBlock,
4771
+ CodeBlockExtension,
4772
+ DiagramBlock,
4773
+ DiagramExtension,
4774
+ DocumentEditor,
4775
+ EditorContent,
4776
+ MarkdownEditor_default as MarkdownEditor,
4777
+ NoticeBlock,
4778
+ NoticeExtension,
4779
+ SlashCommandList,
4780
+ TableControls,
4781
+ TableOfContentsPanel,
4782
+ TaskBlock,
4783
+ TaskItemExtension,
4784
+ TimelineBlock,
4785
+ TimelineExtension,
4786
+ apiClient,
4787
+ apiUtils,
4788
+ createBlockNode,
4789
+ createSlashCommand,
4790
+ filterBlocks,
4791
+ getBackendUrl,
4792
+ getBlockByType,
4793
+ getDefaultBlockItems,
4794
+ handlePaste,
4795
+ instructionsApi,
4796
+ isTiptapJSON,
4797
+ llmApi,
4798
+ markdownToTiptapJSON,
4799
+ migrateToTiptapJSON,
4800
+ registerBlock,
4801
+ serializeSelectionToMarkdown,
4802
+ serializeToMarkdown,
4803
+ setAccessTokenGetter,
4804
+ setBackendUrl,
4805
+ useEditorWithStorage
4806
+ };
4807
+ //# sourceMappingURL=editor.js.map