@frontlit/text-editor 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1474 @@
1
+ "use client";
2
+ import React5, { createContext, useState, useRef, useMemo, useEffect, useCallback, useContext } from 'react';
3
+ import { Node, generateText } from '@tiptap/core';
4
+ import { useEditor, EditorContent } from '@tiptap/react';
5
+ import StarterKit from '@tiptap/starter-kit';
6
+ import Placeholder from '@tiptap/extension-placeholder';
7
+ import Link from '@tiptap/extension-link';
8
+ import { TableRow, TableHeader, TableCell, Table } from '@tiptap/extension-table';
9
+ import Image from '@tiptap/extension-image';
10
+ import Dropcursor from '@tiptap/extension-dropcursor';
11
+ import Gapcursor from '@tiptap/extension-gapcursor';
12
+ import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
13
+ import Heading from '@tiptap/extension-heading';
14
+ import Highlight from '@tiptap/extension-highlight';
15
+ import { lowlight } from 'lowlight';
16
+ import javascript from 'highlight.js/lib/languages/javascript';
17
+ import typescript from 'highlight.js/lib/languages/typescript';
18
+ import json from 'highlight.js/lib/languages/json';
19
+ import css from 'highlight.js/lib/languages/css';
20
+ import xml from 'highlight.js/lib/languages/xml';
21
+ import { Undo2, Redo2, ChevronDown, Bold, Italic, Underline, Code, Link as Link$1, List, ListOrdered, Quote, Plus, Minus, Table as Table$1, Image as Image$1, Trash2, Strikethrough, X, Check, Copy } from 'lucide-react';
22
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
23
+ import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu';
24
+ import { clsx } from 'clsx';
25
+ import { twMerge } from 'tailwind-merge';
26
+ import { renderToReactElement } from '@tiptap/static-renderer';
27
+
28
+ // src/editor.tsx
29
+
30
+ // src/empty-doc.ts
31
+ var emptyDoc = {
32
+ type: "doc",
33
+ content: []
34
+ };
35
+ var empty_doc_default = emptyDoc;
36
+
37
+ // src/create-id.ts
38
+ function createId(textContent) {
39
+ return textContent.replace(/\s/g, "-").toLowerCase();
40
+ }
41
+ var CodeMirrorNode = Node.create({
42
+ name: "codeMirror",
43
+ content: "block+",
44
+ code: true,
45
+ addOptions() {
46
+ return {
47
+ HTMLAttributes: {}
48
+ };
49
+ },
50
+ parseHTML() {
51
+ return [
52
+ {
53
+ tag: "pre"
54
+ }
55
+ ];
56
+ },
57
+ renderHTML({ HTMLAttributes }) {
58
+ return ["pre", HTMLAttributes, 0];
59
+ }
60
+ });
61
+
62
+ // src/extensions.ts
63
+ lowlight.registerLanguage("javascript", javascript);
64
+ lowlight.registerLanguage("typescript", typescript);
65
+ lowlight.registerLanguage("json", json);
66
+ lowlight.registerLanguage("css", css);
67
+ lowlight.registerLanguage("html", xml);
68
+ var createExtensions = ({
69
+ placeholder
70
+ } = {}) => [
71
+ StarterKit.configure({
72
+ codeBlock: false,
73
+ // Configured/replaced explicitly below (custom options or a
74
+ // `.extend()`) — disabled here to avoid tiptap's duplicate-extension
75
+ // warning and constructing throwaway instances of each.
76
+ link: false,
77
+ dropcursor: false,
78
+ gapcursor: false,
79
+ heading: false
80
+ }),
81
+ Placeholder.configure({
82
+ placeholder: placeholder || "Write something\u2026"
83
+ }),
84
+ Link.configure({
85
+ openOnClick: false,
86
+ autolink: true,
87
+ linkOnPaste: true
88
+ }),
89
+ Table.configure({
90
+ resizable: true
91
+ }),
92
+ TableRow,
93
+ TableHeader,
94
+ TableCell,
95
+ Image.configure({
96
+ // We embed uploaded images as data URLs (no backend/media library in
97
+ // this package), so base64 sources must be allowed.
98
+ allowBase64: true,
99
+ HTMLAttributes: {
100
+ class: "max-w-full h-auto rounded-md"
101
+ },
102
+ resize: {
103
+ enabled: true,
104
+ directions: ["top", "bottom", "left", "right"],
105
+ alwaysPreserveAspectRatio: true
106
+ }
107
+ }),
108
+ Dropcursor.configure({
109
+ color: "hsl(var(--foreground))",
110
+ width: 2
111
+ }),
112
+ Gapcursor,
113
+ CodeBlockLowlight.configure({
114
+ lowlight
115
+ }),
116
+ Heading.extend({
117
+ renderHTML({ node, HTMLAttributes }) {
118
+ const level = this.options.levels.includes(node.attrs.level) ? node.attrs.level : this.options.levels[0];
119
+ const id = typeof node.textContent === "string" ? createId(node.textContent) : void 0;
120
+ return [
121
+ `h${level}`,
122
+ {
123
+ ...HTMLAttributes,
124
+ id
125
+ },
126
+ 0
127
+ ];
128
+ }
129
+ }),
130
+ Highlight,
131
+ CodeMirrorNode
132
+ ];
133
+ var LinkEditorPopover = ({
134
+ editor,
135
+ onClose,
136
+ variant = "panel",
137
+ containerRef
138
+ }) => {
139
+ const [linkValue, setLinkValue] = useState("");
140
+ const inputRef = useRef(null);
141
+ const localContainerRef = useRef(null);
142
+ const assignContainerRef = (node) => {
143
+ localContainerRef.current = node;
144
+ if (containerRef) {
145
+ containerRef.current = node;
146
+ }
147
+ };
148
+ useEffect(() => {
149
+ const current = editor.getAttributes("link").href;
150
+ setLinkValue(current ?? "");
151
+ }, [editor]);
152
+ useEffect(() => {
153
+ const timer = window.setTimeout(() => {
154
+ inputRef.current?.focus();
155
+ }, 20);
156
+ return () => window.clearTimeout(timer);
157
+ }, []);
158
+ const applyLink = () => {
159
+ const value = linkValue.trim();
160
+ if (!value) {
161
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
162
+ } else {
163
+ editor.chain().focus().extendMarkRange("link").setLink({ href: value }).run();
164
+ }
165
+ onClose();
166
+ };
167
+ const removeLink = () => {
168
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
169
+ onClose();
170
+ };
171
+ const handleKeyDown = (event) => {
172
+ if (event.key === "Enter") {
173
+ event.preventDefault();
174
+ applyLink();
175
+ }
176
+ if (event.key === "Escape") {
177
+ event.preventDefault();
178
+ onClose();
179
+ }
180
+ };
181
+ if (variant === "inline") {
182
+ return /* @__PURE__ */ jsxs(
183
+ "div",
184
+ {
185
+ ref: assignContainerRef,
186
+ className: "flex items-center gap-2 rounded-md border border-border bg-background p-2 shadow",
187
+ onMouseDown: (event) => event.stopPropagation(),
188
+ children: [
189
+ /* @__PURE__ */ jsx(
190
+ "input",
191
+ {
192
+ ref: inputRef,
193
+ type: "url",
194
+ placeholder: "https://example.com",
195
+ value: linkValue,
196
+ onChange: (event) => setLinkValue(event.target.value),
197
+ onKeyDown: handleKeyDown,
198
+ className: "w-48 rounded border border-border bg-background px-2 py-1 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
199
+ }
200
+ ),
201
+ /* @__PURE__ */ jsx(
202
+ "button",
203
+ {
204
+ type: "button",
205
+ "aria-label": "Cancel link editing",
206
+ className: "inline-flex h-8 w-8 items-center justify-center rounded border border-border text-foreground transition-colors hover:bg-muted",
207
+ onClick: onClose,
208
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
209
+ }
210
+ ),
211
+ /* @__PURE__ */ jsx(
212
+ "button",
213
+ {
214
+ type: "button",
215
+ "aria-label": "Remove link",
216
+ className: "inline-flex h-8 w-8 items-center justify-center rounded border border-border text-foreground transition-colors hover:bg-muted",
217
+ onClick: removeLink,
218
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
219
+ }
220
+ ),
221
+ /* @__PURE__ */ jsx(
222
+ "button",
223
+ {
224
+ type: "button",
225
+ "aria-label": "Apply link",
226
+ className: "inline-flex h-8 w-8 items-center justify-center rounded bg-primary text-primary-foreground transition-colors hover:bg-primary/90",
227
+ onClick: applyLink,
228
+ children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
229
+ }
230
+ )
231
+ ]
232
+ }
233
+ );
234
+ }
235
+ return /* @__PURE__ */ jsx(
236
+ "div",
237
+ {
238
+ ref: assignContainerRef,
239
+ className: "absolute left-2 top-full z-20 mt-2 w-full max-w-sm rounded-md border border-border bg-background p-3 shadow-lg",
240
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
241
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: "Link URL" }),
242
+ /* @__PURE__ */ jsx(
243
+ "input",
244
+ {
245
+ ref: inputRef,
246
+ type: "url",
247
+ placeholder: "https://example.com",
248
+ value: linkValue,
249
+ onChange: (event) => setLinkValue(event.target.value),
250
+ onKeyDown: handleKeyDown,
251
+ className: "w-full rounded border border-border bg-background px-2 py-1 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
252
+ }
253
+ ),
254
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
255
+ /* @__PURE__ */ jsxs(
256
+ "button",
257
+ {
258
+ type: "button",
259
+ className: "inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-sm text-foreground transition-colors hover:bg-muted",
260
+ onClick: onClose,
261
+ children: [
262
+ /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
263
+ "Cancel"
264
+ ]
265
+ }
266
+ ),
267
+ /* @__PURE__ */ jsxs(
268
+ "button",
269
+ {
270
+ type: "button",
271
+ className: "inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-sm text-foreground transition-colors hover:bg-muted",
272
+ onClick: removeLink,
273
+ children: [
274
+ /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" }),
275
+ "Remove"
276
+ ]
277
+ }
278
+ ),
279
+ /* @__PURE__ */ jsxs(
280
+ "button",
281
+ {
282
+ type: "button",
283
+ className: "inline-flex items-center gap-1 rounded bg-primary px-2 py-1 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90",
284
+ onClick: applyLink,
285
+ children: [
286
+ /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
287
+ "Apply"
288
+ ]
289
+ }
290
+ )
291
+ ] })
292
+ ] })
293
+ }
294
+ );
295
+ };
296
+ var link_editor_popover_default = LinkEditorPopover;
297
+ var iconButtonBase = "inline-flex h-8 w-8 items-center justify-center rounded border border-transparent text-foreground transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50";
298
+ function IconButton({
299
+ icon: Icon,
300
+ label,
301
+ active,
302
+ disabled,
303
+ onClick
304
+ }) {
305
+ return /* @__PURE__ */ jsx(
306
+ "button",
307
+ {
308
+ type: "button",
309
+ "aria-label": label,
310
+ title: label,
311
+ className: `${iconButtonBase} ${active ? "bg-primary text-primary-foreground hover:bg-primary/90" : "bg-transparent hover:bg-muted"}`,
312
+ disabled,
313
+ onClick,
314
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
315
+ }
316
+ );
317
+ }
318
+ var headingOptions = [
319
+ { label: "Paragraph", value: "paragraph" },
320
+ { label: "Heading 1", value: "h1", level: 1 },
321
+ { label: "Heading 2", value: "h2", level: 2 },
322
+ { label: "Heading 3", value: "h3", level: 3 }
323
+ ];
324
+ function Divider() {
325
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-1 h-6 w-px shrink-0 bg-border" });
326
+ }
327
+ var Toolbar = ({
328
+ editor,
329
+ onRequestImageUpload,
330
+ isUploadingImage
331
+ }) => {
332
+ const [activePopover, setActivePopover] = useState(null);
333
+ const [wordCount, setWordCount] = useState(0);
334
+ const popoverRef = useRef(null);
335
+ useEffect(() => {
336
+ if (!activePopover) return void 0;
337
+ const handleClick = (event) => {
338
+ if (popoverRef.current && !popoverRef.current.contains(event.target)) {
339
+ setActivePopover(null);
340
+ }
341
+ };
342
+ const handleKeyDown = (event) => {
343
+ if (event.key === "Escape") setActivePopover(null);
344
+ };
345
+ document.addEventListener("mousedown", handleClick);
346
+ document.addEventListener("keydown", handleKeyDown);
347
+ return () => {
348
+ document.removeEventListener("mousedown", handleClick);
349
+ document.removeEventListener("keydown", handleKeyDown);
350
+ };
351
+ }, [activePopover]);
352
+ useEffect(() => {
353
+ if (!editor) return void 0;
354
+ const updateStats = () => {
355
+ const text = editor.getText().trim();
356
+ setWordCount(text ? text.split(/\s+/).length : 0);
357
+ };
358
+ updateStats();
359
+ editor.on("update", updateStats);
360
+ return () => {
361
+ editor.off("update", updateStats);
362
+ };
363
+ }, [editor]);
364
+ if (!editor) return null;
365
+ const activeHeading = headingOptions.find(
366
+ (option) => option.level ? editor.isActive("heading", { level: option.level }) : editor.isActive("paragraph")
367
+ ) ?? headingOptions[0];
368
+ const readTime = wordCount === 0 ? 0 : Math.max(1, Math.ceil(wordCount / 200));
369
+ const selectHeading = (level) => {
370
+ const chain = editor.chain().focus();
371
+ if (level) chain.setHeading({ level }).run();
372
+ else chain.setParagraph().run();
373
+ setActivePopover(null);
374
+ };
375
+ const insertTable = () => {
376
+ editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
377
+ setActivePopover(null);
378
+ };
379
+ return /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
380
+ /* @__PURE__ */ jsx("div", { className: "overflow-visible border-b border-border bg-card", children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-11 flex-wrap items-center gap-1 px-4 py-1", children: [
381
+ /* @__PURE__ */ jsx(
382
+ IconButton,
383
+ {
384
+ icon: Undo2,
385
+ label: "Undo",
386
+ disabled: !editor.can().undo(),
387
+ onClick: () => editor.chain().focus().undo().run()
388
+ }
389
+ ),
390
+ /* @__PURE__ */ jsx(
391
+ IconButton,
392
+ {
393
+ icon: Redo2,
394
+ label: "Redo",
395
+ disabled: !editor.can().redo(),
396
+ onClick: () => editor.chain().focus().redo().run()
397
+ }
398
+ ),
399
+ /* @__PURE__ */ jsx(Divider, {}),
400
+ /* @__PURE__ */ jsxs(
401
+ "div",
402
+ {
403
+ ref: activePopover === "heading" ? popoverRef : void 0,
404
+ className: "relative",
405
+ children: [
406
+ /* @__PURE__ */ jsxs(
407
+ "button",
408
+ {
409
+ type: "button",
410
+ className: "flex h-8 min-w-28 items-center justify-between gap-3 rounded border border-input bg-card px-3 text-sm font-medium hover:bg-muted",
411
+ "aria-haspopup": "menu",
412
+ "aria-expanded": activePopover === "heading",
413
+ onClick: () => setActivePopover(
414
+ (current) => current === "heading" ? null : "heading"
415
+ ),
416
+ children: [
417
+ activeHeading.label,
418
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-4 text-muted-foreground" })
419
+ ]
420
+ }
421
+ ),
422
+ activePopover === "heading" && /* @__PURE__ */ jsx("div", { className: "te-popover absolute left-0 top-full z-30 mt-1 w-40 rounded-md p-1 shadow-lg", children: headingOptions.map((option) => /* @__PURE__ */ jsx(
423
+ "button",
424
+ {
425
+ type: "button",
426
+ role: "menuitem",
427
+ className: "flex w-full rounded px-2.5 py-2 text-left text-sm hover:bg-muted",
428
+ onClick: () => selectHeading(option.level),
429
+ children: option.label
430
+ },
431
+ option.value
432
+ )) })
433
+ ]
434
+ }
435
+ ),
436
+ /* @__PURE__ */ jsx(Divider, {}),
437
+ /* @__PURE__ */ jsx(
438
+ IconButton,
439
+ {
440
+ icon: Bold,
441
+ label: "Bold",
442
+ active: editor.isActive("bold"),
443
+ onClick: () => editor.chain().focus().toggleBold().run()
444
+ }
445
+ ),
446
+ /* @__PURE__ */ jsx(
447
+ IconButton,
448
+ {
449
+ icon: Italic,
450
+ label: "Italic",
451
+ active: editor.isActive("italic"),
452
+ onClick: () => editor.chain().focus().toggleItalic().run()
453
+ }
454
+ ),
455
+ /* @__PURE__ */ jsx(
456
+ IconButton,
457
+ {
458
+ icon: Underline,
459
+ label: "Underline",
460
+ active: editor.isActive("underline"),
461
+ onClick: () => editor.chain().focus().toggleMark("underline").run()
462
+ }
463
+ ),
464
+ /* @__PURE__ */ jsx(
465
+ IconButton,
466
+ {
467
+ icon: Code,
468
+ label: "Inline code",
469
+ active: editor.isActive("code"),
470
+ onClick: () => editor.chain().focus().toggleCode().run()
471
+ }
472
+ ),
473
+ /* @__PURE__ */ jsx(
474
+ IconButton,
475
+ {
476
+ icon: Link$1,
477
+ label: "Insert link",
478
+ active: editor.isActive("link"),
479
+ onClick: () => setActivePopover(
480
+ (current) => current === "link" ? null : "link"
481
+ )
482
+ }
483
+ ),
484
+ /* @__PURE__ */ jsx(Divider, {}),
485
+ /* @__PURE__ */ jsx(
486
+ IconButton,
487
+ {
488
+ icon: List,
489
+ label: "Bullet list",
490
+ active: editor.isActive("bulletList"),
491
+ onClick: () => editor.chain().focus().toggleBulletList().run()
492
+ }
493
+ ),
494
+ /* @__PURE__ */ jsx(
495
+ IconButton,
496
+ {
497
+ icon: ListOrdered,
498
+ label: "Numbered list",
499
+ active: editor.isActive("orderedList"),
500
+ onClick: () => editor.chain().focus().toggleOrderedList().run()
501
+ }
502
+ ),
503
+ /* @__PURE__ */ jsx(
504
+ IconButton,
505
+ {
506
+ icon: Quote,
507
+ label: "Blockquote",
508
+ active: editor.isActive("blockquote"),
509
+ onClick: () => editor.chain().focus().toggleBlockquote().run()
510
+ }
511
+ ),
512
+ /* @__PURE__ */ jsx(Divider, {}),
513
+ /* @__PURE__ */ jsxs(
514
+ "div",
515
+ {
516
+ ref: activePopover === "insert" ? popoverRef : void 0,
517
+ className: "relative",
518
+ children: [
519
+ /* @__PURE__ */ jsxs(
520
+ "button",
521
+ {
522
+ type: "button",
523
+ className: "flex h-8 items-center gap-1.5 rounded border border-primary px-2.5 text-sm font-semibold text-primary hover:bg-[var(--primary-soft)]",
524
+ "aria-haspopup": "menu",
525
+ "aria-expanded": activePopover === "insert",
526
+ onClick: () => setActivePopover(
527
+ (current) => current === "insert" ? null : "insert"
528
+ ),
529
+ children: [
530
+ /* @__PURE__ */ jsx(Plus, { className: "size-4" }),
531
+ "Insert"
532
+ ]
533
+ }
534
+ ),
535
+ activePopover === "insert" && /* @__PURE__ */ jsxs("div", { className: "te-popover absolute right-0 top-full z-30 mt-1 w-52 rounded-md p-1 shadow-lg", children: [
536
+ /* @__PURE__ */ jsxs(
537
+ "button",
538
+ {
539
+ type: "button",
540
+ className: "flex w-full items-center gap-2 rounded px-2.5 py-2 text-sm hover:bg-muted",
541
+ onClick: () => {
542
+ editor.chain().focus().setHorizontalRule().run();
543
+ setActivePopover(null);
544
+ },
545
+ children: [
546
+ /* @__PURE__ */ jsx(Minus, { className: "size-4" }),
547
+ "Horizontal rule"
548
+ ]
549
+ }
550
+ ),
551
+ /* @__PURE__ */ jsxs(
552
+ "button",
553
+ {
554
+ type: "button",
555
+ className: "flex w-full items-center gap-2 rounded px-2.5 py-2 text-sm hover:bg-muted",
556
+ onClick: () => {
557
+ editor.chain().focus().toggleCodeBlock().run();
558
+ setActivePopover(null);
559
+ },
560
+ children: [
561
+ /* @__PURE__ */ jsx(Code, { className: "size-4" }),
562
+ "Code block"
563
+ ]
564
+ }
565
+ ),
566
+ /* @__PURE__ */ jsxs(
567
+ "button",
568
+ {
569
+ type: "button",
570
+ className: "flex w-full items-center gap-2 rounded px-2.5 py-2 text-sm hover:bg-muted disabled:opacity-50",
571
+ onClick: insertTable,
572
+ disabled: !editor.can().chain().focus().insertTable({
573
+ rows: 3,
574
+ cols: 3,
575
+ withHeaderRow: true
576
+ }).run(),
577
+ children: [
578
+ /* @__PURE__ */ jsx(Table$1, { className: "size-4" }),
579
+ "Table"
580
+ ]
581
+ }
582
+ ),
583
+ /* @__PURE__ */ jsxs(
584
+ "button",
585
+ {
586
+ type: "button",
587
+ className: "flex w-full items-center gap-2 rounded px-2.5 py-2 text-sm hover:bg-muted disabled:opacity-50",
588
+ disabled: isUploadingImage,
589
+ onClick: () => {
590
+ onRequestImageUpload();
591
+ setActivePopover(null);
592
+ },
593
+ children: [
594
+ /* @__PURE__ */ jsx(Image$1, { className: "size-4" }),
595
+ isUploadingImage ? "Uploading image\u2026" : "Image"
596
+ ]
597
+ }
598
+ ),
599
+ editor.isActive("table") && /* @__PURE__ */ jsxs(Fragment, { children: [
600
+ /* @__PURE__ */ jsx("div", { className: "my-1 h-px bg-border" }),
601
+ /* @__PURE__ */ jsx(
602
+ "button",
603
+ {
604
+ type: "button",
605
+ className: "flex w-full rounded px-2.5 py-2 text-sm hover:bg-muted",
606
+ onClick: () => {
607
+ editor.chain().focus().addRowAfter().run();
608
+ setActivePopover(null);
609
+ },
610
+ children: "Add table row"
611
+ }
612
+ ),
613
+ /* @__PURE__ */ jsx(
614
+ "button",
615
+ {
616
+ type: "button",
617
+ className: "flex w-full rounded px-2.5 py-2 text-sm hover:bg-muted",
618
+ onClick: () => {
619
+ editor.chain().focus().addColumnAfter().run();
620
+ setActivePopover(null);
621
+ },
622
+ children: "Add table column"
623
+ }
624
+ ),
625
+ /* @__PURE__ */ jsxs(
626
+ "button",
627
+ {
628
+ type: "button",
629
+ className: "flex w-full items-center gap-2 rounded px-2.5 py-2 text-sm text-destructive hover:bg-destructive/10",
630
+ onClick: () => {
631
+ editor.chain().focus().deleteTable().run();
632
+ setActivePopover(null);
633
+ },
634
+ children: [
635
+ /* @__PURE__ */ jsx(Trash2, { className: "size-4" }),
636
+ "Remove table"
637
+ ]
638
+ }
639
+ )
640
+ ] })
641
+ ] })
642
+ ]
643
+ }
644
+ ),
645
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto hidden pl-8 text-xs tabular-nums text-muted-foreground xl:inline", children: [
646
+ wordCount.toLocaleString(),
647
+ " ",
648
+ wordCount === 1 ? "word" : "words",
649
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-2", children: "\xB7" }),
650
+ readTime,
651
+ " min"
652
+ ] })
653
+ ] }) }),
654
+ activePopover === "link" && /* @__PURE__ */ jsx(
655
+ link_editor_popover_default,
656
+ {
657
+ editor,
658
+ onClose: () => setActivePopover(null),
659
+ containerRef: popoverRef
660
+ }
661
+ )
662
+ ] });
663
+ };
664
+ var toolbar_default = Toolbar;
665
+ var buttonBase = "inline-flex h-8 w-8 items-center justify-center rounded text-foreground transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
666
+ var TipTapBubbleMenu = ({
667
+ editor,
668
+ className,
669
+ children,
670
+ pluginKey = "inlineBubbleMenu",
671
+ updateDelay,
672
+ resizeDelay,
673
+ shouldShow,
674
+ appendTo,
675
+ getReferencedVirtualElement,
676
+ options
677
+ }) => {
678
+ const menuRef = useRef(null);
679
+ useEffect(() => {
680
+ const element = menuRef.current;
681
+ if (!editor || editor.isDestroyed || !element) {
682
+ return;
683
+ }
684
+ const plugin = BubbleMenuPlugin({
685
+ editor,
686
+ element,
687
+ pluginKey,
688
+ updateDelay,
689
+ resizeDelay,
690
+ shouldShow: shouldShow ?? null,
691
+ appendTo,
692
+ getReferencedVirtualElement,
693
+ options
694
+ });
695
+ editor.registerPlugin(plugin);
696
+ return () => {
697
+ editor.unregisterPlugin(pluginKey);
698
+ };
699
+ }, [
700
+ editor,
701
+ pluginKey,
702
+ updateDelay,
703
+ resizeDelay,
704
+ shouldShow,
705
+ appendTo,
706
+ getReferencedVirtualElement,
707
+ options
708
+ ]);
709
+ return /* @__PURE__ */ jsx(
710
+ "div",
711
+ {
712
+ ref: menuRef,
713
+ className,
714
+ style: { visibility: "hidden" },
715
+ children
716
+ }
717
+ );
718
+ };
719
+ function InlineButton({
720
+ icon: Icon,
721
+ label,
722
+ active,
723
+ onClick
724
+ }) {
725
+ return /* @__PURE__ */ jsx(
726
+ "button",
727
+ {
728
+ type: "button",
729
+ "aria-label": label,
730
+ title: label,
731
+ className: `${buttonBase} ${active ? "bg-primary text-primary-foreground hover:bg-primary/90" : "bg-transparent hover:bg-muted"}`,
732
+ onClick,
733
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
734
+ }
735
+ );
736
+ }
737
+ var InlineBubbleMenu = ({
738
+ editor
739
+ }) => {
740
+ const [isLinkEditing, setIsLinkEditing] = useState(false);
741
+ const openLinkEditor = useCallback(() => {
742
+ setIsLinkEditing((prev) => !prev);
743
+ }, []);
744
+ useEffect(() => {
745
+ const handleSelectionUpdate = () => {
746
+ setIsLinkEditing(false);
747
+ };
748
+ editor.on("selectionUpdate", handleSelectionUpdate);
749
+ return () => {
750
+ editor.off("selectionUpdate", handleSelectionUpdate);
751
+ };
752
+ }, [editor]);
753
+ if (!editor) {
754
+ return null;
755
+ }
756
+ return /* @__PURE__ */ jsxs(
757
+ TipTapBubbleMenu,
758
+ {
759
+ editor,
760
+ className: "bubble-menu flex flex-col gap-1 rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-xl",
761
+ children: [
762
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
763
+ /* @__PURE__ */ jsx(
764
+ InlineButton,
765
+ {
766
+ icon: Bold,
767
+ label: "Bold",
768
+ active: editor.isActive("bold"),
769
+ onClick: () => editor.chain().focus().toggleBold().run()
770
+ }
771
+ ),
772
+ /* @__PURE__ */ jsx(
773
+ InlineButton,
774
+ {
775
+ icon: Italic,
776
+ label: "Italic",
777
+ active: editor.isActive("italic"),
778
+ onClick: () => editor.chain().focus().toggleItalic().run()
779
+ }
780
+ ),
781
+ /* @__PURE__ */ jsx(
782
+ InlineButton,
783
+ {
784
+ icon: Underline,
785
+ label: "Underline",
786
+ active: editor.isActive("underline"),
787
+ onClick: () => editor.chain().focus().toggleMark("underline").run()
788
+ }
789
+ ),
790
+ /* @__PURE__ */ jsx(
791
+ InlineButton,
792
+ {
793
+ icon: Strikethrough,
794
+ label: "Strikethrough",
795
+ active: editor.isActive("strike"),
796
+ onClick: () => editor.chain().focus().toggleStrike().run()
797
+ }
798
+ ),
799
+ /* @__PURE__ */ jsx(
800
+ InlineButton,
801
+ {
802
+ icon: Code,
803
+ label: "Inline code",
804
+ active: editor.isActive("code"),
805
+ onClick: () => editor.chain().focus().toggleCode().run()
806
+ }
807
+ ),
808
+ /* @__PURE__ */ jsx(
809
+ InlineButton,
810
+ {
811
+ icon: Link$1,
812
+ label: "Insert link",
813
+ active: editor.isActive("link"),
814
+ onClick: openLinkEditor
815
+ }
816
+ )
817
+ ] }),
818
+ isLinkEditing && /* @__PURE__ */ jsx(
819
+ link_editor_popover_default,
820
+ {
821
+ editor,
822
+ onClose: () => setIsLinkEditing(false),
823
+ variant: "inline"
824
+ }
825
+ )
826
+ ]
827
+ }
828
+ );
829
+ };
830
+ var bubble_menu_default = InlineBubbleMenu;
831
+ function cn(...inputs) {
832
+ return twMerge(clsx(inputs));
833
+ }
834
+ var ImagePickerContext = createContext(null);
835
+ function ImagePickerContextProvider({
836
+ pickImage,
837
+ children
838
+ }) {
839
+ return /* @__PURE__ */ jsx(ImagePickerContext.Provider, { value: pickImage, children });
840
+ }
841
+ function useImagePicker() {
842
+ return useContext(ImagePickerContext);
843
+ }
844
+ var Editor = Object.assign(
845
+ ({
846
+ initialContent,
847
+ onChange,
848
+ placeholder,
849
+ children,
850
+ showToolbar = true,
851
+ editable = true,
852
+ refresh,
853
+ url,
854
+ autoFocus,
855
+ imageSizeLimit,
856
+ onError,
857
+ className,
858
+ editorClassName,
859
+ contentClassName,
860
+ beforeContent
861
+ }) => {
862
+ const [isUploading, setIsUploading] = useState(false);
863
+ const fileInputRef = useRef(null);
864
+ const pickImage = useImagePicker();
865
+ const hasInteractedRef = useRef(false);
866
+ const lastSerializedContentRef = useRef(
867
+ JSON.stringify(initialContent ?? empty_doc_default)
868
+ );
869
+ const refreshRef = useRef(refresh);
870
+ const extensions = useMemo(
871
+ () => createExtensions({ placeholder }),
872
+ [placeholder]
873
+ );
874
+ const changeTimeoutRef = useRef(
875
+ null
876
+ );
877
+ const editor = useEditor(
878
+ {
879
+ immediatelyRender: false,
880
+ extensions,
881
+ content: initialContent ?? empty_doc_default,
882
+ autofocus: autoFocus ? "end" : false,
883
+ editable,
884
+ editorProps: {
885
+ attributes: {
886
+ class: `tiptap-content ${editorClassName ?? ""}`,
887
+ "data-placeholder": placeholder ?? ""
888
+ }
889
+ },
890
+ onUpdate: ({ editor: instance }) => {
891
+ if (!hasInteractedRef.current) {
892
+ return;
893
+ }
894
+ if (changeTimeoutRef.current) {
895
+ clearTimeout(changeTimeoutRef.current);
896
+ }
897
+ changeTimeoutRef.current = setTimeout(() => {
898
+ onChange(instance.getJSON());
899
+ changeTimeoutRef.current = null;
900
+ }, 1e3);
901
+ },
902
+ onSelectionUpdate: ({ editor: instance }) => {
903
+ hasInteractedRef.current = true;
904
+ }
905
+ },
906
+ [extensions]
907
+ );
908
+ useEffect(() => {
909
+ return () => {
910
+ if (changeTimeoutRef.current) {
911
+ clearTimeout(changeTimeoutRef.current);
912
+ }
913
+ };
914
+ }, []);
915
+ useEffect(() => {
916
+ if (!editor) {
917
+ return;
918
+ }
919
+ editor.setEditable(editable);
920
+ }, [editor, editable]);
921
+ useEffect(() => {
922
+ if (!editor) {
923
+ return;
924
+ }
925
+ const nextContent = initialContent ?? empty_doc_default;
926
+ const nextSerialized = JSON.stringify(nextContent);
927
+ const refreshChanged = refreshRef.current !== refresh;
928
+ if (refreshChanged) {
929
+ refreshRef.current = refresh;
930
+ hasInteractedRef.current = false;
931
+ lastSerializedContentRef.current = nextSerialized;
932
+ editor.commands.setContent(nextContent, {
933
+ emitUpdate: false
934
+ });
935
+ return;
936
+ }
937
+ if (!hasInteractedRef.current) {
938
+ if (lastSerializedContentRef.current !== nextSerialized) {
939
+ lastSerializedContentRef.current = nextSerialized;
940
+ editor.commands.setContent(nextContent, {
941
+ emitUpdate: false
942
+ });
943
+ }
944
+ return;
945
+ }
946
+ lastSerializedContentRef.current = nextSerialized;
947
+ }, [editor, initialContent, refresh]);
948
+ const handleImageUpload = useCallback(
949
+ async (file) => {
950
+ const sizeLimit = imageSizeLimit ?? 2097152;
951
+ if (file.size > sizeLimit) {
952
+ onError?.("File is larger than the allowed size");
953
+ return;
954
+ }
955
+ try {
956
+ setIsUploading(true);
957
+ const dataUrl = await new Promise(
958
+ (resolve, reject) => {
959
+ const reader = new FileReader();
960
+ reader.onload = () => resolve(reader.result);
961
+ reader.onerror = reject;
962
+ reader.readAsDataURL(file);
963
+ }
964
+ );
965
+ editor?.chain().focus().setImage({
966
+ src: dataUrl,
967
+ alt: file.name,
968
+ title: file.name
969
+ }).run();
970
+ } catch (error) {
971
+ console.error("Image upload failed", error);
972
+ } finally {
973
+ setIsUploading(false);
974
+ }
975
+ },
976
+ [editor]
977
+ );
978
+ const handleSelectFile = useCallback(
979
+ (event) => {
980
+ const files = event.target.files;
981
+ if (!files || files.length === 0) {
982
+ return;
983
+ }
984
+ const file = files[0];
985
+ void handleImageUpload(file);
986
+ event.target.value = "";
987
+ },
988
+ [handleImageUpload]
989
+ );
990
+ const insertPickedImage = useCallback(
991
+ (image) => {
992
+ editor?.chain().focus().setImage({
993
+ src: image.src,
994
+ alt: image.alt,
995
+ title: image.alt
996
+ }).run();
997
+ },
998
+ [editor]
999
+ );
1000
+ const openFileDialog = useCallback(() => {
1001
+ if (pickImage) {
1002
+ void pickImage().then((image) => {
1003
+ if (image) insertPickedImage(image);
1004
+ });
1005
+ return;
1006
+ }
1007
+ fileInputRef.current?.click();
1008
+ }, [insertPickedImage, pickImage]);
1009
+ if (typeof window === "undefined") {
1010
+ return /* @__PURE__ */ jsx(Fragment, {});
1011
+ }
1012
+ return /* @__PURE__ */ jsxs(
1013
+ "div",
1014
+ {
1015
+ className: cn(
1016
+ "tiptap-editor flex flex-col gap-4 border",
1017
+ className
1018
+ ),
1019
+ children: [
1020
+ editable && showToolbar && /* @__PURE__ */ jsx(
1021
+ "div",
1022
+ {
1023
+ className: "sticky top-0 bg-background",
1024
+ style: {
1025
+ zIndex: 1
1026
+ },
1027
+ children: /* @__PURE__ */ jsx(
1028
+ toolbar_default,
1029
+ {
1030
+ editor,
1031
+ onRequestImageUpload: openFileDialog,
1032
+ isUploadingImage: isUploading
1033
+ }
1034
+ )
1035
+ }
1036
+ ),
1037
+ /* @__PURE__ */ jsx("div", { className: "flex w-full justify-center", children: /* @__PURE__ */ jsxs(
1038
+ "div",
1039
+ {
1040
+ className: cn(
1041
+ "flex w-full max-w-3xl flex-col",
1042
+ contentClassName
1043
+ ),
1044
+ children: [
1045
+ beforeContent,
1046
+ /* @__PURE__ */ jsx(
1047
+ EditorContent,
1048
+ {
1049
+ editor,
1050
+ className: `${editorClassName ?? ""} ${editorClassName?.includes("min-h-") ? "" : "min-h-[200px]"}`
1051
+ }
1052
+ ),
1053
+ pickImage ? null : /* @__PURE__ */ jsx(
1054
+ "input",
1055
+ {
1056
+ ref: fileInputRef,
1057
+ type: "file",
1058
+ accept: "image/*",
1059
+ className: "hidden",
1060
+ onChange: handleSelectFile
1061
+ }
1062
+ ),
1063
+ children,
1064
+ editor && /* @__PURE__ */ jsx(bubble_menu_default, { editor })
1065
+ ]
1066
+ }
1067
+ ) })
1068
+ ]
1069
+ }
1070
+ );
1071
+ },
1072
+ {
1073
+ getPlainText: (doc) => generateText(doc || empty_doc_default, createExtensions()),
1074
+ emptyDoc: empty_doc_default
1075
+ }
1076
+ );
1077
+ var editor_default = Editor;
1078
+
1079
+ // src/extract-headings.ts
1080
+ function extractHeadings(json2) {
1081
+ const headings = [];
1082
+ const collectHeadings = (node) => {
1083
+ if (!node) {
1084
+ return;
1085
+ }
1086
+ if (node.type === "heading") {
1087
+ const level = typeof node.attrs?.level === "number" ? node.attrs.level : 1;
1088
+ const collectText = (current) => {
1089
+ if (!current) {
1090
+ return "";
1091
+ }
1092
+ if (typeof current.text === "string") {
1093
+ return current.text;
1094
+ }
1095
+ if (!current.content) {
1096
+ return "";
1097
+ }
1098
+ return current.content.map(collectText).join("");
1099
+ };
1100
+ const textContent = collectText(node);
1101
+ headings.push({
1102
+ level,
1103
+ text: textContent,
1104
+ id: createId(textContent)
1105
+ });
1106
+ }
1107
+ if (Array.isArray(node.content)) {
1108
+ node.content.forEach((child) => {
1109
+ collectHeadings(child);
1110
+ });
1111
+ }
1112
+ };
1113
+ collectHeadings(json2 || void 0);
1114
+ return headings;
1115
+ }
1116
+
1117
+ // src/extract-text-from-node.ts
1118
+ function extractTextFromNode(node) {
1119
+ if (!node) {
1120
+ return "";
1121
+ }
1122
+ if (typeof node.textContent === "string") {
1123
+ return node.textContent;
1124
+ }
1125
+ if (node.content && node.content.content && Array.isArray(node.content.content)) {
1126
+ return node.content.content.map((child) => {
1127
+ if (child.text) {
1128
+ return child.text;
1129
+ }
1130
+ return extractTextFromNode(child);
1131
+ }).join("");
1132
+ }
1133
+ if (Array.isArray(node.content)) {
1134
+ return node.content.map((child) => {
1135
+ if (child.text) {
1136
+ return child.text;
1137
+ }
1138
+ return extractTextFromNode(child);
1139
+ }).join("");
1140
+ }
1141
+ return "";
1142
+ }
1143
+ function textFromChildren(node) {
1144
+ if (node == null || typeof node === "boolean") return "";
1145
+ if (typeof node === "string" || typeof node === "number") {
1146
+ return String(node);
1147
+ }
1148
+ if (Array.isArray(node)) return node.map(textFromChildren).join("");
1149
+ if (React5.isValidElement(node)) {
1150
+ return textFromChildren(node.props.children);
1151
+ }
1152
+ return "";
1153
+ }
1154
+ async function writeClipboard(text) {
1155
+ if (navigator.clipboard?.writeText) {
1156
+ await navigator.clipboard.writeText(text);
1157
+ return;
1158
+ }
1159
+ const area = document.createElement("textarea");
1160
+ area.value = text;
1161
+ area.setAttribute("readonly", "");
1162
+ area.style.position = "fixed";
1163
+ area.style.left = "-9999px";
1164
+ document.body.appendChild(area);
1165
+ area.select();
1166
+ document.execCommand("copy");
1167
+ area.remove();
1168
+ }
1169
+ function RenderedCodeBlock({
1170
+ node,
1171
+ children,
1172
+ language
1173
+ }) {
1174
+ const [copied, setCopied] = useState(false);
1175
+ const source = extractTextFromNode(node) || textFromChildren(children);
1176
+ useEffect(() => {
1177
+ if (!copied) return;
1178
+ const timer = window.setTimeout(() => setCopied(false), 1500);
1179
+ return () => window.clearTimeout(timer);
1180
+ }, [copied]);
1181
+ async function handleCopy() {
1182
+ if (!source) return;
1183
+ try {
1184
+ await writeClipboard(source);
1185
+ setCopied(true);
1186
+ } catch {
1187
+ setCopied(false);
1188
+ }
1189
+ }
1190
+ return /* @__PURE__ */ jsxs("div", { className: "te-code-block", children: [
1191
+ /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { className: language ? `language-${language}` : void 0, children }) }),
1192
+ /* @__PURE__ */ jsx(
1193
+ "button",
1194
+ {
1195
+ type: "button",
1196
+ className: "te-code-copy",
1197
+ "aria-label": copied ? "Copied" : "Copy code",
1198
+ title: copied ? "Copied" : "Copy",
1199
+ onClick: () => {
1200
+ void handleCopy();
1201
+ },
1202
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "te-code-copy__icon", "aria-hidden": true }) : /* @__PURE__ */ jsx(Copy, { className: "te-code-copy__icon", "aria-hidden": true })
1203
+ }
1204
+ )
1205
+ ] });
1206
+ }
1207
+ var typographyClasses = (typography) => [
1208
+ typography?.fontFamily,
1209
+ typography?.fontSize,
1210
+ typography?.fontWeight,
1211
+ typography?.lineHeight,
1212
+ typography?.letterSpacing,
1213
+ typography?.textTransform,
1214
+ typography?.textDecoration,
1215
+ typography?.textOverflow
1216
+ ];
1217
+ function removeEmptyTextNodes(node) {
1218
+ if (!node || typeof node !== "object") {
1219
+ return node;
1220
+ }
1221
+ if (node.type === "text" && node.text === "") {
1222
+ return void 0;
1223
+ }
1224
+ if (!Array.isArray(node.content)) {
1225
+ return node;
1226
+ }
1227
+ const content = node.content.map(removeEmptyTextNodes).filter((child) => child !== void 0);
1228
+ return {
1229
+ ...node,
1230
+ content
1231
+ };
1232
+ }
1233
+ function TextRenderer({ json: json2, className, theme }) {
1234
+ const extensions = createExtensions();
1235
+ const content = removeEmptyTextNodes(
1236
+ json2 ?? empty_doc_default
1237
+ );
1238
+ const rendered = renderToReactElement({
1239
+ extensions,
1240
+ content,
1241
+ options: {
1242
+ nodeMapping: {
1243
+ paragraph: ({ children }) => {
1244
+ const classes = cn(
1245
+ "text-base",
1246
+ ...typographyClasses(theme?.typography?.text1)
1247
+ );
1248
+ return /* @__PURE__ */ jsx("p", { className: classes, children });
1249
+ },
1250
+ hardBreak: () => /* @__PURE__ */ jsx("br", {}),
1251
+ heading: ({ node, children }) => {
1252
+ const level = node?.attrs?.level ?? 1;
1253
+ const textContent = extractTextFromNode(node);
1254
+ const id = createId(textContent);
1255
+ if (!theme) {
1256
+ const Tag = `h${level}`;
1257
+ return /* @__PURE__ */ jsx(Tag, { id, children });
1258
+ }
1259
+ if (level === 1) {
1260
+ const classes2 = cn(
1261
+ "scroll-m-20 text-4xl font-bold tracking-tight",
1262
+ ...typographyClasses(theme?.typography?.header1)
1263
+ );
1264
+ return /* @__PURE__ */ jsx("h1", { id, className: classes2, children });
1265
+ }
1266
+ if (level === 2) {
1267
+ const classes2 = cn(
1268
+ "scroll-m-20 text-3xl font-bold tracking-tight",
1269
+ ...typographyClasses(theme?.typography?.header2)
1270
+ );
1271
+ return /* @__PURE__ */ jsx("h2", { id, className: classes2, children });
1272
+ }
1273
+ const classes = cn(
1274
+ "scroll-m-20 text-2xl font-semibold tracking-normal",
1275
+ ...typographyClasses(theme?.typography?.header3)
1276
+ );
1277
+ return /* @__PURE__ */ jsx("h3", { id, className: classes, children });
1278
+ },
1279
+ codeBlock: ({ node, children }) => /* @__PURE__ */ jsx(
1280
+ RenderedCodeBlock,
1281
+ {
1282
+ node,
1283
+ language: node?.attrs?.language,
1284
+ children
1285
+ }
1286
+ ),
1287
+ codeMirror: ({ node, children }) => /* @__PURE__ */ jsx(RenderedCodeBlock, { node, children })
1288
+ },
1289
+ markMapping: {
1290
+ link: ({ mark, children }) => {
1291
+ const href = mark?.attrs?.href;
1292
+ const linkStyle = theme?.interactives?.link;
1293
+ const classes = cn(
1294
+ "font-medium cursor-pointer",
1295
+ ...typographyClasses(theme?.typography?.link),
1296
+ linkStyle?.padding?.x,
1297
+ linkStyle?.padding?.y,
1298
+ linkStyle?.border?.width,
1299
+ linkStyle?.border?.radius,
1300
+ linkStyle?.border?.style,
1301
+ linkStyle?.custom
1302
+ );
1303
+ return /* @__PURE__ */ jsx(
1304
+ "a",
1305
+ {
1306
+ href,
1307
+ target: "_blank",
1308
+ rel: "noopener noreferrer",
1309
+ className: classes,
1310
+ style: {
1311
+ textShadow: linkStyle?.textShadow || void 0
1312
+ },
1313
+ children
1314
+ }
1315
+ );
1316
+ },
1317
+ highlight: ({ children }) => /* @__PURE__ */ jsx("mark", { className: "bg-accent text-accent-foreground", children })
1318
+ }
1319
+ }
1320
+ });
1321
+ const combinedClassName = ["tiptap-renderer", className].filter(Boolean).join(" ");
1322
+ return /* @__PURE__ */ jsx("div", { className: "text-editor flex flex-col gap-4", children: /* @__PURE__ */ jsx("div", { className: combinedClassName, children: rendered }) });
1323
+ }
1324
+
1325
+ // src/normalize-text-editor-content.ts
1326
+ function createParagraphNode(text) {
1327
+ const lines = text.split("\n");
1328
+ const content = [];
1329
+ lines.forEach((line, index) => {
1330
+ if (line) {
1331
+ content.push({
1332
+ type: "text",
1333
+ text: line
1334
+ });
1335
+ }
1336
+ if (index < lines.length - 1) {
1337
+ content.push({
1338
+ type: "hardBreak"
1339
+ });
1340
+ }
1341
+ });
1342
+ return {
1343
+ type: "paragraph",
1344
+ ...content.length ? { content } : {}
1345
+ };
1346
+ }
1347
+ function normalizeTextEditorContent(json2) {
1348
+ if (json2 && typeof json2 === "object" && "type" in json2 && json2.type === "doc") {
1349
+ return json2;
1350
+ }
1351
+ const text = typeof json2 === "string" ? json2.replace(/\r\n?/g, "\n") : "";
1352
+ return {
1353
+ type: "doc",
1354
+ content: text ? text.split(/\n{2,}/).map((paragraph) => createParagraphNode(paragraph)) : []
1355
+ };
1356
+ }
1357
+
1358
+ // src/extract-text-from-text-editor-content.ts
1359
+ function extractTextFromTextEditorContent(content) {
1360
+ if (typeof content === "string") {
1361
+ return content;
1362
+ }
1363
+ if (!content || typeof content !== "object") {
1364
+ return "";
1365
+ }
1366
+ const record = content;
1367
+ const ownText = typeof record.text === "string" ? record.text : "";
1368
+ const children = Array.isArray(record.content) ? record.content.map(extractTextFromTextEditorContent).join("") : "";
1369
+ return `${ownText}${children}`;
1370
+ }
1371
+
1372
+ // src/truncate-text-editor-content.ts
1373
+ function appendEllipsisToLastTextNode(nodes) {
1374
+ for (let i = nodes.length - 1; i >= 0; i--) {
1375
+ const node = nodes[i];
1376
+ if (!node) {
1377
+ continue;
1378
+ }
1379
+ if (node.type === "text" && typeof node.text === "string") {
1380
+ if (!node.text.endsWith("...")) {
1381
+ nodes[i] = {
1382
+ ...node,
1383
+ text: `${node.text.trimEnd()}...`
1384
+ };
1385
+ }
1386
+ return nodes;
1387
+ }
1388
+ if (Array.isArray(node.content) && node.content.length) {
1389
+ nodes[i] = {
1390
+ ...node,
1391
+ content: appendEllipsisToLastTextNode([...node.content])
1392
+ };
1393
+ return nodes;
1394
+ }
1395
+ }
1396
+ return nodes;
1397
+ }
1398
+ function truncateNodes(nodes, remaining) {
1399
+ const truncatedNodes = [];
1400
+ let truncated = false;
1401
+ for (let index = 0; index < nodes.length; index++) {
1402
+ const node = nodes[index];
1403
+ if (!node) {
1404
+ continue;
1405
+ }
1406
+ if (remaining <= 0) {
1407
+ truncated = true;
1408
+ break;
1409
+ }
1410
+ if (node.type === "text" && typeof node.text === "string") {
1411
+ if (node.text.length <= remaining) {
1412
+ truncatedNodes.push(node);
1413
+ remaining -= node.text.length;
1414
+ } else {
1415
+ truncatedNodes.push({
1416
+ ...node,
1417
+ text: node.text.slice(0, remaining).trimEnd()
1418
+ });
1419
+ remaining = 0;
1420
+ truncated = true;
1421
+ break;
1422
+ }
1423
+ continue;
1424
+ }
1425
+ if (node.type === "hardBreak") {
1426
+ truncatedNodes.push(node);
1427
+ remaining -= 1;
1428
+ continue;
1429
+ }
1430
+ if (Array.isArray(node.content)) {
1431
+ const result = truncateNodes(node.content, remaining);
1432
+ remaining = result.remaining;
1433
+ if (result.nodes.length > 0) {
1434
+ truncatedNodes.push({
1435
+ ...node,
1436
+ content: result.nodes
1437
+ });
1438
+ }
1439
+ if (result.truncated) {
1440
+ truncated = true;
1441
+ break;
1442
+ }
1443
+ continue;
1444
+ }
1445
+ truncatedNodes.push(node);
1446
+ }
1447
+ if (truncated) {
1448
+ return {
1449
+ nodes: appendEllipsisToLastTextNode(truncatedNodes),
1450
+ remaining,
1451
+ truncated
1452
+ };
1453
+ }
1454
+ return {
1455
+ nodes: truncatedNodes,
1456
+ remaining,
1457
+ truncated
1458
+ };
1459
+ }
1460
+ function truncateTextEditorContent(json2, characterLimit) {
1461
+ if (!characterLimit || characterLimit <= 0) {
1462
+ return json2;
1463
+ }
1464
+ const content = Array.isArray(json2?.content) ? json2.content : [];
1465
+ const result = truncateNodes(content, characterLimit);
1466
+ return {
1467
+ ...json2,
1468
+ content: result.nodes
1469
+ };
1470
+ }
1471
+
1472
+ export { editor_default as Editor, ImagePickerContextProvider, TextRenderer, createExtensions, createId, empty_doc_default as emptyDoc, extractHeadings, extractTextFromNode, extractTextFromTextEditorContent, normalizeTextEditorContent, truncateTextEditorContent, useImagePicker };
1473
+ //# sourceMappingURL=index.js.map
1474
+ //# sourceMappingURL=index.js.map