@godxjp/block-editor 31.27.0

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,1817 @@
1
+ "use client";
2
+
3
+ // src/block-editor.tsx
4
+ import * as React6 from "react";
5
+ import { EditorContent, useEditor } from "@tiptap/react";
6
+ import { FileText } from "lucide-react";
7
+ import { normalize, parse as parse2, serialize } from "@godxjp/markdown/codec";
8
+ import { Text as Text2 } from "@godxjp/ui/general";
9
+ import { useTranslation } from "@godxjp/ui/i18n";
10
+
11
+ // src/blocks.ts
12
+ import { Extension } from "@tiptap/core";
13
+ import { NodeSelection, TextSelection } from "@tiptap/pm/state";
14
+ function blockStartAt(editor, pos) {
15
+ const { doc } = editor.state;
16
+ if (pos < 0 || pos > doc.content.size) return null;
17
+ const $pos = doc.resolve(pos);
18
+ if ($pos.depth === 0) {
19
+ return pos < doc.content.size ? pos : null;
20
+ }
21
+ return $pos.before(1);
22
+ }
23
+ function selectionBlockStart(editor) {
24
+ return blockStartAt(editor, editor.state.selection.from);
25
+ }
26
+ function nodeAt(editor, start) {
27
+ const node = editor.state.doc.nodeAt(start);
28
+ return node ? { node, end: start + node.nodeSize } : null;
29
+ }
30
+ function moveBlock(editor, start, direction) {
31
+ const current = nodeAt(editor, start);
32
+ if (!current) return null;
33
+ const { doc } = editor.state;
34
+ const $start = doc.resolve(start);
35
+ const index = $start.index(0);
36
+ const sibling = direction === -1 ? index > 0 ? doc.child(index - 1) : null : index < doc.childCount - 1 ? doc.child(index + 1) : null;
37
+ if (!sibling) return null;
38
+ const tr = editor.state.tr.delete(start, current.end);
39
+ const target = direction === -1 ? start - sibling.nodeSize : start + sibling.nodeSize;
40
+ tr.insert(target, current.node);
41
+ tr.setSelection(TextSelection.near(tr.doc.resolve(target + 1)));
42
+ editor.view.dispatch(tr.scrollIntoView());
43
+ return target;
44
+ }
45
+ function duplicateBlock(editor, start) {
46
+ const current = nodeAt(editor, start);
47
+ if (!current) return;
48
+ const tr = editor.state.tr.insert(current.end, current.node.copy(current.node.content));
49
+ tr.setSelection(TextSelection.near(tr.doc.resolve(current.end + 1)));
50
+ editor.view.dispatch(tr.scrollIntoView());
51
+ }
52
+ function deleteBlock(editor, start) {
53
+ const current = nodeAt(editor, start);
54
+ if (!current) return;
55
+ const tr = editor.state.tr.delete(start, current.end);
56
+ if (tr.doc.childCount === 0) tr.insert(0, editor.schema.nodes.paragraph.create());
57
+ tr.setSelection(TextSelection.near(tr.doc.resolve(Math.min(start, tr.doc.content.size))));
58
+ editor.view.dispatch(tr.scrollIntoView());
59
+ }
60
+ function focusBlock(editor, start) {
61
+ const current = nodeAt(editor, start);
62
+ if (!current) return;
63
+ const tr = editor.state.tr.setSelection(TextSelection.near(editor.state.doc.resolve(start + 1)));
64
+ editor.view.dispatch(tr);
65
+ editor.view.focus();
66
+ }
67
+ function insertParagraphAfter(editor, start) {
68
+ const current = nodeAt(editor, start);
69
+ if (!current) return null;
70
+ const tr = editor.state.tr.insert(current.end, editor.schema.nodes.paragraph.create());
71
+ tr.setSelection(TextSelection.create(tr.doc, current.end + 1));
72
+ editor.view.dispatch(tr.scrollIntoView());
73
+ editor.view.focus();
74
+ return current.end;
75
+ }
76
+ function startBlockDrag(editor, start, event) {
77
+ const { view } = editor;
78
+ const selection = NodeSelection.create(view.state.doc, start);
79
+ view.dispatch(view.state.tr.setSelection(selection));
80
+ view.dragging = { slice: selection.content(), move: true };
81
+ if (event.dataTransfer) {
82
+ event.dataTransfer.effectAllowed = "copyMove";
83
+ event.dataTransfer.setData("text/plain", "");
84
+ const dom = view.nodeDOM(start);
85
+ if (dom instanceof HTMLElement) event.dataTransfer.setDragImage(dom, 0, 0);
86
+ }
87
+ }
88
+ var BlockKeys = Extension.create({
89
+ name: "blockKeys",
90
+ addOptions() {
91
+ return { openMenu: () => void 0 };
92
+ },
93
+ addKeyboardShortcuts() {
94
+ const move = (direction) => () => {
95
+ const start = selectionBlockStart(this.editor);
96
+ return start != null && moveBlock(this.editor, start, direction) != null;
97
+ };
98
+ return {
99
+ "Alt-Shift-ArrowUp": move(-1),
100
+ "Alt-Shift-ArrowDown": move(1),
101
+ "Mod-/": () => {
102
+ const start = selectionBlockStart(this.editor);
103
+ if (start == null) return false;
104
+ this.options.openMenu(start);
105
+ return true;
106
+ }
107
+ };
108
+ }
109
+ });
110
+
111
+ // src/commands.tsx
112
+ import { Extension as Extension2 } from "@tiptap/core";
113
+ import { PluginKey } from "@tiptap/pm/state";
114
+ import { Suggestion } from "@tiptap/suggestion";
115
+ import {
116
+ ChevronRight,
117
+ Code,
118
+ Columns2,
119
+ Heading1,
120
+ Heading2,
121
+ Heading3,
122
+ Image as ImageIcon,
123
+ Info,
124
+ List,
125
+ ListChecks,
126
+ ListOrdered,
127
+ Minus,
128
+ Pilcrow,
129
+ Quote,
130
+ Table as TableIcon
131
+ } from "lucide-react";
132
+ import { jsx } from "react/jsx-runtime";
133
+ var TURN_INTO = [
134
+ "text",
135
+ "heading1",
136
+ "heading2",
137
+ "heading3",
138
+ "bulletList",
139
+ "numberedList",
140
+ "taskList",
141
+ "quote",
142
+ "callout",
143
+ "code"
144
+ ];
145
+ function builtinCommands(labels, insertImage) {
146
+ const commands = [
147
+ {
148
+ key: "text",
149
+ label: labels.text,
150
+ icon: /* @__PURE__ */ jsx(Pilcrow, {}),
151
+ keywords: ["paragraph", "p"],
152
+ run: (e) => e.chain().focus().setParagraph().run()
153
+ },
154
+ {
155
+ key: "heading1",
156
+ label: labels.heading1,
157
+ icon: /* @__PURE__ */ jsx(Heading1, {}),
158
+ keywords: ["h1", "title"],
159
+ run: (e) => e.chain().focus().setHeading({ level: 1 }).run()
160
+ },
161
+ {
162
+ key: "heading2",
163
+ label: labels.heading2,
164
+ icon: /* @__PURE__ */ jsx(Heading2, {}),
165
+ keywords: ["h2"],
166
+ run: (e) => e.chain().focus().setHeading({ level: 2 }).run()
167
+ },
168
+ {
169
+ key: "heading3",
170
+ label: labels.heading3,
171
+ icon: /* @__PURE__ */ jsx(Heading3, {}),
172
+ keywords: ["h3"],
173
+ run: (e) => e.chain().focus().setHeading({ level: 3 }).run()
174
+ },
175
+ {
176
+ key: "bulletList",
177
+ label: labels.bulletList,
178
+ icon: /* @__PURE__ */ jsx(List, {}),
179
+ keywords: ["ul", "bullet", "list"],
180
+ run: (e) => e.chain().focus().toggleBulletList().run()
181
+ },
182
+ {
183
+ key: "numberedList",
184
+ label: labels.numberedList,
185
+ icon: /* @__PURE__ */ jsx(ListOrdered, {}),
186
+ keywords: ["ol", "number", "ordered"],
187
+ run: (e) => e.chain().focus().toggleOrderedList().run()
188
+ },
189
+ {
190
+ key: "taskList",
191
+ label: labels.taskList,
192
+ icon: /* @__PURE__ */ jsx(ListChecks, {}),
193
+ keywords: ["todo", "task", "checkbox"],
194
+ run: (e) => e.chain().focus().toggleTaskList().run()
195
+ },
196
+ {
197
+ key: "quote",
198
+ label: labels.quote,
199
+ icon: /* @__PURE__ */ jsx(Quote, {}),
200
+ keywords: ["blockquote", ">"],
201
+ run: (e) => e.chain().focus().toggleBlockquote().run()
202
+ },
203
+ {
204
+ key: "callout",
205
+ label: labels.callout,
206
+ icon: /* @__PURE__ */ jsx(Info, {}),
207
+ keywords: ["note", "alert", "tip", "warning"],
208
+ run: (e) => e.chain().focus().wrapIn("callout", { kind: "note" }).run()
209
+ },
210
+ {
211
+ key: "code",
212
+ label: labels.code,
213
+ icon: /* @__PURE__ */ jsx(Code, {}),
214
+ keywords: ["codeblock", "```", "pre"],
215
+ run: (e) => e.chain().focus().setCodeBlock().run()
216
+ },
217
+ {
218
+ key: "divider",
219
+ label: labels.divider,
220
+ icon: /* @__PURE__ */ jsx(Minus, {}),
221
+ keywords: ["hr", "rule", "---"],
222
+ run: (e) => e.chain().focus().setHorizontalRule().run()
223
+ },
224
+ {
225
+ key: "table",
226
+ label: labels.table,
227
+ icon: /* @__PURE__ */ jsx(TableIcon, {}),
228
+ keywords: ["grid"],
229
+ run: (e) => e.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
230
+ },
231
+ {
232
+ key: "toggle",
233
+ label: labels.toggle,
234
+ icon: /* @__PURE__ */ jsx(ChevronRight, {}),
235
+ keywords: ["details", "collapse", "fold"],
236
+ run: (e) => e.chain().focus().setDetails().run()
237
+ },
238
+ {
239
+ key: "columns",
240
+ label: labels.columns,
241
+ icon: /* @__PURE__ */ jsx(Columns2, {}),
242
+ keywords: ["layout", "column", "split"],
243
+ run: (e) => e.chain().focus().insertContent({
244
+ type: "columns",
245
+ content: [
246
+ { type: "column", content: [{ type: "paragraph" }] },
247
+ { type: "column", content: [{ type: "paragraph" }] }
248
+ ]
249
+ }).run()
250
+ }
251
+ ];
252
+ if (insertImage) {
253
+ commands.splice(11, 0, {
254
+ key: "image",
255
+ label: labels.image,
256
+ icon: /* @__PURE__ */ jsx(ImageIcon, {}),
257
+ keywords: ["img", "picture", "photo", "media"],
258
+ run: insertImage
259
+ });
260
+ }
261
+ return commands;
262
+ }
263
+ var fold = (value) => value.normalize("NFKC").toLowerCase();
264
+ function filterCommands(commands, query) {
265
+ const q = fold(query.trim());
266
+ if (!q) return [...commands];
267
+ const scored = commands.map((command) => {
268
+ const terms = [command.label, command.key, ...command.keywords ?? []].map(fold);
269
+ const score = terms.some((t) => t.startsWith(q)) ? 0 : terms.some((t) => t.includes(q)) ? 1 : 2;
270
+ return { command, score };
271
+ }).filter((entry) => entry.score < 2);
272
+ return scored.sort((a, b) => a.score - b.score).map((entry) => entry.command);
273
+ }
274
+ function suggestionTrigger(name, char, bridge, options = {}) {
275
+ return Extension2.create({
276
+ name,
277
+ addProseMirrorPlugins() {
278
+ return [
279
+ Suggestion({
280
+ editor: this.editor,
281
+ pluginKey: new PluginKey(name),
282
+ char,
283
+ allowSpaces: options.allowSpaces ?? false,
284
+ allowedPrefixes: options.allowedPrefixes === void 0 ? [" ", "\u3000"] : options.allowedPrefixes,
285
+ items: ({ query }) => bridge.current?.items(query) ?? [],
286
+ command: ({ editor, range, props }) => bridge.current?.pick(editor, range, props),
287
+ render: () => ({
288
+ onStart: (props) => bridge.current?.onStart(props),
289
+ onUpdate: (props) => bridge.current?.onUpdate(props),
290
+ onKeyDown: (props) => bridge.current?.onKeyDown(props) ?? false,
291
+ onExit: () => bridge.current?.onExit()
292
+ })
293
+ })
294
+ ];
295
+ }
296
+ });
297
+ }
298
+
299
+ // src/extensions.ts
300
+ import { Extension as Extension3, Node, mergeAttributes, nodeInputRule } from "@tiptap/core";
301
+ import { Plugin, PluginKey as PluginKey2 } from "@tiptap/pm/state";
302
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
303
+ import StarterKit from "@tiptap/starter-kit";
304
+ import { TaskItem, TaskList } from "@tiptap/extension-list";
305
+ import { Table, TableCell, TableHeader, TableRow } from "@tiptap/extension-table";
306
+ import { Image } from "@tiptap/extension-image";
307
+ import { Details, DetailsContent, DetailsSummary } from "@tiptap/extension-details";
308
+ import { Placeholder } from "@tiptap/extensions";
309
+ var ListTightness = Extension3.create({
310
+ name: "listTightness",
311
+ addGlobalAttributes() {
312
+ return [
313
+ {
314
+ types: ["bulletList", "orderedList", "taskList"],
315
+ attributes: {
316
+ tight: {
317
+ default: true,
318
+ parseHTML: (element) => element.getAttribute("data-tight") !== "false",
319
+ renderHTML: (attrs) => ({ "data-tight": attrs.tight ? "true" : "false" })
320
+ }
321
+ }
322
+ }
323
+ ];
324
+ }
325
+ });
326
+ var CellAlignment = Extension3.create({
327
+ name: "cellAlignment",
328
+ addGlobalAttributes() {
329
+ return [
330
+ {
331
+ types: ["tableHeader", "tableCell"],
332
+ attributes: {
333
+ align: {
334
+ default: null,
335
+ parseHTML: (element) => element.style.textAlign || null,
336
+ renderHTML: (attrs) => attrs.align ? { style: `text-align:${attrs.align}` } : {}
337
+ }
338
+ }
339
+ }
340
+ ];
341
+ }
342
+ });
343
+ var Callout = Node.create({
344
+ name: "callout",
345
+ group: "block",
346
+ content: "block+",
347
+ defining: true,
348
+ addOptions() {
349
+ return {
350
+ titles: {
351
+ note: "Note",
352
+ tip: "Tip",
353
+ important: "Important",
354
+ warning: "Warning",
355
+ caution: "Caution"
356
+ }
357
+ };
358
+ },
359
+ addAttributes() {
360
+ return {
361
+ kind: { default: "note" },
362
+ title: { default: null }
363
+ };
364
+ },
365
+ parseHTML() {
366
+ return [{ tag: 'div[data-type="callout"]', contentElement: "div[data-callout-body]" }];
367
+ },
368
+ renderHTML({ node, HTMLAttributes }) {
369
+ const kind = node.attrs.kind;
370
+ return [
371
+ "div",
372
+ mergeAttributes(HTMLAttributes, {
373
+ "data-type": "callout",
374
+ "data-kind": kind,
375
+ class: "ui-prose-callout",
376
+ role: "note"
377
+ }),
378
+ [
379
+ "p",
380
+ { class: "ui-prose-callout-title", contenteditable: "false" },
381
+ node.attrs.title ?? this.options.titles[kind] ?? kind
382
+ ],
383
+ ["div", { "data-callout-body": "" }, 0]
384
+ ];
385
+ }
386
+ });
387
+ function shares(widths) {
388
+ const explicit = widths.reduce((sum, w) => sum + (w ?? 0), 0);
389
+ const open = widths.filter((w) => w == null).length;
390
+ const each = open ? Math.max(1, (100 - explicit) / open) : 0;
391
+ return widths.map((w) => w ?? Math.round(each * 100) / 100);
392
+ }
393
+ var Columns = Node.create({
394
+ name: "columns",
395
+ group: "block",
396
+ content: "column{2,4}",
397
+ defining: true,
398
+ isolating: true,
399
+ parseHTML() {
400
+ return [{ tag: 'div[data-type="columns"]' }];
401
+ },
402
+ renderHTML({ HTMLAttributes }) {
403
+ return [
404
+ "div",
405
+ mergeAttributes(HTMLAttributes, { "data-type": "columns", class: "ui-prose-columns" }),
406
+ 0
407
+ ];
408
+ },
409
+ addProseMirrorPlugins() {
410
+ return [
411
+ new Plugin({
412
+ key: new PluginKey2("columnShares"),
413
+ props: {
414
+ decorations: (state) => {
415
+ const decorations = [];
416
+ state.doc.descendants((node, pos) => {
417
+ if (node.type.name !== "columns") return true;
418
+ const widths = [];
419
+ node.forEach((column) => widths.push(column.attrs.width ?? null));
420
+ const grow = shares(widths);
421
+ node.forEach((column, offset, index) => {
422
+ const from = pos + 1 + offset;
423
+ decorations.push(
424
+ Decoration.node(from, from + column.nodeSize, {
425
+ style: `--prose-column-grow:${grow[index]}`
426
+ })
427
+ );
428
+ });
429
+ return true;
430
+ });
431
+ return DecorationSet.create(state.doc, decorations);
432
+ }
433
+ }
434
+ })
435
+ ];
436
+ }
437
+ });
438
+ var Column = Node.create({
439
+ name: "column",
440
+ content: "block+",
441
+ isolating: true,
442
+ addAttributes() {
443
+ return { width: { default: null } };
444
+ },
445
+ parseHTML() {
446
+ return [{ tag: 'div[data-type="column"]' }];
447
+ },
448
+ renderHTML({ HTMLAttributes }) {
449
+ return [
450
+ "div",
451
+ mergeAttributes(HTMLAttributes, { "data-type": "column", class: "ui-prose-column" }),
452
+ 0
453
+ ];
454
+ }
455
+ });
456
+ var Wikilink = Node.create({
457
+ name: "wikilink",
458
+ group: "inline",
459
+ inline: true,
460
+ atom: true,
461
+ selectable: true,
462
+ addAttributes() {
463
+ return { target: { default: "" }, heading: { default: null }, label: { default: null } };
464
+ },
465
+ parseHTML() {
466
+ return [{ tag: "span[data-wikilink]" }];
467
+ },
468
+ renderHTML({ node, HTMLAttributes }) {
469
+ const { target, heading, label } = node.attrs;
470
+ return [
471
+ "span",
472
+ mergeAttributes(HTMLAttributes, {
473
+ "data-wikilink": target,
474
+ class: "ui-block-editor-wikilink"
475
+ }),
476
+ label ?? (heading ? `${target}#${heading}` : target)
477
+ ];
478
+ },
479
+ renderText({ node }) {
480
+ const { target, heading, label } = node.attrs;
481
+ return `[[${target}${heading ? `#${heading}` : ""}${label ? `|${label}` : ""}]]`;
482
+ },
483
+ addInputRules() {
484
+ return [
485
+ nodeInputRule({
486
+ find: /(?<!!)\[\[([^[\]\n|#]+)(?:#([^[\]\n|]+))?(?:\|([^[\]\n]+))?\]\]$/,
487
+ type: this.type,
488
+ getAttributes: (match) => ({
489
+ target: match[1].trim(),
490
+ heading: match[2]?.trim() ?? null,
491
+ label: match[3] ?? null
492
+ })
493
+ })
494
+ ];
495
+ }
496
+ });
497
+ var EmbedInline = Node.create({
498
+ name: "embedInline",
499
+ group: "inline",
500
+ inline: true,
501
+ atom: true,
502
+ addAttributes() {
503
+ return { target: { default: "" } };
504
+ },
505
+ parseHTML() {
506
+ return [{ tag: "span[data-embed]" }];
507
+ },
508
+ renderHTML({ node, HTMLAttributes }) {
509
+ return [
510
+ "span",
511
+ mergeAttributes(HTMLAttributes, {
512
+ "data-embed": node.attrs.target,
513
+ class: "ui-block-editor-embed-inline"
514
+ }),
515
+ `![[${node.attrs.target}]]`
516
+ ];
517
+ },
518
+ renderText({ node }) {
519
+ return `![[${node.attrs.target}]]`;
520
+ },
521
+ addInputRules() {
522
+ return [
523
+ nodeInputRule({
524
+ find: /!\[\[([^[\]\n|#]+)\]\]$/,
525
+ type: this.type,
526
+ getAttributes: (match) => ({ target: match[1].trim() })
527
+ })
528
+ ];
529
+ }
530
+ });
531
+ var RawInline = Node.create({
532
+ name: "rawInline",
533
+ group: "inline",
534
+ inline: true,
535
+ atom: true,
536
+ addAttributes() {
537
+ return { source: { default: "" } };
538
+ },
539
+ parseHTML() {
540
+ return [{ tag: "code[data-raw-inline]" }];
541
+ },
542
+ renderHTML({ node, HTMLAttributes }) {
543
+ return [
544
+ "code",
545
+ mergeAttributes(HTMLAttributes, {
546
+ "data-raw-inline": "",
547
+ class: "ui-block-editor-raw-inline"
548
+ }),
549
+ node.attrs.source
550
+ ];
551
+ },
552
+ renderText({ node }) {
553
+ return node.attrs.source;
554
+ }
555
+ });
556
+ var Embed = Node.create({
557
+ name: "embed",
558
+ group: "block",
559
+ atom: true,
560
+ draggable: true,
561
+ addAttributes() {
562
+ return { target: { default: "" } };
563
+ },
564
+ parseHTML() {
565
+ return [{ tag: "div[data-embed]" }];
566
+ },
567
+ renderHTML({ node, HTMLAttributes }) {
568
+ return [
569
+ "div",
570
+ mergeAttributes(HTMLAttributes, {
571
+ "data-embed": node.attrs.target,
572
+ class: "ui-block-editor-embed"
573
+ }),
574
+ `![[${node.attrs.target}]]`
575
+ ];
576
+ }
577
+ });
578
+ var RawBlock = Node.create({
579
+ name: "rawBlock",
580
+ group: "block",
581
+ atom: true,
582
+ addAttributes() {
583
+ return { source: { default: "" } };
584
+ },
585
+ parseHTML() {
586
+ return [{ tag: "pre[data-raw-block]" }];
587
+ },
588
+ renderHTML({ node, HTMLAttributes }) {
589
+ return [
590
+ "pre",
591
+ mergeAttributes(HTMLAttributes, { "data-raw-block": "", class: "ui-block-editor-raw-block" }),
592
+ node.attrs.source
593
+ ];
594
+ }
595
+ });
596
+ function schemaExtensions(options) {
597
+ const resolve = options.resolveUrl;
598
+ return [
599
+ StarterKit.configure({
600
+ underline: false,
601
+ link: {
602
+ openOnClick: false,
603
+ autolink: true,
604
+ linkOnPaste: true,
605
+ HTMLAttributes: { rel: "noopener noreferrer nofollow", target: null }
606
+ },
607
+ heading: { levels: [1, 2, 3, 4, 5, 6] }
608
+ }),
609
+ ListTightness,
610
+ TaskList,
611
+ TaskItem.configure({ nested: true }),
612
+ Table.configure({ resizable: false }),
613
+ TableRow,
614
+ TableHeader,
615
+ TableCell,
616
+ CellAlignment,
617
+ Image.extend({
618
+ renderHTML({ HTMLAttributes }) {
619
+ const src = HTMLAttributes.src;
620
+ return [
621
+ "img",
622
+ mergeAttributes(HTMLAttributes, src && resolve ? { src: resolve(src) ?? src } : {})
623
+ ];
624
+ }
625
+ }).configure({ inline: true, allowBase64: false }),
626
+ Details.configure({ persist: false }),
627
+ DetailsSummary,
628
+ DetailsContent,
629
+ Callout.configure({ titles: options.calloutTitles }),
630
+ Columns,
631
+ Column,
632
+ Wikilink,
633
+ EmbedInline,
634
+ RawInline,
635
+ options.embed ?? Embed,
636
+ options.rawBlock ?? RawBlock,
637
+ Placeholder.configure({
638
+ includeChildren: true,
639
+ placeholder: ({ node }) => {
640
+ if (node.type.name === "heading")
641
+ return options.headingPlaceholder(node.attrs.level);
642
+ if (node.type.name === "detailsSummary") return options.togglePlaceholder;
643
+ return options.placeholder;
644
+ }
645
+ })
646
+ ];
647
+ }
648
+
649
+ // src/handle.tsx
650
+ import * as React2 from "react";
651
+ import { ArrowDown, ArrowUp, Copy, GripVertical, Plus, Trash2 } from "lucide-react";
652
+ import { Button } from "@godxjp/ui/general";
653
+ import {
654
+ DropdownMenu,
655
+ DropdownMenuContent,
656
+ DropdownMenuItem,
657
+ DropdownMenuLabel,
658
+ DropdownMenuSeparator,
659
+ DropdownMenuSub,
660
+ DropdownMenuSubContent,
661
+ DropdownMenuSubTrigger,
662
+ DropdownMenuTrigger
663
+ } from "@godxjp/ui/navigation";
664
+
665
+ // src/labels.tsx
666
+ import * as React from "react";
667
+ var LabelsContext = React.createContext((key) => key);
668
+ var useLabel = () => React.useContext(LabelsContext);
669
+
670
+ // src/handle.tsx
671
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
672
+ function BlockHandle({
673
+ editor,
674
+ host,
675
+ turnInto,
676
+ onAddBelow,
677
+ menuRequest
678
+ }) {
679
+ const label = useLabel();
680
+ const [block, setBlock] = React2.useState(null);
681
+ const [menuOpen, setMenuOpen] = React2.useState(false);
682
+ const pinned = React2.useRef(null);
683
+ const place = React2.useCallback(
684
+ (start2) => {
685
+ if (start2 == null || !host) return setBlock(null);
686
+ const dom = editor.view.nodeDOM(start2);
687
+ if (!(dom instanceof HTMLElement)) return setBlock(null);
688
+ const top = dom.getBoundingClientRect().top - host.getBoundingClientRect().top;
689
+ setBlock((prev) => prev?.start === start2 && prev.top === top ? prev : { start: start2, top });
690
+ },
691
+ [editor, host]
692
+ );
693
+ React2.useEffect(() => {
694
+ if (!host) return void 0;
695
+ const onMove = (event) => {
696
+ if (menuOpen || event.pointerType === "touch") return;
697
+ const children = [...editor.view.dom.children];
698
+ const row = children.find((child) => {
699
+ const r = child.getBoundingClientRect();
700
+ return event.clientY >= r.top && event.clientY <= r.bottom;
701
+ });
702
+ if (!row) return;
703
+ place(blockStartAt(editor, editor.view.posAtDOM(row, 0)));
704
+ };
705
+ host.addEventListener("pointermove", onMove);
706
+ return () => host.removeEventListener("pointermove", onMove);
707
+ }, [editor, host, menuOpen, place]);
708
+ React2.useEffect(() => {
709
+ const onSelection = () => {
710
+ if (!menuOpen && editor.isFocused) place(selectionBlockStart(editor));
711
+ };
712
+ editor.on("selectionUpdate", onSelection);
713
+ editor.on("update", onSelection);
714
+ editor.on("focus", onSelection);
715
+ return () => {
716
+ editor.off("selectionUpdate", onSelection);
717
+ editor.off("update", onSelection);
718
+ editor.off("focus", onSelection);
719
+ };
720
+ }, [editor, menuOpen, place]);
721
+ React2.useEffect(() => {
722
+ if (!menuRequest) return;
723
+ place(menuRequest.start);
724
+ pinned.current = menuRequest.start;
725
+ setMenuOpen(true);
726
+ }, [menuRequest, place]);
727
+ if (!block || !editor.isEditable) return null;
728
+ const start = pinned.current ?? block.start;
729
+ const run = (action) => () => {
730
+ action();
731
+ pinned.current = null;
732
+ };
733
+ return /* @__PURE__ */ jsxs("div", { className: "ui-block-editor-handle", style: { insetBlockStart: block.top }, children: [
734
+ /* @__PURE__ */ jsx2(
735
+ Button,
736
+ {
737
+ size: "icon-xs",
738
+ variant: "ghost",
739
+ "aria-label": label("addBlock"),
740
+ onMouseDown: (event) => event.preventDefault(),
741
+ onClick: () => onAddBelow(block.start),
742
+ children: /* @__PURE__ */ jsx2(Plus, {})
743
+ }
744
+ ),
745
+ /* @__PURE__ */ jsxs(
746
+ DropdownMenu,
747
+ {
748
+ open: menuOpen,
749
+ onOpenChange: (open) => {
750
+ setMenuOpen(open);
751
+ if (open) pinned.current = block.start;
752
+ else pinned.current = null;
753
+ },
754
+ children: [
755
+ /* @__PURE__ */ jsx2(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx2(
756
+ Button,
757
+ {
758
+ size: "icon-xs",
759
+ variant: "ghost",
760
+ "aria-label": label("dragHandle"),
761
+ draggable: true,
762
+ onDragStart: (event) => startBlockDrag(editor, block.start, event.nativeEvent),
763
+ children: /* @__PURE__ */ jsx2(GripVertical, {})
764
+ }
765
+ ) }),
766
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", "aria-label": label("blockActions"), children: [
767
+ /* @__PURE__ */ jsx2(DropdownMenuLabel, { children: label("blockActions") }),
768
+ /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [
769
+ /* @__PURE__ */ jsx2(DropdownMenuSubTrigger, { children: label("turnInto") }),
770
+ /* @__PURE__ */ jsx2(DropdownMenuSubContent, { children: turnInto.map((command) => /* @__PURE__ */ jsxs(
771
+ DropdownMenuItem,
772
+ {
773
+ onSelect: run(() => {
774
+ focusBlock(editor, start);
775
+ command.run(editor);
776
+ }),
777
+ children: [
778
+ /* @__PURE__ */ jsx2("span", { className: "ui-block-editor-menu-icon", "aria-hidden": "true", children: command.icon }),
779
+ command.label
780
+ ]
781
+ },
782
+ command.key
783
+ )) })
784
+ ] }),
785
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: run(() => duplicateBlock(editor, start)), children: [
786
+ /* @__PURE__ */ jsx2(Copy, { "aria-hidden": "true" }),
787
+ label("duplicate")
788
+ ] }),
789
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: run(() => void moveBlock(editor, start, -1)), children: [
790
+ /* @__PURE__ */ jsx2(ArrowUp, { "aria-hidden": "true" }),
791
+ label("moveUp")
792
+ ] }),
793
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: run(() => void moveBlock(editor, start, 1)), children: [
794
+ /* @__PURE__ */ jsx2(ArrowDown, { "aria-hidden": "true" }),
795
+ label("moveDown")
796
+ ] }),
797
+ /* @__PURE__ */ jsx2(DropdownMenuSeparator, {}),
798
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { variant: "destructive", onSelect: run(() => deleteBlock(editor, start)), children: [
799
+ /* @__PURE__ */ jsx2(Trash2, { "aria-hidden": "true" }),
800
+ label("delete")
801
+ ] })
802
+ ] })
803
+ ]
804
+ }
805
+ )
806
+ ] });
807
+ }
808
+
809
+ // src/messages.ts
810
+ import { registerMessages } from "@godxjp/ui/i18n";
811
+ var BLOCK_EDITOR_MESSAGES = {
812
+ ja: {
813
+ editor: "\u30A8\u30C7\u30A3\u30BF\u30FC",
814
+ placeholder: "\u300C/\u300D\u3067\u30D6\u30ED\u30C3\u30AF\u3092\u633F\u5165",
815
+ placeholderHeading: "\u898B\u51FA\u3057{level}",
816
+ placeholderToggle: "\u30C8\u30B0\u30EB",
817
+ blocks: "\u30D6\u30ED\u30C3\u30AF",
818
+ noResults: "\u8A72\u5F53\u3059\u308B\u30D6\u30ED\u30C3\u30AF\u304C\u3042\u308A\u307E\u305B\u3093",
819
+ text: "\u30C6\u30AD\u30B9\u30C8",
820
+ heading1: "\u898B\u51FA\u30571",
821
+ heading2: "\u898B\u51FA\u30572",
822
+ heading3: "\u898B\u51FA\u30573",
823
+ bulletList: "\u7B87\u6761\u66F8\u304D",
824
+ numberedList: "\u756A\u53F7\u4ED8\u304D\u30EA\u30B9\u30C8",
825
+ taskList: "ToDo\u30EA\u30B9\u30C8",
826
+ quote: "\u5F15\u7528",
827
+ callout: "\u30B3\u30FC\u30EB\u30A2\u30A6\u30C8",
828
+ code: "\u30B3\u30FC\u30C9",
829
+ divider: "\u533A\u5207\u308A\u7DDA",
830
+ table: "\u8868",
831
+ image: "\u753B\u50CF",
832
+ toggle: "\u30C8\u30B0\u30EB",
833
+ columns: "2\u5217",
834
+ addBlock: "\u4E0B\u306B\u30D6\u30ED\u30C3\u30AF\u3092\u8FFD\u52A0",
835
+ dragHandle: "\u30C9\u30E9\u30C3\u30B0\u3067\u79FB\u52D5\u3001\u30AF\u30EA\u30C3\u30AF\u3067\u30D6\u30ED\u30C3\u30AF\u306E\u64CD\u4F5C",
836
+ blockActions: "\u30D6\u30ED\u30C3\u30AF\u306E\u64CD\u4F5C",
837
+ turnInto: "\u30D6\u30ED\u30C3\u30AF\u306E\u7A2E\u985E\u3092\u5909\u66F4",
838
+ duplicate: "\u8907\u88FD",
839
+ delete: "\u524A\u9664",
840
+ moveUp: "\u4E0A\u3078\u79FB\u52D5",
841
+ moveDown: "\u4E0B\u3078\u79FB\u52D5",
842
+ formatting: "\u66F8\u5F0F",
843
+ bold: "\u592A\u5B57",
844
+ italic: "\u659C\u4F53",
845
+ strike: "\u53D6\u308A\u6D88\u3057\u7DDA",
846
+ inlineCode: "\u30B3\u30FC\u30C9",
847
+ link: "\u30EA\u30F3\u30AF",
848
+ linkUrl: "\u30EA\u30F3\u30AF\u5148\u306EURL",
849
+ applyLink: "\u9069\u7528",
850
+ removeLink: "\u30EA\u30F3\u30AF\u3092\u89E3\u9664",
851
+ uploading: "{name} \u3092\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u4E2D\u2026",
852
+ uploadFailed: "{name} \u3092\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
853
+ retry: "\u518D\u8A66\u884C",
854
+ remove: "\u524A\u9664",
855
+ rawBlock: "Markdown \u306E\u30BD\u30FC\u30B9",
856
+ editSource: "Markdown \u3092\u7DE8\u96C6",
857
+ done: "\u5B8C\u4E86",
858
+ embed: "\u57CB\u3081\u8FBC\u307F: {target}",
859
+ calloutNote: "\u30E1\u30E2",
860
+ calloutTip: "\u30D2\u30F3\u30C8",
861
+ calloutImportant: "\u91CD\u8981",
862
+ calloutWarning: "\u8B66\u544A",
863
+ calloutCaution: "\u6CE8\u610F"
864
+ },
865
+ en: {
866
+ editor: "Editor",
867
+ placeholder: "Type / for blocks",
868
+ placeholderHeading: "Heading {level}",
869
+ placeholderToggle: "Toggle",
870
+ blocks: "Blocks",
871
+ noResults: "No matching blocks",
872
+ text: "Text",
873
+ heading1: "Heading 1",
874
+ heading2: "Heading 2",
875
+ heading3: "Heading 3",
876
+ bulletList: "Bulleted list",
877
+ numberedList: "Numbered list",
878
+ taskList: "To-do list",
879
+ quote: "Quote",
880
+ callout: "Callout",
881
+ code: "Code",
882
+ divider: "Divider",
883
+ table: "Table",
884
+ image: "Image",
885
+ toggle: "Toggle",
886
+ columns: "2 columns",
887
+ addBlock: "Add a block below",
888
+ dragHandle: "Drag to move, click for block actions",
889
+ blockActions: "Block actions",
890
+ turnInto: "Turn into",
891
+ duplicate: "Duplicate",
892
+ delete: "Delete",
893
+ moveUp: "Move up",
894
+ moveDown: "Move down",
895
+ formatting: "Formatting",
896
+ bold: "Bold",
897
+ italic: "Italic",
898
+ strike: "Strikethrough",
899
+ inlineCode: "Code",
900
+ link: "Link",
901
+ linkUrl: "Link URL",
902
+ applyLink: "Apply",
903
+ removeLink: "Remove link",
904
+ uploading: "Uploading {name}\u2026",
905
+ uploadFailed: "Could not upload {name}.",
906
+ retry: "Retry",
907
+ remove: "Remove",
908
+ rawBlock: "Markdown source",
909
+ editSource: "Edit Markdown",
910
+ done: "Done",
911
+ embed: "Embed: {target}",
912
+ calloutNote: "Note",
913
+ calloutTip: "Tip",
914
+ calloutImportant: "Important",
915
+ calloutWarning: "Warning",
916
+ calloutCaution: "Caution"
917
+ },
918
+ vi: {
919
+ editor: "Tr\xECnh so\u1EA1n th\u1EA3o",
920
+ placeholder: "G\xF5 / \u0111\u1EC3 ch\xE8n kh\u1ED1i",
921
+ placeholderHeading: "Ti\xEAu \u0111\u1EC1 {level}",
922
+ placeholderToggle: "M\u1EE5c thu g\u1ECDn",
923
+ blocks: "Kh\u1ED1i",
924
+ noResults: "Kh\xF4ng c\xF3 kh\u1ED1i ph\xF9 h\u1EE3p",
925
+ text: "V\u0103n b\u1EA3n",
926
+ heading1: "Ti\xEAu \u0111\u1EC1 1",
927
+ heading2: "Ti\xEAu \u0111\u1EC1 2",
928
+ heading3: "Ti\xEAu \u0111\u1EC1 3",
929
+ bulletList: "Danh s\xE1ch g\u1EA1ch \u0111\u1EA7u d\xF2ng",
930
+ numberedList: "Danh s\xE1ch \u0111\xE1nh s\u1ED1",
931
+ taskList: "Danh s\xE1ch vi\u1EC7c c\u1EA7n l\xE0m",
932
+ quote: "Tr\xEDch d\u1EABn",
933
+ callout: "Ch\xFA th\xEDch n\u1ED5i b\u1EADt",
934
+ code: "M\xE3",
935
+ divider: "\u0110\u01B0\u1EDDng ph\xE2n c\xE1ch",
936
+ table: "B\u1EA3ng",
937
+ image: "H\xECnh \u1EA3nh",
938
+ toggle: "M\u1EE5c thu g\u1ECDn",
939
+ columns: "2 c\u1ED9t",
940
+ addBlock: "Th\xEAm kh\u1ED1i b\xEAn d\u01B0\u1EDBi",
941
+ dragHandle: "K\xE9o \u0111\u1EC3 di chuy\u1EC3n, b\u1EA5m \u0111\u1EC3 thao t\xE1c v\u1EDBi kh\u1ED1i",
942
+ blockActions: "Thao t\xE1c v\u1EDBi kh\u1ED1i",
943
+ turnInto: "Chuy\u1EC3n th\xE0nh",
944
+ duplicate: "Nh\xE2n b\u1EA3n",
945
+ delete: "Xo\xE1",
946
+ moveUp: "Chuy\u1EC3n l\xEAn",
947
+ moveDown: "Chuy\u1EC3n xu\u1ED1ng",
948
+ formatting: "\u0110\u1ECBnh d\u1EA1ng",
949
+ bold: "In \u0111\u1EADm",
950
+ italic: "In nghi\xEAng",
951
+ strike: "G\u1EA1ch ngang",
952
+ inlineCode: "M\xE3",
953
+ link: "Li\xEAn k\u1EBFt",
954
+ linkUrl: "URL li\xEAn k\u1EBFt",
955
+ applyLink: "\xC1p d\u1EE5ng",
956
+ removeLink: "B\u1ECF li\xEAn k\u1EBFt",
957
+ uploading: "\u0110ang t\u1EA3i {name} l\xEAn\u2026",
958
+ uploadFailed: "Kh\xF4ng t\u1EA3i \u0111\u01B0\u1EE3c {name} l\xEAn.",
959
+ retry: "Th\u1EED l\u1EA1i",
960
+ remove: "G\u1EE1 b\u1ECF",
961
+ rawBlock: "M\xE3 ngu\u1ED3n Markdown",
962
+ editSource: "S\u1EEDa Markdown",
963
+ done: "Xong",
964
+ embed: "Nh\xFAng: {target}",
965
+ calloutNote: "Ghi ch\xFA",
966
+ calloutTip: "M\u1EB9o",
967
+ calloutImportant: "Quan tr\u1ECDng",
968
+ calloutWarning: "C\u1EA3nh b\xE1o",
969
+ calloutCaution: "Th\u1EADn tr\u1ECDng"
970
+ }
971
+ };
972
+ var registered = false;
973
+ function ensureBlockEditorMessages() {
974
+ if (registered) return;
975
+ registered = true;
976
+ for (const locale of ["ja", "en", "vi"]) {
977
+ registerMessages(locale, { blockEditor: BLOCK_EDITOR_MESSAGES[locale] });
978
+ }
979
+ }
980
+
981
+ // src/suggestion-menu.tsx
982
+ import * as React3 from "react";
983
+ import { Popover, PopoverAnchor, PopoverContent } from "@godxjp/ui/data-display";
984
+ import { Command, CommandEmpty, CommandItem, CommandList } from "@godxjp/ui/data-entry";
985
+ import { VisuallyHidden } from "@godxjp/ui/general";
986
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
987
+ function SuggestionMenu({
988
+ open,
989
+ rect,
990
+ host,
991
+ items,
992
+ active,
993
+ onActiveChange,
994
+ onPick,
995
+ onDismiss,
996
+ label,
997
+ empty,
998
+ countLabel,
999
+ editable
1000
+ }) {
1001
+ const menuId = React3.useId();
1002
+ const panel = () => editable?.ownerDocument.querySelector(`[data-block-editor-menu="${CSS.escape(menuId)}"]`);
1003
+ const box = host?.getBoundingClientRect();
1004
+ const anchorStyle = rect && box ? {
1005
+ position: "absolute",
1006
+ insetInlineStart: rect.left - box.left,
1007
+ insetBlockStart: rect.top - box.top,
1008
+ blockSize: rect.height,
1009
+ inlineSize: 1,
1010
+ pointerEvents: "none"
1011
+ } : { position: "absolute", inlineSize: 0, blockSize: 0 };
1012
+ const wired = React3.useRef(false);
1013
+ React3.useEffect(() => {
1014
+ if (!editable) return void 0;
1015
+ if (!open) {
1016
+ if (wired.current) {
1017
+ editable.removeAttribute("aria-controls");
1018
+ editable.removeAttribute("aria-activedescendant");
1019
+ wired.current = false;
1020
+ }
1021
+ return void 0;
1022
+ }
1023
+ const apply = () => {
1024
+ const list = panel()?.querySelector("[cmdk-list]")?.id;
1025
+ const option = panel()?.querySelector('[cmdk-item][aria-selected="true"]')?.id;
1026
+ editable.setAttribute("aria-haspopup", "listbox");
1027
+ editable.setAttribute("aria-autocomplete", "list");
1028
+ if (list) editable.setAttribute("aria-controls", list);
1029
+ if (option) editable.setAttribute("aria-activedescendant", option);
1030
+ else editable.removeAttribute("aria-activedescendant");
1031
+ wired.current = true;
1032
+ };
1033
+ apply();
1034
+ const frame = requestAnimationFrame(apply);
1035
+ const observer = new MutationObserver(apply);
1036
+ observer.observe(editable.ownerDocument.body, {
1037
+ subtree: true,
1038
+ childList: true,
1039
+ attributes: true,
1040
+ attributeFilter: ["aria-selected"]
1041
+ });
1042
+ return () => {
1043
+ cancelAnimationFrame(frame);
1044
+ observer.disconnect();
1045
+ };
1046
+ });
1047
+ return /* @__PURE__ */ jsxs2(Popover, { open, onOpenChange: (next) => next ? void 0 : onDismiss(), children: [
1048
+ /* @__PURE__ */ jsx3(PopoverAnchor, { style: anchorStyle, "aria-hidden": "true" }),
1049
+ /* @__PURE__ */ jsx3(VisuallyHidden, { role: "status", "aria-live": "polite", children: open ? countLabel : "" }),
1050
+ /* @__PURE__ */ jsx3(
1051
+ PopoverContent,
1052
+ {
1053
+ "data-block-editor-menu": menuId,
1054
+ flush: true,
1055
+ align: "start",
1056
+ "aria-label": label,
1057
+ className: "ui-block-editor-menu",
1058
+ onOpenAutoFocus: (event) => event.preventDefault(),
1059
+ onCloseAutoFocus: (event) => event.preventDefault(),
1060
+ children: /* @__PURE__ */ jsx3(
1061
+ Command,
1062
+ {
1063
+ value: active ?? "",
1064
+ onValueChange: onActiveChange,
1065
+ shouldFilter: false,
1066
+ label,
1067
+ children: /* @__PURE__ */ jsxs2(CommandList, { label, children: [
1068
+ /* @__PURE__ */ jsx3(CommandEmpty, { children: empty }),
1069
+ items.map((item) => /* @__PURE__ */ jsxs2(
1070
+ CommandItem,
1071
+ {
1072
+ value: item.value,
1073
+ onMouseDown: (event) => event.preventDefault(),
1074
+ onSelect: () => onPick(item.value),
1075
+ children: [
1076
+ item.icon ? /* @__PURE__ */ jsx3("span", { className: "ui-block-editor-menu-icon", "aria-hidden": "true", children: item.icon }) : null,
1077
+ /* @__PURE__ */ jsxs2("span", { className: "ui-block-editor-menu-text", children: [
1078
+ /* @__PURE__ */ jsx3("span", { children: item.label }),
1079
+ item.description ? /* @__PURE__ */ jsx3("span", { className: "ui-block-editor-menu-description", children: item.description }) : null
1080
+ ] })
1081
+ ]
1082
+ },
1083
+ item.value
1084
+ ))
1085
+ ] })
1086
+ }
1087
+ )
1088
+ }
1089
+ )
1090
+ ] });
1091
+ }
1092
+
1093
+ // src/toolbar.tsx
1094
+ import * as React4 from "react";
1095
+ import { useEditorState } from "@tiptap/react";
1096
+ import { BubbleMenu } from "@tiptap/react/menus";
1097
+ import { Bold, Code as Code2, Italic, Link2, Strikethrough } from "lucide-react";
1098
+ import { Input, ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
1099
+ import { Button as Button2 } from "@godxjp/ui/general";
1100
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1101
+ var MARKS = ["bold", "italic", "strike", "code"];
1102
+ function FormatToolbar({ editor }) {
1103
+ const label = useLabel();
1104
+ const [linking, setLinking] = React4.useState(false);
1105
+ const [href, setHref] = React4.useState("");
1106
+ const state = useEditorState({
1107
+ editor,
1108
+ selector: ({ editor: e }) => ({
1109
+ marks: MARKS.filter((mark) => e.isActive(mark)),
1110
+ link: e.getAttributes("link").href ?? null
1111
+ })
1112
+ });
1113
+ const toggle = (next) => {
1114
+ const chain = editor.chain().focus();
1115
+ for (const mark of MARKS) {
1116
+ const on = next.includes(mark);
1117
+ if (on === state.marks.includes(mark)) continue;
1118
+ if (mark === "bold") chain.toggleBold();
1119
+ else if (mark === "italic") chain.toggleItalic();
1120
+ else if (mark === "strike") chain.toggleStrike();
1121
+ else chain.toggleCode();
1122
+ }
1123
+ chain.run();
1124
+ };
1125
+ const applyLink = () => {
1126
+ const url = href.trim();
1127
+ const chain = editor.chain().focus().extendMarkRange("link");
1128
+ if (url) chain.setLink({ href: url }).run();
1129
+ else chain.unsetLink().run();
1130
+ setLinking(false);
1131
+ };
1132
+ return /* @__PURE__ */ jsx4(
1133
+ BubbleMenu,
1134
+ {
1135
+ editor,
1136
+ className: "ui-block-editor-toolbar",
1137
+ shouldShow: ({ editor: e, state: s }) => e.isEditable && !s.selection.empty && !e.isActive("codeBlock") && s.selection.content().size > 0,
1138
+ options: { placement: "top" },
1139
+ children: linking ? /* @__PURE__ */ jsxs3(
1140
+ "form",
1141
+ {
1142
+ className: "ui-block-editor-toolbar-link",
1143
+ onSubmit: (event) => {
1144
+ event.preventDefault();
1145
+ applyLink();
1146
+ },
1147
+ children: [
1148
+ /* @__PURE__ */ jsx4(
1149
+ Input,
1150
+ {
1151
+ size: "sm",
1152
+ type: "url",
1153
+ inputMode: "url",
1154
+ "aria-label": label("linkUrl"),
1155
+ value: href,
1156
+ onValueChange: setHref,
1157
+ onKeyDown: (event) => {
1158
+ if (event.key === "Escape") {
1159
+ setLinking(false);
1160
+ editor.commands.focus();
1161
+ }
1162
+ },
1163
+ autoFocus: true
1164
+ }
1165
+ ),
1166
+ /* @__PURE__ */ jsx4(Button2, { size: "sm", type: "submit", children: label("applyLink") }),
1167
+ state.link ? /* @__PURE__ */ jsx4(
1168
+ Button2,
1169
+ {
1170
+ size: "sm",
1171
+ variant: "ghost",
1172
+ onClick: () => {
1173
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
1174
+ setLinking(false);
1175
+ },
1176
+ children: label("removeLink")
1177
+ }
1178
+ ) : null
1179
+ ]
1180
+ }
1181
+ ) : /* @__PURE__ */ jsxs3("div", { className: "ui-block-editor-toolbar-row", children: [
1182
+ /* @__PURE__ */ jsxs3(
1183
+ ToggleGroup,
1184
+ {
1185
+ type: "multiple",
1186
+ size: "sm",
1187
+ "aria-label": label("formatting"),
1188
+ value: state.marks,
1189
+ onValueChange: toggle,
1190
+ children: [
1191
+ /* @__PURE__ */ jsx4(ToggleGroupItem, { value: "bold", "aria-label": label("bold"), children: /* @__PURE__ */ jsx4(Bold, {}) }),
1192
+ /* @__PURE__ */ jsx4(ToggleGroupItem, { value: "italic", "aria-label": label("italic"), children: /* @__PURE__ */ jsx4(Italic, {}) }),
1193
+ /* @__PURE__ */ jsx4(ToggleGroupItem, { value: "strike", "aria-label": label("strike"), children: /* @__PURE__ */ jsx4(Strikethrough, {}) }),
1194
+ /* @__PURE__ */ jsx4(ToggleGroupItem, { value: "code", "aria-label": label("inlineCode"), children: /* @__PURE__ */ jsx4(Code2, {}) })
1195
+ ]
1196
+ }
1197
+ ),
1198
+ /* @__PURE__ */ jsx4(
1199
+ Button2,
1200
+ {
1201
+ size: "icon-sm",
1202
+ variant: state.link ? "secondary" : "ghost",
1203
+ "aria-label": label("link"),
1204
+ "aria-pressed": state.link != null,
1205
+ onClick: () => {
1206
+ setHref(state.link ?? "");
1207
+ setLinking(true);
1208
+ },
1209
+ children: /* @__PURE__ */ jsx4(Link2, {})
1210
+ }
1211
+ )
1212
+ ] })
1213
+ }
1214
+ );
1215
+ }
1216
+
1217
+ // src/views.tsx
1218
+ import * as React5 from "react";
1219
+ import { Node as Node2, mergeAttributes as mergeAttributes2 } from "@tiptap/core";
1220
+ import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
1221
+ import { LoaderCircle, TriangleAlert } from "lucide-react";
1222
+ import { parse } from "@godxjp/markdown/codec";
1223
+ import { Textarea } from "@godxjp/ui/data-entry";
1224
+ import { Button as Button3, Text } from "@godxjp/ui/general";
1225
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1226
+ function RawBlockView({ node, editor, getPos, updateAttributes, selected }) {
1227
+ const tr = useLabel();
1228
+ const [editing, setEditing] = React5.useState(false);
1229
+ const [draft, setDraft] = React5.useState(node.attrs.source);
1230
+ const commit = () => {
1231
+ setEditing(false);
1232
+ const doc = parse(draft);
1233
+ const pos = getPos();
1234
+ const stillRaw = doc.content.length === 1 && doc.content[0].type === "rawBlock";
1235
+ if (stillRaw || typeof pos !== "number") {
1236
+ updateAttributes({ source: draft });
1237
+ return;
1238
+ }
1239
+ editor.chain().focus().insertContentAt({ from: pos, to: pos + node.nodeSize }, doc.content).run();
1240
+ };
1241
+ return /* @__PURE__ */ jsxs4(
1242
+ NodeViewWrapper,
1243
+ {
1244
+ className: "ui-block-editor-raw-block",
1245
+ "data-selected": selected ? "" : void 0,
1246
+ children: [
1247
+ /* @__PURE__ */ jsxs4("div", { className: "ui-block-editor-raw-block-head", contentEditable: false, children: [
1248
+ /* @__PURE__ */ jsx5(Text, { size: "xs", tone: "muted", children: tr("rawBlock") }),
1249
+ editor.isEditable ? /* @__PURE__ */ jsx5(
1250
+ Button3,
1251
+ {
1252
+ size: "xs",
1253
+ variant: "ghost",
1254
+ onClick: () => editing ? commit() : (setDraft(node.attrs.source), setEditing(true)),
1255
+ children: editing ? tr("done") : tr("editSource")
1256
+ }
1257
+ ) : null
1258
+ ] }),
1259
+ editing ? /* @__PURE__ */ jsx5(
1260
+ Textarea,
1261
+ {
1262
+ "aria-label": tr("rawBlock"),
1263
+ value: draft,
1264
+ rows: Math.min(12, Math.max(3, draft.split("\n").length)),
1265
+ onValueChange: setDraft,
1266
+ onKeyDown: (event) => {
1267
+ event.stopPropagation();
1268
+ if (event.key === "Escape") commit();
1269
+ },
1270
+ autoFocus: true
1271
+ }
1272
+ ) : /* @__PURE__ */ jsx5("pre", { className: "ui-block-editor-raw-block-source", contentEditable: false, children: node.attrs.source })
1273
+ ]
1274
+ }
1275
+ );
1276
+ }
1277
+ function rawBlockNode() {
1278
+ return RawBlock.extend({ addNodeView: () => ReactNodeViewRenderer(RawBlockView) });
1279
+ }
1280
+ function embedNode(render) {
1281
+ function EmbedView({ node, selected }) {
1282
+ const tr = useLabel();
1283
+ const target = node.attrs.target;
1284
+ return /* @__PURE__ */ jsx5(
1285
+ NodeViewWrapper,
1286
+ {
1287
+ className: "ui-block-editor-embed",
1288
+ "data-selected": selected ? "" : void 0,
1289
+ contentEditable: false,
1290
+ children: render ? render(target) : /* @__PURE__ */ jsx5(Text, { size: "sm", tone: "muted", children: tr("embed", { target }) })
1291
+ }
1292
+ );
1293
+ }
1294
+ return Embed.extend({ addNodeView: () => ReactNodeViewRenderer(EmbedView) });
1295
+ }
1296
+ function uploadNode(actions) {
1297
+ function UploadView({ node }) {
1298
+ const tr = useLabel();
1299
+ const { id, name, status } = node.attrs;
1300
+ return (
1301
+ // A plain span, not NodeViewWrapper: Tiptap's wrapper spreads its `as` prop onto the DOM as an
1302
+ // invalid attribute, and this inline view needs nothing else it adds (no drag).
1303
+ /* @__PURE__ */ jsx5(
1304
+ "span",
1305
+ {
1306
+ "data-node-view-wrapper": "",
1307
+ className: "ui-block-editor-upload",
1308
+ "data-status": status,
1309
+ contentEditable: false,
1310
+ children: status === "uploading" ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1311
+ /* @__PURE__ */ jsx5(LoaderCircle, { className: "ui-block-editor-upload-spinner", "aria-hidden": "true" }),
1312
+ /* @__PURE__ */ jsx5(Text, { as: "span", size: "sm", tone: "muted", role: "status", children: tr("uploading", { name }) })
1313
+ ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
1314
+ /* @__PURE__ */ jsx5(TriangleAlert, { "aria-hidden": "true" }),
1315
+ /* @__PURE__ */ jsx5(Text, { as: "span", size: "sm", tone: "destructive", role: "alert", children: tr("uploadFailed", { name }) }),
1316
+ /* @__PURE__ */ jsx5(Button3, { size: "xs", variant: "ghost", onClick: () => actions.retry(id), children: tr("retry") }),
1317
+ /* @__PURE__ */ jsx5(Button3, { size: "xs", variant: "ghost", onClick: () => actions.remove(id), children: tr("remove") })
1318
+ ] })
1319
+ }
1320
+ )
1321
+ );
1322
+ }
1323
+ return Node2.create({
1324
+ name: "uploadPlaceholder",
1325
+ group: "inline",
1326
+ inline: true,
1327
+ atom: true,
1328
+ selectable: false,
1329
+ addAttributes() {
1330
+ return { id: { default: "" }, name: { default: "" }, status: { default: "uploading" } };
1331
+ },
1332
+ parseHTML() {
1333
+ return [{ tag: "span[data-upload]" }];
1334
+ },
1335
+ renderHTML({ HTMLAttributes }) {
1336
+ return ["span", mergeAttributes2(HTMLAttributes, { "data-upload": "" })];
1337
+ },
1338
+ addNodeView: () => ReactNodeViewRenderer(UploadView, { as: "span" })
1339
+ });
1340
+ }
1341
+
1342
+ // src/block-editor.tsx
1343
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1344
+ function useSuggestion(toMenuItem, items, pick) {
1345
+ const [menu, setMenu] = React6.useState(null);
1346
+ const menuRef = React6.useRef(menu);
1347
+ menuRef.current = menu;
1348
+ const bridge = React6.useRef(null);
1349
+ const show = (props) => setMenu((prev) => {
1350
+ const first = props.items[0] ? toMenuItem(props.items[0]).value : void 0;
1351
+ const keep = prev?.active && props.items.some((i) => toMenuItem(i).value === prev.active);
1352
+ return {
1353
+ items: props.items,
1354
+ active: keep ? prev.active : first,
1355
+ rect: props.clientRect?.() ?? null,
1356
+ command: props.command
1357
+ };
1358
+ });
1359
+ bridge.current = {
1360
+ items,
1361
+ pick,
1362
+ onStart: show,
1363
+ onUpdate: show,
1364
+ onExit: () => setMenu(null),
1365
+ onKeyDown: ({ event }) => {
1366
+ const current = menuRef.current;
1367
+ if (!current) return false;
1368
+ const values = current.items.map((i) => toMenuItem(i).value);
1369
+ const at = Math.max(0, values.indexOf(current.active ?? ""));
1370
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1371
+ if (!values.length) return true;
1372
+ const next = values[(at + (event.key === "ArrowDown" ? 1 : -1) + values.length) % values.length];
1373
+ setMenu({ ...current, active: next });
1374
+ return true;
1375
+ }
1376
+ if (event.key === "Enter" || event.key === "Tab") {
1377
+ const item = current.items[at];
1378
+ if (!item) return false;
1379
+ current.command(item);
1380
+ return true;
1381
+ }
1382
+ if (event.key === "Escape") {
1383
+ setMenu(null);
1384
+ return true;
1385
+ }
1386
+ return false;
1387
+ }
1388
+ };
1389
+ return { menu, setMenu, bridge };
1390
+ }
1391
+ var uploadSeq = 0;
1392
+ var BlockEditor = React6.forwardRef(
1393
+ function BlockEditor2({
1394
+ value: valueProp,
1395
+ defaultValue = "",
1396
+ onValueChange,
1397
+ upload,
1398
+ pickMedia,
1399
+ uploadBlockedReason = null,
1400
+ suggestWikilinks,
1401
+ renderEmbed,
1402
+ resolveUrl,
1403
+ actions = [],
1404
+ labels,
1405
+ disabled = false,
1406
+ readOnly = false,
1407
+ autoFocus = false,
1408
+ id,
1409
+ className,
1410
+ "aria-label": ariaLabel,
1411
+ "aria-labelledby": ariaLabelledby,
1412
+ "aria-describedby": ariaDescribedby
1413
+ }, ref) {
1414
+ ensureBlockEditorMessages();
1415
+ const { t } = useTranslation();
1416
+ const label = React6.useCallback(
1417
+ (key, params) => labels?.[key] ?? t(`blockEditor.${key}`, params),
1418
+ [labels, t]
1419
+ );
1420
+ const labelRef = React6.useRef(label);
1421
+ labelRef.current = label;
1422
+ const hostRef = React6.useRef(null);
1423
+ const [host, setHost] = React6.useState(null);
1424
+ const [status, setStatus] = React6.useState(null);
1425
+ const [menuRequest, setMenuRequest] = React6.useState(
1426
+ null
1427
+ );
1428
+ const fileInput = React6.useRef(null);
1429
+ const live = React6.useRef({
1430
+ onValueChange,
1431
+ upload,
1432
+ pickMedia,
1433
+ uploadBlockedReason,
1434
+ suggestWikilinks,
1435
+ actions
1436
+ });
1437
+ live.current = {
1438
+ onValueChange,
1439
+ upload,
1440
+ pickMedia,
1441
+ uploadBlockedReason,
1442
+ suggestWikilinks,
1443
+ actions
1444
+ };
1445
+ const initial = valueProp ?? defaultValue;
1446
+ const lastMarkdown = React6.useRef(normalize(initial));
1447
+ const uploads = React6.useRef(/* @__PURE__ */ new Map());
1448
+ const editorRef = React6.useRef(null);
1449
+ const findUpload = (editor2, uploadId) => {
1450
+ let found = null;
1451
+ editor2.state.doc.descendants((node, pos) => {
1452
+ if (found) return false;
1453
+ if (node.type.name === "uploadPlaceholder" && node.attrs.id === uploadId)
1454
+ found = { pos, size: node.nodeSize };
1455
+ return true;
1456
+ });
1457
+ return found;
1458
+ };
1459
+ const runUpload = React6.useCallback((uploadId) => {
1460
+ const entry = uploads.current.get(uploadId);
1461
+ const editor2 = editorRef.current;
1462
+ const store = live.current.upload;
1463
+ if (!entry || !editor2 || !store) return;
1464
+ const { file, controller } = entry;
1465
+ store(file, { signal: controller.signal }).then((result) => {
1466
+ const target = findUpload(editor2, uploadId);
1467
+ uploads.current.delete(uploadId);
1468
+ if (!target) return;
1469
+ const name = result.name ?? file.name;
1470
+ const node = file.type.startsWith("image/") ? { type: "image", attrs: { src: result.url, alt: name, title: null } } : {
1471
+ type: "text",
1472
+ text: name,
1473
+ marks: [{ type: "link", attrs: { href: result.url, title: null } }]
1474
+ };
1475
+ editor2.chain().insertContentAt({ from: target.pos, to: target.pos + target.size }, node).run();
1476
+ }).catch(() => {
1477
+ if (controller.signal.aborted) return;
1478
+ const target = findUpload(editor2, uploadId);
1479
+ if (!target) return;
1480
+ editor2.view.dispatch(editor2.state.tr.setNodeAttribute(target.pos, "status", "error"));
1481
+ });
1482
+ }, []);
1483
+ const startUploads = React6.useCallback(
1484
+ (editor2, files, at) => {
1485
+ if (files.length === 0) return false;
1486
+ const blocked = live.current.uploadBlockedReason;
1487
+ if (blocked || !live.current.upload) {
1488
+ if (blocked) setStatus(blocked);
1489
+ return Boolean(blocked);
1490
+ }
1491
+ const nodes = files.map((file) => {
1492
+ const uploadId = `upload-${++uploadSeq}`;
1493
+ uploads.current.set(uploadId, { file, controller: new AbortController() });
1494
+ return {
1495
+ uploadId,
1496
+ node: {
1497
+ type: "uploadPlaceholder",
1498
+ attrs: { id: uploadId, name: file.name, status: "uploading" }
1499
+ }
1500
+ };
1501
+ });
1502
+ const position = at ?? editor2.state.selection.from;
1503
+ editor2.chain().focus().insertContentAt(
1504
+ position,
1505
+ nodes.map((n) => n.node)
1506
+ ).run();
1507
+ for (const { uploadId } of nodes) runUpload(uploadId);
1508
+ return true;
1509
+ },
1510
+ [runUpload]
1511
+ );
1512
+ const uploadActions = React6.useMemo(
1513
+ () => ({
1514
+ retry: (uploadId) => {
1515
+ const editor2 = editorRef.current;
1516
+ const entry = uploads.current.get(uploadId);
1517
+ if (!editor2 || !entry) return;
1518
+ entry.controller = new AbortController();
1519
+ const target = findUpload(editor2, uploadId);
1520
+ if (target)
1521
+ editor2.view.dispatch(
1522
+ editor2.state.tr.setNodeAttribute(target.pos, "status", "uploading")
1523
+ );
1524
+ runUpload(uploadId);
1525
+ },
1526
+ remove: (uploadId) => {
1527
+ const editor2 = editorRef.current;
1528
+ const target = editor2 ? findUpload(editor2, uploadId) : null;
1529
+ if (editor2 && target)
1530
+ editor2.chain().focus().deleteRange({ from: target.pos, to: target.pos + target.size }).run();
1531
+ }
1532
+ }),
1533
+ [runUpload]
1534
+ );
1535
+ const insertImage = React6.useCallback((editor2) => {
1536
+ const pick = live.current.pickMedia;
1537
+ if (pick) {
1538
+ const at = editor2.state.selection.from;
1539
+ void pick().then((media) => {
1540
+ if (media)
1541
+ editor2.chain().focus().insertContentAt(at, {
1542
+ type: "image",
1543
+ attrs: { src: media.url, alt: media.name ?? "", title: null }
1544
+ }).run();
1545
+ });
1546
+ return;
1547
+ }
1548
+ fileInput.current?.click();
1549
+ }, []);
1550
+ const canInsertImage = Boolean(pickMedia || upload);
1551
+ const commands = React6.useMemo(() => {
1552
+ const builtin = builtinCommands(
1553
+ Object.fromEntries(
1554
+ Object.keys(BLOCK_EDITOR_MESSAGES.en).map((k) => [
1555
+ k,
1556
+ label(k)
1557
+ ])
1558
+ ),
1559
+ canInsertImage ? insertImage : null
1560
+ );
1561
+ const host2 = actions.map((action) => ({
1562
+ key: action.key,
1563
+ label: action.label,
1564
+ icon: action.icon,
1565
+ keywords: action.keywords,
1566
+ run: (editor2) => action.run({
1567
+ editor: editor2,
1568
+ insertMarkdown: (markdown) => editor2.chain().focus().insertContent(parse2(markdown).content).run(),
1569
+ markdown: () => serialize(editor2.getJSON())
1570
+ })
1571
+ }));
1572
+ return [...builtin, ...host2];
1573
+ }, [actions, canInsertImage, insertImage, label]);
1574
+ const commandsRef = React6.useRef(commands);
1575
+ commandsRef.current = commands;
1576
+ const turnInto = React6.useMemo(
1577
+ () => commands.filter((c) => TURN_INTO.includes(c.key)),
1578
+ [commands]
1579
+ );
1580
+ const slash = useSuggestion(
1581
+ (c) => ({ value: c.key, label: c.label, icon: c.icon }),
1582
+ (query) => filterCommands(commandsRef.current, query),
1583
+ (editor2, range, command) => {
1584
+ editor2.chain().focus().deleteRange(range).run();
1585
+ command.run(editor2);
1586
+ }
1587
+ );
1588
+ const wiki = useSuggestion(
1589
+ (s) => ({
1590
+ value: `${s.target}#${s.heading ?? ""}`,
1591
+ label: s.label ?? s.target,
1592
+ description: s.description,
1593
+ icon: /* @__PURE__ */ jsx6(FileText, {})
1594
+ }),
1595
+ (query) => live.current.suggestWikilinks?.(query) ?? [],
1596
+ (editor2, range, item) => {
1597
+ editor2.chain().focus().deleteRange(range).insertContent({
1598
+ type: "wikilink",
1599
+ attrs: {
1600
+ target: item.target,
1601
+ heading: item.heading ?? null,
1602
+ label: item.label ?? null
1603
+ }
1604
+ }).run();
1605
+ }
1606
+ );
1607
+ const extensions = React6.useMemo(
1608
+ () => [
1609
+ ...schemaExtensions({
1610
+ placeholder: labelRef.current("placeholder"),
1611
+ headingPlaceholder: (level) => labelRef.current("placeholderHeading", { level }),
1612
+ togglePlaceholder: labelRef.current("placeholderToggle"),
1613
+ calloutTitles: {
1614
+ note: labelRef.current("calloutNote"),
1615
+ tip: labelRef.current("calloutTip"),
1616
+ important: labelRef.current("calloutImportant"),
1617
+ warning: labelRef.current("calloutWarning"),
1618
+ caution: labelRef.current("calloutCaution")
1619
+ },
1620
+ embed: embedNode(renderEmbed),
1621
+ rawBlock: rawBlockNode(),
1622
+ resolveUrl
1623
+ }),
1624
+ uploadNode(uploadActions),
1625
+ BlockKeys.configure({ openMenu: (start) => setMenuRequest({ start, nonce: Date.now() }) }),
1626
+ suggestionTrigger("slashMenu", "/", slash.bridge),
1627
+ // A Japanese keyboard types the full-width slash; it opens the same menu.
1628
+ suggestionTrigger("slashMenuWide", "\uFF0F", slash.bridge),
1629
+ suggestionTrigger("wikilinkMenu", "[[", wiki.bridge, {
1630
+ allowSpaces: true,
1631
+ allowedPrefixes: null
1632
+ })
1633
+ ],
1634
+ // Built once per editor: labels are read through a ref, host callbacks through `live`.
1635
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1636
+ []
1637
+ );
1638
+ const editor = useEditor({
1639
+ extensions,
1640
+ content: parse2(initial),
1641
+ editable: !disabled && !readOnly,
1642
+ autofocus: autoFocus ? "end" : false,
1643
+ immediatelyRender: false,
1644
+ editorProps: {
1645
+ attributes: {
1646
+ class: "ui-prose ui-block-editor-content",
1647
+ role: "textbox",
1648
+ "aria-multiline": "true",
1649
+ ...id ? { id } : {},
1650
+ ...ariaLabelledby ? { "aria-labelledby": ariaLabelledby } : { "aria-label": ariaLabel ?? label("editor") },
1651
+ ...ariaDescribedby ? { "aria-describedby": ariaDescribedby } : {}
1652
+ },
1653
+ // Copy writes Markdown, so a block pasted into a plain-text field reads as the body does.
1654
+ clipboardTextSerializer: (slice) => serialize({ type: "doc", content: slice.content.toJSON() }).trimEnd(),
1655
+ handlePaste: (view, event) => {
1656
+ const instance = editorRef.current;
1657
+ if (!instance) return false;
1658
+ const files = [...event.clipboardData?.files ?? []];
1659
+ if (files.length) return startUploads(instance, files);
1660
+ const html = event.clipboardData?.getData("text/html");
1661
+ const text = event.clipboardData?.getData("text/plain");
1662
+ if (!html && text && !view.state.selection.$from.parent.type.spec.code) {
1663
+ const doc = parse2(text);
1664
+ instance.chain().focus().insertContent(doc.content).run();
1665
+ return true;
1666
+ }
1667
+ return false;
1668
+ },
1669
+ handleDrop: (view, event, _slice, moved) => {
1670
+ const instance = editorRef.current;
1671
+ if (moved || !instance) return false;
1672
+ const files = [...event.dataTransfer?.files ?? []];
1673
+ if (!files.length) return false;
1674
+ const at = view.posAtCoords({ left: event.clientX, top: event.clientY })?.pos;
1675
+ event.preventDefault();
1676
+ return startUploads(instance, files, at);
1677
+ }
1678
+ },
1679
+ onUpdate: ({ editor: instance }) => {
1680
+ const markdown = serialize(instance.getJSON());
1681
+ for (const [uploadId, entry] of uploads.current) {
1682
+ if (!findUpload(instance, uploadId)) {
1683
+ entry.controller.abort();
1684
+ uploads.current.delete(uploadId);
1685
+ }
1686
+ }
1687
+ if (markdown === lastMarkdown.current) return;
1688
+ lastMarkdown.current = markdown;
1689
+ live.current.onValueChange?.(markdown);
1690
+ }
1691
+ });
1692
+ editorRef.current = editor;
1693
+ React6.useEffect(() => {
1694
+ if (!editor || valueProp === void 0) return;
1695
+ const next = normalize(valueProp);
1696
+ if (next === lastMarkdown.current || editor.view.composing) return;
1697
+ lastMarkdown.current = next;
1698
+ editor.commands.setContent(parse2(valueProp), { emitUpdate: false });
1699
+ }, [editor, valueProp]);
1700
+ React6.useEffect(() => {
1701
+ editor?.setEditable(!disabled && !readOnly);
1702
+ }, [editor, disabled, readOnly]);
1703
+ React6.useImperativeHandle(
1704
+ ref,
1705
+ () => ({
1706
+ focus: () => editor?.commands.focus(),
1707
+ markdown: () => editor ? serialize(editor.getJSON()) : lastMarkdown.current,
1708
+ editor
1709
+ }),
1710
+ [editor]
1711
+ );
1712
+ const openSlashBelow = (start) => {
1713
+ if (!editor) return;
1714
+ if (insertParagraphAfter(editor, start) == null) return;
1715
+ editor.chain().focus().insertContent("/").run();
1716
+ };
1717
+ const menuItems = (state) => (state?.items ?? []).map((c) => ({ value: c.key, label: c.label, icon: c.icon }));
1718
+ return /* @__PURE__ */ jsx6(LabelsContext.Provider, { value: label, children: /* @__PURE__ */ jsxs5(
1719
+ "div",
1720
+ {
1721
+ ref: (node) => {
1722
+ hostRef.current = node;
1723
+ setHost(node);
1724
+ },
1725
+ className: ["ui-block-editor", className].filter(Boolean).join(" "),
1726
+ "data-disabled": disabled ? "" : void 0,
1727
+ children: [
1728
+ editor ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1729
+ /* @__PURE__ */ jsx6(
1730
+ BlockHandle,
1731
+ {
1732
+ editor,
1733
+ host,
1734
+ turnInto,
1735
+ onAddBelow: openSlashBelow,
1736
+ menuRequest
1737
+ }
1738
+ ),
1739
+ /* @__PURE__ */ jsx6(FormatToolbar, { editor })
1740
+ ] }) : null,
1741
+ /* @__PURE__ */ jsx6(EditorContent, { editor }),
1742
+ /* @__PURE__ */ jsx6(
1743
+ SuggestionMenu,
1744
+ {
1745
+ open: slash.menu != null,
1746
+ rect: slash.menu?.rect ?? null,
1747
+ host,
1748
+ items: menuItems(slash.menu),
1749
+ active: slash.menu?.active,
1750
+ onActiveChange: (active) => slash.setMenu((m) => m ? { ...m, active } : m),
1751
+ onPick: (key) => {
1752
+ const item = slash.menu?.items.find((c) => c.key === key);
1753
+ if (item) slash.menu?.command(item);
1754
+ },
1755
+ onDismiss: () => slash.setMenu(null),
1756
+ label: label("blocks"),
1757
+ empty: label("noResults"),
1758
+ countLabel: t("dataEntry.chatSuggestion.count", {
1759
+ count: slash.menu?.items.length ?? 0
1760
+ }),
1761
+ editable: editor?.view.dom ?? null
1762
+ }
1763
+ ),
1764
+ /* @__PURE__ */ jsx6(
1765
+ SuggestionMenu,
1766
+ {
1767
+ open: wiki.menu != null,
1768
+ rect: wiki.menu?.rect ?? null,
1769
+ host,
1770
+ items: (wiki.menu?.items ?? []).map((s) => ({
1771
+ value: `${s.target}#${s.heading ?? ""}`,
1772
+ label: s.label ?? s.target,
1773
+ description: s.description,
1774
+ icon: /* @__PURE__ */ jsx6(FileText, {})
1775
+ })),
1776
+ active: wiki.menu?.active,
1777
+ onActiveChange: (active) => wiki.setMenu((m) => m ? { ...m, active } : m),
1778
+ onPick: (value) => {
1779
+ const item = wiki.menu?.items.find((s) => `${s.target}#${s.heading ?? ""}` === value);
1780
+ if (item) wiki.menu?.command(item);
1781
+ },
1782
+ onDismiss: () => wiki.setMenu(null),
1783
+ label: label("link"),
1784
+ empty: label("noResults"),
1785
+ countLabel: t("dataEntry.chatSuggestion.count", {
1786
+ count: wiki.menu?.items.length ?? 0
1787
+ }),
1788
+ editable: editor?.view.dom ?? null
1789
+ }
1790
+ ),
1791
+ status ? /* @__PURE__ */ jsx6(Text2, { size: "sm", tone: "destructive", role: "alert", className: "ui-block-editor-status", children: status }) : null,
1792
+ canInsertImage && !pickMedia ? /* @__PURE__ */ jsx6(
1793
+ "input",
1794
+ {
1795
+ ref: fileInput,
1796
+ type: "file",
1797
+ accept: "image/*",
1798
+ hidden: true,
1799
+ "aria-hidden": "true",
1800
+ tabIndex: -1,
1801
+ onChange: (event) => {
1802
+ const files = [...event.currentTarget.files ?? []];
1803
+ event.currentTarget.value = "";
1804
+ if (editor) startUploads(editor, files);
1805
+ }
1806
+ }
1807
+ ) : null
1808
+ ]
1809
+ }
1810
+ ) });
1811
+ }
1812
+ );
1813
+ export {
1814
+ BLOCK_EDITOR_MESSAGES,
1815
+ BlockEditor,
1816
+ schemaExtensions
1817
+ };