@godxjp/block-editor 31.27.0 → 31.28.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.d.ts CHANGED
@@ -59,6 +59,27 @@ declare const BLOCK_EDITOR_MESSAGES: {
59
59
  readonly calloutImportant: "重要";
60
60
  readonly calloutWarning: "警告";
61
61
  readonly calloutCaution: "注意";
62
+ readonly calloutKind: "コールアウトの種類";
63
+ readonly calloutTitle: "コールアウトの見出し";
64
+ readonly addColumn: "列を追加";
65
+ readonly removeColumn: "この列を削除";
66
+ readonly columnWidth: "この列の幅";
67
+ readonly widthAuto: "自動";
68
+ readonly expandToggle: "開く";
69
+ readonly collapseToggle: "閉じる";
70
+ readonly editLink: "リンクを編集";
71
+ readonly linkText: "表示テキスト";
72
+ readonly linkTitle: "タイトル(ツールチップ)";
73
+ readonly openLink: "リンクを開く";
74
+ readonly save: "保存";
75
+ readonly editImage: "画像を編集";
76
+ readonly imageSrc: "画像のURL";
77
+ readonly imageAlt: "代替テキスト";
78
+ readonly imageAltHint: "画像が見えない人のための説明。装飾だけの画像なら空にします。";
79
+ readonly imageTitle: "タイトル(ツールチップ)";
80
+ readonly replaceImage: "画像を差し替え";
81
+ readonly removeImage: "画像を削除";
82
+ readonly properties: "プロパティ(YAML)";
62
83
  };
63
84
  readonly en: {
64
85
  readonly editor: "Editor";
@@ -112,6 +133,27 @@ declare const BLOCK_EDITOR_MESSAGES: {
112
133
  readonly calloutImportant: "Important";
113
134
  readonly calloutWarning: "Warning";
114
135
  readonly calloutCaution: "Caution";
136
+ readonly calloutKind: "Callout type";
137
+ readonly calloutTitle: "Callout title";
138
+ readonly addColumn: "Add a column";
139
+ readonly removeColumn: "Remove this column";
140
+ readonly columnWidth: "This column's width";
141
+ readonly widthAuto: "Auto";
142
+ readonly expandToggle: "Expand";
143
+ readonly collapseToggle: "Collapse";
144
+ readonly editLink: "Edit link";
145
+ readonly linkText: "Text";
146
+ readonly linkTitle: "Title (tooltip)";
147
+ readonly openLink: "Open link";
148
+ readonly save: "Save";
149
+ readonly editImage: "Edit image";
150
+ readonly imageSrc: "Image URL";
151
+ readonly imageAlt: "Alternative text";
152
+ readonly imageAltHint: "Describes the image for people who cannot see it. Leave empty for a purely decorative image.";
153
+ readonly imageTitle: "Title (tooltip)";
154
+ readonly replaceImage: "Replace image";
155
+ readonly removeImage: "Remove image";
156
+ readonly properties: "Properties (YAML)";
115
157
  };
116
158
  readonly vi: {
117
159
  readonly editor: "Trình soạn thảo";
@@ -165,6 +207,27 @@ declare const BLOCK_EDITOR_MESSAGES: {
165
207
  readonly calloutImportant: "Quan trọng";
166
208
  readonly calloutWarning: "Cảnh báo";
167
209
  readonly calloutCaution: "Thận trọng";
210
+ readonly calloutKind: "Loại chú thích";
211
+ readonly calloutTitle: "Tiêu đề chú thích";
212
+ readonly addColumn: "Thêm cột";
213
+ readonly removeColumn: "Xoá cột này";
214
+ readonly columnWidth: "Độ rộng cột này";
215
+ readonly widthAuto: "Tự động";
216
+ readonly expandToggle: "Mở";
217
+ readonly collapseToggle: "Thu gọn";
218
+ readonly editLink: "Sửa liên kết";
219
+ readonly linkText: "Văn bản hiển thị";
220
+ readonly linkTitle: "Tiêu đề (chú giải)";
221
+ readonly openLink: "Mở liên kết";
222
+ readonly save: "Lưu";
223
+ readonly editImage: "Sửa ảnh";
224
+ readonly imageSrc: "URL ảnh";
225
+ readonly imageAlt: "Văn bản thay thế";
226
+ readonly imageAltHint: "Mô tả ảnh cho người không nhìn thấy. Để trống nếu ảnh chỉ để trang trí.";
227
+ readonly imageTitle: "Tiêu đề (chú giải)";
228
+ readonly replaceImage: "Thay ảnh";
229
+ readonly removeImage: "Xoá ảnh";
230
+ readonly properties: "Thuộc tính (YAML)";
168
231
  };
169
232
  };
170
233
  type BlockEditorLabels = {
@@ -224,6 +287,11 @@ type BlockEditorProps = {
224
287
  actions?: readonly BlockEditorAction[];
225
288
  /** Override any string the editor renders (defaults: the kit's ja / en / vi catalogue). */
226
289
  labels?: Partial<BlockEditorLabels>;
290
+ /**
291
+ * `framed` (default): a field's border and padding, so the editor reads as an editor inside a
292
+ * form. `plain`: no frame — the borderless Notion-style canvas for a full-page editor.
293
+ */
294
+ appearance?: "framed" | "plain";
227
295
  disabled?: boolean;
228
296
  readOnly?: boolean;
229
297
  autoFocus?: boolean;
@@ -273,6 +341,11 @@ declare function schemaExtensions(options: {
273
341
  rawBlock?: Node;
274
342
  /** Resolves a stored image URL (`asset:<id>`) to one the browser can load; the doc keeps the original. */
275
343
  resolveUrl?: (url: string) => string | undefined;
344
+ callout?: Node;
345
+ columns?: Node;
346
+ frontmatter?: Node;
347
+ /** The toggle button's accessible name, open and closed. */
348
+ toggleLabel?: (open: boolean) => string;
276
349
  }): Extensions;
277
350
 
278
351
  export { BLOCK_EDITOR_MESSAGES, BlockEditor, type BlockEditorAction, type BlockEditorApi, type BlockEditorHandle, type BlockEditorLabels, type BlockEditorProps, type BlockEditorUploadResult, type CalloutTitles, type WikilinkSuggestion, schemaExtensions };
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/block-editor.tsx
4
4
  import * as React6 from "react";
5
+ import { getMarkRange } from "@tiptap/core";
5
6
  import { EditorContent, useEditor } from "@tiptap/react";
6
7
  import { FileText } from "lucide-react";
7
8
  import { normalize, parse as parse2, serialize } from "@godxjp/markdown/codec";
@@ -20,8 +21,22 @@ function blockStartAt(editor, pos) {
20
21
  }
21
22
  return $pos.before(1);
22
23
  }
24
+ function liveSelection(editor) {
25
+ let { from, to } = editor.state.selection;
26
+ const dom = editor.view.dom.ownerDocument.getSelection();
27
+ if (dom?.anchorNode && dom.focusNode && editor.view.dom.contains(dom.anchorNode)) {
28
+ try {
29
+ const anchor = editor.view.posAtDOM(dom.anchorNode, dom.anchorOffset);
30
+ const head = editor.view.posAtDOM(dom.focusNode, dom.focusOffset);
31
+ from = Math.min(anchor, head);
32
+ to = Math.max(anchor, head);
33
+ } catch {
34
+ }
35
+ }
36
+ return { from, to };
37
+ }
23
38
  function selectionBlockStart(editor) {
24
- return blockStartAt(editor, editor.state.selection.from);
39
+ return blockStartAt(editor, liveSelection(editor).from);
25
40
  }
26
41
  function nodeAt(editor, start) {
27
42
  const node = editor.state.doc.nodeAt(start);
@@ -88,7 +103,7 @@ function startBlockDrag(editor, start, event) {
88
103
  var BlockKeys = Extension.create({
89
104
  name: "blockKeys",
90
105
  addOptions() {
91
- return { openMenu: () => void 0 };
106
+ return { openMenu: () => void 0, editLink: () => void 0 };
92
107
  },
93
108
  addKeyboardShortcuts() {
94
109
  const move = (direction) => () => {
@@ -98,6 +113,11 @@ var BlockKeys = Extension.create({
98
113
  return {
99
114
  "Alt-Shift-ArrowUp": move(-1),
100
115
  "Alt-Shift-ArrowDown": move(1),
116
+ // The link drawer for the link under the caret, or a new link on the selection.
117
+ "Mod-k": () => {
118
+ this.options.editLink();
119
+ return true;
120
+ },
101
121
  "Mod-/": () => {
102
122
  const start = selectionBlockStart(this.editor);
103
123
  if (start == null) return false;
@@ -575,6 +595,27 @@ var Embed = Node.create({
575
595
  ];
576
596
  }
577
597
  });
598
+ var Frontmatter = Node.create({
599
+ name: "frontmatter",
600
+ group: "block",
601
+ atom: true,
602
+ addAttributes() {
603
+ return { source: { default: "" } };
604
+ },
605
+ parseHTML() {
606
+ return [{ tag: "pre[data-frontmatter]" }];
607
+ },
608
+ renderHTML({ node, HTMLAttributes }) {
609
+ return [
610
+ "pre",
611
+ mergeAttributes(HTMLAttributes, {
612
+ "data-frontmatter": "",
613
+ class: "ui-block-editor-raw-block"
614
+ }),
615
+ node.attrs.source
616
+ ];
617
+ }
618
+ });
578
619
  var RawBlock = Node.create({
579
620
  name: "rawBlock",
580
621
  group: "block",
@@ -623,17 +664,28 @@ function schemaExtensions(options) {
623
664
  ];
624
665
  }
625
666
  }).configure({ inline: true, allowBase64: false }),
626
- Details.configure({ persist: false }),
667
+ Details.configure({
668
+ persist: false,
669
+ renderToggleButton: ({ element, isOpen }) => {
670
+ element.className = "ui-block-editor-toggle-button";
671
+ element.setAttribute("aria-expanded", String(isOpen));
672
+ element.setAttribute(
673
+ "aria-label",
674
+ options.toggleLabel?.(isOpen) ?? (isOpen ? "Collapse" : "Expand")
675
+ );
676
+ }
677
+ }),
627
678
  DetailsSummary,
628
679
  DetailsContent,
629
- Callout.configure({ titles: options.calloutTitles }),
630
- Columns,
680
+ (options.callout ?? Callout).configure({ titles: options.calloutTitles }),
681
+ options.columns ?? Columns,
631
682
  Column,
632
683
  Wikilink,
633
684
  EmbedInline,
634
685
  RawInline,
635
686
  options.embed ?? Embed,
636
687
  options.rawBlock ?? RawBlock,
688
+ options.frontmatter ?? Frontmatter,
637
689
  Placeholder.configure({
638
690
  includeChildren: true,
639
691
  placeholder: ({ node }) => {
@@ -860,7 +912,28 @@ var BLOCK_EDITOR_MESSAGES = {
860
912
  calloutTip: "\u30D2\u30F3\u30C8",
861
913
  calloutImportant: "\u91CD\u8981",
862
914
  calloutWarning: "\u8B66\u544A",
863
- calloutCaution: "\u6CE8\u610F"
915
+ calloutCaution: "\u6CE8\u610F",
916
+ calloutKind: "\u30B3\u30FC\u30EB\u30A2\u30A6\u30C8\u306E\u7A2E\u985E",
917
+ calloutTitle: "\u30B3\u30FC\u30EB\u30A2\u30A6\u30C8\u306E\u898B\u51FA\u3057",
918
+ addColumn: "\u5217\u3092\u8FFD\u52A0",
919
+ removeColumn: "\u3053\u306E\u5217\u3092\u524A\u9664",
920
+ columnWidth: "\u3053\u306E\u5217\u306E\u5E45",
921
+ widthAuto: "\u81EA\u52D5",
922
+ expandToggle: "\u958B\u304F",
923
+ collapseToggle: "\u9589\u3058\u308B",
924
+ editLink: "\u30EA\u30F3\u30AF\u3092\u7DE8\u96C6",
925
+ linkText: "\u8868\u793A\u30C6\u30AD\u30B9\u30C8",
926
+ linkTitle: "\u30BF\u30A4\u30C8\u30EB\uFF08\u30C4\u30FC\u30EB\u30C1\u30C3\u30D7\uFF09",
927
+ openLink: "\u30EA\u30F3\u30AF\u3092\u958B\u304F",
928
+ save: "\u4FDD\u5B58",
929
+ editImage: "\u753B\u50CF\u3092\u7DE8\u96C6",
930
+ imageSrc: "\u753B\u50CF\u306EURL",
931
+ imageAlt: "\u4EE3\u66FF\u30C6\u30AD\u30B9\u30C8",
932
+ imageAltHint: "\u753B\u50CF\u304C\u898B\u3048\u306A\u3044\u4EBA\u306E\u305F\u3081\u306E\u8AAC\u660E\u3002\u88C5\u98FE\u3060\u3051\u306E\u753B\u50CF\u306A\u3089\u7A7A\u306B\u3057\u307E\u3059\u3002",
933
+ imageTitle: "\u30BF\u30A4\u30C8\u30EB\uFF08\u30C4\u30FC\u30EB\u30C1\u30C3\u30D7\uFF09",
934
+ replaceImage: "\u753B\u50CF\u3092\u5DEE\u3057\u66FF\u3048",
935
+ removeImage: "\u753B\u50CF\u3092\u524A\u9664",
936
+ properties: "\u30D7\u30ED\u30D1\u30C6\u30A3\uFF08YAML\uFF09"
864
937
  },
865
938
  en: {
866
939
  editor: "Editor",
@@ -913,7 +986,28 @@ var BLOCK_EDITOR_MESSAGES = {
913
986
  calloutTip: "Tip",
914
987
  calloutImportant: "Important",
915
988
  calloutWarning: "Warning",
916
- calloutCaution: "Caution"
989
+ calloutCaution: "Caution",
990
+ calloutKind: "Callout type",
991
+ calloutTitle: "Callout title",
992
+ addColumn: "Add a column",
993
+ removeColumn: "Remove this column",
994
+ columnWidth: "This column's width",
995
+ widthAuto: "Auto",
996
+ expandToggle: "Expand",
997
+ collapseToggle: "Collapse",
998
+ editLink: "Edit link",
999
+ linkText: "Text",
1000
+ linkTitle: "Title (tooltip)",
1001
+ openLink: "Open link",
1002
+ save: "Save",
1003
+ editImage: "Edit image",
1004
+ imageSrc: "Image URL",
1005
+ imageAlt: "Alternative text",
1006
+ imageAltHint: "Describes the image for people who cannot see it. Leave empty for a purely decorative image.",
1007
+ imageTitle: "Title (tooltip)",
1008
+ replaceImage: "Replace image",
1009
+ removeImage: "Remove image",
1010
+ properties: "Properties (YAML)"
917
1011
  },
918
1012
  vi: {
919
1013
  editor: "Tr\xECnh so\u1EA1n th\u1EA3o",
@@ -966,7 +1060,28 @@ var BLOCK_EDITOR_MESSAGES = {
966
1060
  calloutTip: "M\u1EB9o",
967
1061
  calloutImportant: "Quan tr\u1ECDng",
968
1062
  calloutWarning: "C\u1EA3nh b\xE1o",
969
- calloutCaution: "Th\u1EADn tr\u1ECDng"
1063
+ calloutCaution: "Th\u1EADn tr\u1ECDng",
1064
+ calloutKind: "Lo\u1EA1i ch\xFA th\xEDch",
1065
+ calloutTitle: "Ti\xEAu \u0111\u1EC1 ch\xFA th\xEDch",
1066
+ addColumn: "Th\xEAm c\u1ED9t",
1067
+ removeColumn: "Xo\xE1 c\u1ED9t n\xE0y",
1068
+ columnWidth: "\u0110\u1ED9 r\u1ED9ng c\u1ED9t n\xE0y",
1069
+ widthAuto: "T\u1EF1 \u0111\u1ED9ng",
1070
+ expandToggle: "M\u1EDF",
1071
+ collapseToggle: "Thu g\u1ECDn",
1072
+ editLink: "S\u1EEDa li\xEAn k\u1EBFt",
1073
+ linkText: "V\u0103n b\u1EA3n hi\u1EC3n th\u1ECB",
1074
+ linkTitle: "Ti\xEAu \u0111\u1EC1 (ch\xFA gi\u1EA3i)",
1075
+ openLink: "M\u1EDF li\xEAn k\u1EBFt",
1076
+ save: "L\u01B0u",
1077
+ editImage: "S\u1EEDa \u1EA3nh",
1078
+ imageSrc: "URL \u1EA3nh",
1079
+ imageAlt: "V\u0103n b\u1EA3n thay th\u1EBF",
1080
+ imageAltHint: "M\xF4 t\u1EA3 \u1EA3nh cho ng\u01B0\u1EDDi kh\xF4ng nh\xECn th\u1EA5y. \u0110\u1EC3 tr\u1ED1ng n\u1EBFu \u1EA3nh ch\u1EC9 \u0111\u1EC3 trang tr\xED.",
1081
+ imageTitle: "Ti\xEAu \u0111\u1EC1 (ch\xFA gi\u1EA3i)",
1082
+ replaceImage: "Thay \u1EA3nh",
1083
+ removeImage: "Xo\xE1 \u1EA3nh",
1084
+ properties: "Thu\u1ED9c t\xEDnh (YAML)"
970
1085
  }
971
1086
  };
972
1087
  var registered = false;
@@ -1091,18 +1206,15 @@ function SuggestionMenu({
1091
1206
  }
1092
1207
 
1093
1208
  // src/toolbar.tsx
1094
- import * as React4 from "react";
1095
1209
  import { useEditorState } from "@tiptap/react";
1096
1210
  import { BubbleMenu } from "@tiptap/react/menus";
1097
1211
  import { Bold, Code as Code2, Italic, Link2, Strikethrough } from "lucide-react";
1098
- import { Input, ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
1212
+ import { ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
1099
1213
  import { Button as Button2 } from "@godxjp/ui/general";
1100
1214
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1101
1215
  var MARKS = ["bold", "italic", "strike", "code"];
1102
- function FormatToolbar({ editor }) {
1216
+ function FormatToolbar({ editor, onEditLink }) {
1103
1217
  const label = useLabel();
1104
- const [linking, setLinking] = React4.useState(false);
1105
- const [href, setHref] = React4.useState("");
1106
1218
  const state = useEditorState({
1107
1219
  editor,
1108
1220
  selector: ({ editor: e }) => ({
@@ -1122,13 +1234,6 @@ function FormatToolbar({ editor }) {
1122
1234
  }
1123
1235
  chain.run();
1124
1236
  };
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
1237
  return /* @__PURE__ */ jsx4(
1133
1238
  BubbleMenu,
1134
1239
  {
@@ -1136,49 +1241,7 @@ function FormatToolbar({ editor }) {
1136
1241
  className: "ui-block-editor-toolbar",
1137
1242
  shouldShow: ({ editor: e, state: s }) => e.isEditable && !s.selection.empty && !e.isActive("codeBlock") && s.selection.content().size > 0,
1138
1243
  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: [
1244
+ children: /* @__PURE__ */ jsxs3("div", { className: "ui-block-editor-toolbar-row", children: [
1182
1245
  /* @__PURE__ */ jsxs3(
1183
1246
  ToggleGroup,
1184
1247
  {
@@ -1202,10 +1265,7 @@ function FormatToolbar({ editor }) {
1202
1265
  variant: state.link ? "secondary" : "ghost",
1203
1266
  "aria-label": label("link"),
1204
1267
  "aria-pressed": state.link != null,
1205
- onClick: () => {
1206
- setHref(state.link ?? "");
1207
- setLinking(true);
1208
- },
1268
+ onClick: onEditLink,
1209
1269
  children: /* @__PURE__ */ jsx4(Link2, {})
1210
1270
  }
1211
1271
  )
@@ -1214,15 +1274,163 @@ function FormatToolbar({ editor }) {
1214
1274
  );
1215
1275
  }
1216
1276
 
1277
+ // src/edit-drawer.tsx
1278
+ import * as React4 from "react";
1279
+ import { ExternalLink, ImageUp, Trash2 as Trash22 } from "lucide-react";
1280
+ import { Image as Image2 } from "@godxjp/ui/data-display";
1281
+ import { FormField, Input } from "@godxjp/ui/data-entry";
1282
+ import { Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader } from "@godxjp/ui/feedback";
1283
+ import { Button as Button3 } from "@godxjp/ui/general";
1284
+ import { Flex } from "@godxjp/ui/layout";
1285
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1286
+ function EditDrawer({
1287
+ editor,
1288
+ target,
1289
+ onClose,
1290
+ resolveUrl,
1291
+ replaceImage
1292
+ }) {
1293
+ const label = useLabel();
1294
+ const [draft, setDraft] = React4.useState(target);
1295
+ React4.useEffect(() => setDraft(target), [target]);
1296
+ if (!draft) return /* @__PURE__ */ jsx5(Sheet, { open: false, onOpenChange: () => void 0 });
1297
+ const set = (patch) => setDraft((d) => d ? { ...d, ...patch } : d);
1298
+ const save = () => {
1299
+ if (draft.kind === "link") {
1300
+ const href = draft.href.trim();
1301
+ const text = draft.text === "" ? href : draft.text;
1302
+ const chain = editor.chain().focus();
1303
+ if (!href) chain.setTextSelection({ from: draft.from, to: draft.to }).unsetLink();
1304
+ else
1305
+ chain.insertContentAt(
1306
+ { from: draft.from, to: draft.to },
1307
+ {
1308
+ type: "text",
1309
+ text,
1310
+ marks: [{ type: "link", attrs: { href, title: draft.title.trim() || null } }]
1311
+ }
1312
+ );
1313
+ chain.run();
1314
+ } else {
1315
+ const { tr } = editor.state;
1316
+ tr.setNodeMarkup(draft.pos, void 0, {
1317
+ ...editor.state.doc.nodeAt(draft.pos)?.attrs,
1318
+ src: draft.src.trim(),
1319
+ alt: draft.alt,
1320
+ title: draft.title.trim() || null
1321
+ });
1322
+ editor.view.dispatch(tr);
1323
+ }
1324
+ onClose();
1325
+ };
1326
+ const remove = () => {
1327
+ if (draft.kind === "link") {
1328
+ editor.chain().focus().setTextSelection({ from: draft.from, to: draft.to }).unsetLink().run();
1329
+ } else {
1330
+ const node = editor.state.doc.nodeAt(draft.pos);
1331
+ if (node)
1332
+ editor.chain().focus().deleteRange({ from: draft.pos, to: draft.pos + node.nodeSize }).run();
1333
+ }
1334
+ onClose();
1335
+ };
1336
+ const preview = draft.kind === "image" ? resolveUrl?.(draft.src) ?? draft.src : "";
1337
+ return /* @__PURE__ */ jsx5(Sheet, { open: true, onOpenChange: (open) => open ? void 0 : onClose(), children: /* @__PURE__ */ jsxs4(SheetContent, { side: "right", responsive: "auto", className: "ui-block-editor-drawer", children: [
1338
+ /* @__PURE__ */ jsx5(SheetHeader, { title: label(draft.kind === "link" ? "editLink" : "editImage") }),
1339
+ /* @__PURE__ */ jsx5(SheetBody, { children: /* @__PURE__ */ jsx5(
1340
+ "form",
1341
+ {
1342
+ id: "block-editor-edit",
1343
+ onSubmit: (event) => {
1344
+ event.preventDefault();
1345
+ save();
1346
+ },
1347
+ children: /* @__PURE__ */ jsx5(Flex, { direction: "col", gap: "md", children: draft.kind === "link" ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1348
+ /* @__PURE__ */ jsx5(FormField, { id: "block-editor-link-text", label: label("linkText"), children: /* @__PURE__ */ jsx5(Input, { value: draft.text, onValueChange: (text) => set({ text }) }) }),
1349
+ /* @__PURE__ */ jsx5(FormField, { id: "block-editor-link-href", label: label("linkUrl"), required: true, children: /* @__PURE__ */ jsx5(
1350
+ Input,
1351
+ {
1352
+ type: "url",
1353
+ inputMode: "url",
1354
+ value: draft.href,
1355
+ onValueChange: (href) => set({ href }),
1356
+ autoFocus: true
1357
+ }
1358
+ ) }),
1359
+ /* @__PURE__ */ jsx5(FormField, { id: "block-editor-link-title", label: label("linkTitle"), children: /* @__PURE__ */ jsx5(Input, { value: draft.title, onValueChange: (title) => set({ title }) }) })
1360
+ ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
1361
+ preview ? /* @__PURE__ */ jsx5(
1362
+ Image2,
1363
+ {
1364
+ src: preview,
1365
+ alt: draft.alt,
1366
+ className: "ui-block-editor-drawer-preview"
1367
+ }
1368
+ ) : null,
1369
+ /* @__PURE__ */ jsx5(FormField, { id: "block-editor-image-src", label: label("imageSrc"), required: true, children: /* @__PURE__ */ jsx5(Input, { value: draft.src, onValueChange: (src) => set({ src }) }) }),
1370
+ /* @__PURE__ */ jsx5(
1371
+ FormField,
1372
+ {
1373
+ id: "block-editor-image-alt",
1374
+ label: label("imageAlt"),
1375
+ helper: label("imageAltHint"),
1376
+ children: /* @__PURE__ */ jsx5(Input, { value: draft.alt, onValueChange: (alt) => set({ alt }), autoFocus: true })
1377
+ }
1378
+ ),
1379
+ /* @__PURE__ */ jsx5(FormField, { id: "block-editor-image-title", label: label("imageTitle"), children: /* @__PURE__ */ jsx5(Input, { value: draft.title, onValueChange: (title) => set({ title }) }) })
1380
+ ] }) })
1381
+ }
1382
+ ) }),
1383
+ /* @__PURE__ */ jsx5(SheetFooter, { children: /* @__PURE__ */ jsxs4(Flex, { gap: "sm", wrap: true, justify: "between", className: "ui-block-editor-drawer-actions", children: [
1384
+ /* @__PURE__ */ jsxs4(Flex, { gap: "sm", wrap: true, children: [
1385
+ draft.kind === "link" && draft.href.trim() ? /* @__PURE__ */ jsx5(Button3, { variant: "ghost", size: "sm", asChild: true, children: /* @__PURE__ */ jsxs4("a", { href: draft.href.trim(), target: "_blank", rel: "noopener noreferrer", children: [
1386
+ /* @__PURE__ */ jsx5(ExternalLink, { "aria-hidden": "true" }),
1387
+ label("openLink")
1388
+ ] }) }) : null,
1389
+ draft.kind === "image" && replaceImage ? /* @__PURE__ */ jsxs4(
1390
+ Button3,
1391
+ {
1392
+ variant: "ghost",
1393
+ size: "sm",
1394
+ onClick: () => {
1395
+ replaceImage(draft.pos);
1396
+ onClose();
1397
+ },
1398
+ children: [
1399
+ /* @__PURE__ */ jsx5(ImageUp, { "aria-hidden": "true" }),
1400
+ label("replaceImage")
1401
+ ]
1402
+ }
1403
+ ) : null,
1404
+ /* @__PURE__ */ jsxs4(Button3, { variant: "ghost", size: "sm", onClick: remove, children: [
1405
+ /* @__PURE__ */ jsx5(Trash22, { "aria-hidden": "true" }),
1406
+ label(draft.kind === "link" ? "removeLink" : "removeImage")
1407
+ ] })
1408
+ ] }),
1409
+ /* @__PURE__ */ jsx5(Button3, { type: "submit", form: "block-editor-edit", size: "sm", children: label("save") })
1410
+ ] }) })
1411
+ ] }) });
1412
+ }
1413
+
1217
1414
  // src/views.tsx
1218
1415
  import * as React5 from "react";
1219
1416
  import { Node as Node2, mergeAttributes as mergeAttributes2 } from "@tiptap/core";
1220
- import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
1221
- import { LoaderCircle, TriangleAlert } from "lucide-react";
1417
+ import {
1418
+ NodeViewContent,
1419
+ NodeViewWrapper,
1420
+ ReactNodeViewRenderer
1421
+ } from "@tiptap/react";
1422
+ import { ChevronDown, LoaderCircle, TriangleAlert } from "lucide-react";
1222
1423
  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";
1424
+ import { Input as Input2, Select, Textarea } from "@godxjp/ui/data-entry";
1425
+ import {
1426
+ DropdownMenu as DropdownMenu2,
1427
+ DropdownMenuContent as DropdownMenuContent2,
1428
+ DropdownMenuRadioGroup,
1429
+ DropdownMenuRadioItem,
1430
+ DropdownMenuTrigger as DropdownMenuTrigger2
1431
+ } from "@godxjp/ui/navigation";
1432
+ import { Button as Button4, Text } from "@godxjp/ui/general";
1433
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1226
1434
  function RawBlockView({ node, editor, getPos, updateAttributes, selected }) {
1227
1435
  const tr = useLabel();
1228
1436
  const [editing, setEditing] = React5.useState(false);
@@ -1238,16 +1446,16 @@ function RawBlockView({ node, editor, getPos, updateAttributes, selected }) {
1238
1446
  }
1239
1447
  editor.chain().focus().insertContentAt({ from: pos, to: pos + node.nodeSize }, doc.content).run();
1240
1448
  };
1241
- return /* @__PURE__ */ jsxs4(
1449
+ return /* @__PURE__ */ jsxs5(
1242
1450
  NodeViewWrapper,
1243
1451
  {
1244
1452
  className: "ui-block-editor-raw-block",
1245
1453
  "data-selected": selected ? "" : void 0,
1246
1454
  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,
1455
+ /* @__PURE__ */ jsxs5("div", { className: "ui-block-editor-raw-block-head", contentEditable: false, children: [
1456
+ /* @__PURE__ */ jsx6(Text, { size: "xs", tone: "muted", children: tr("rawBlock") }),
1457
+ editor.isEditable ? /* @__PURE__ */ jsx6(
1458
+ Button4,
1251
1459
  {
1252
1460
  size: "xs",
1253
1461
  variant: "ghost",
@@ -1256,7 +1464,7 @@ function RawBlockView({ node, editor, getPos, updateAttributes, selected }) {
1256
1464
  }
1257
1465
  ) : null
1258
1466
  ] }),
1259
- editing ? /* @__PURE__ */ jsx5(
1467
+ editing ? /* @__PURE__ */ jsx6(
1260
1468
  Textarea,
1261
1469
  {
1262
1470
  "aria-label": tr("rawBlock"),
@@ -1269,7 +1477,7 @@ function RawBlockView({ node, editor, getPos, updateAttributes, selected }) {
1269
1477
  },
1270
1478
  autoFocus: true
1271
1479
  }
1272
- ) : /* @__PURE__ */ jsx5("pre", { className: "ui-block-editor-raw-block-source", contentEditable: false, children: node.attrs.source })
1480
+ ) : /* @__PURE__ */ jsx6("pre", { className: "ui-block-editor-raw-block-source", contentEditable: false, children: node.attrs.source })
1273
1481
  ]
1274
1482
  }
1275
1483
  );
@@ -1277,17 +1485,65 @@ function RawBlockView({ node, editor, getPos, updateAttributes, selected }) {
1277
1485
  function rawBlockNode() {
1278
1486
  return RawBlock.extend({ addNodeView: () => ReactNodeViewRenderer(RawBlockView) });
1279
1487
  }
1488
+ function FrontmatterView({ node, updateAttributes, editor, selected }) {
1489
+ const label = useLabel();
1490
+ const [editing, setEditing] = React5.useState(false);
1491
+ const [draft, setDraft] = React5.useState(node.attrs.source);
1492
+ const commit = () => {
1493
+ setEditing(false);
1494
+ updateAttributes({ source: draft });
1495
+ };
1496
+ return /* @__PURE__ */ jsxs5(
1497
+ NodeViewWrapper,
1498
+ {
1499
+ className: "ui-block-editor-raw-block",
1500
+ "data-frontmatter": "",
1501
+ "data-selected": selected ? "" : void 0,
1502
+ children: [
1503
+ /* @__PURE__ */ jsxs5("div", { className: "ui-block-editor-raw-block-head", contentEditable: false, children: [
1504
+ /* @__PURE__ */ jsx6(Text, { size: "xs", tone: "muted", children: label("properties") }),
1505
+ editor.isEditable ? /* @__PURE__ */ jsx6(
1506
+ Button4,
1507
+ {
1508
+ size: "xs",
1509
+ variant: "ghost",
1510
+ onClick: () => editing ? commit() : (setDraft(node.attrs.source), setEditing(true)),
1511
+ children: editing ? label("done") : label("editSource")
1512
+ }
1513
+ ) : null
1514
+ ] }),
1515
+ editing ? /* @__PURE__ */ jsx6(
1516
+ Textarea,
1517
+ {
1518
+ "aria-label": label("properties"),
1519
+ value: draft,
1520
+ rows: Math.min(12, Math.max(3, draft.split("\n").length)),
1521
+ onValueChange: setDraft,
1522
+ onKeyDown: (event) => {
1523
+ event.stopPropagation();
1524
+ if (event.key === "Escape") commit();
1525
+ },
1526
+ autoFocus: true
1527
+ }
1528
+ ) : /* @__PURE__ */ jsx6("pre", { className: "ui-block-editor-raw-block-source", contentEditable: false, children: node.attrs.source })
1529
+ ]
1530
+ }
1531
+ );
1532
+ }
1533
+ function frontmatterNode() {
1534
+ return Frontmatter.extend({ addNodeView: () => ReactNodeViewRenderer(FrontmatterView) });
1535
+ }
1280
1536
  function embedNode(render) {
1281
1537
  function EmbedView({ node, selected }) {
1282
1538
  const tr = useLabel();
1283
1539
  const target = node.attrs.target;
1284
- return /* @__PURE__ */ jsx5(
1540
+ return /* @__PURE__ */ jsx6(
1285
1541
  NodeViewWrapper,
1286
1542
  {
1287
1543
  className: "ui-block-editor-embed",
1288
1544
  "data-selected": selected ? "" : void 0,
1289
1545
  contentEditable: false,
1290
- children: render ? render(target) : /* @__PURE__ */ jsx5(Text, { size: "sm", tone: "muted", children: tr("embed", { target }) })
1546
+ children: render ? render(target) : /* @__PURE__ */ jsx6(Text, { size: "sm", tone: "muted", children: tr("embed", { target }) })
1291
1547
  }
1292
1548
  );
1293
1549
  }
@@ -1300,21 +1556,21 @@ function uploadNode(actions) {
1300
1556
  return (
1301
1557
  // A plain span, not NodeViewWrapper: Tiptap's wrapper spreads its `as` prop onto the DOM as an
1302
1558
  // invalid attribute, and this inline view needs nothing else it adds (no drag).
1303
- /* @__PURE__ */ jsx5(
1559
+ /* @__PURE__ */ jsx6(
1304
1560
  "span",
1305
1561
  {
1306
1562
  "data-node-view-wrapper": "",
1307
1563
  className: "ui-block-editor-upload",
1308
1564
  "data-status": status,
1309
1565
  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") })
1566
+ children: status === "uploading" ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1567
+ /* @__PURE__ */ jsx6(LoaderCircle, { className: "ui-block-editor-upload-spinner", "aria-hidden": "true" }),
1568
+ /* @__PURE__ */ jsx6(Text, { as: "span", size: "sm", tone: "muted", role: "status", children: tr("uploading", { name }) })
1569
+ ] }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
1570
+ /* @__PURE__ */ jsx6(TriangleAlert, { "aria-hidden": "true" }),
1571
+ /* @__PURE__ */ jsx6(Text, { as: "span", size: "sm", tone: "destructive", role: "alert", children: tr("uploadFailed", { name }) }),
1572
+ /* @__PURE__ */ jsx6(Button4, { size: "xs", variant: "ghost", onClick: () => actions.retry(id), children: tr("retry") }),
1573
+ /* @__PURE__ */ jsx6(Button4, { size: "xs", variant: "ghost", onClick: () => actions.remove(id), children: tr("remove") })
1318
1574
  ] })
1319
1575
  }
1320
1576
  )
@@ -1338,9 +1594,143 @@ function uploadNode(actions) {
1338
1594
  addNodeView: () => ReactNodeViewRenderer(UploadView, { as: "span" })
1339
1595
  });
1340
1596
  }
1597
+ var CALLOUT_KINDS = ["note", "tip", "important", "warning", "caution"];
1598
+ var kindLabelKey = {
1599
+ note: "calloutNote",
1600
+ tip: "calloutTip",
1601
+ important: "calloutImportant",
1602
+ warning: "calloutWarning",
1603
+ caution: "calloutCaution"
1604
+ };
1605
+ function CalloutView({ node, updateAttributes, editor }) {
1606
+ const label = useLabel();
1607
+ const kind = node.attrs.kind;
1608
+ const title = node.attrs.title ?? "";
1609
+ const defaultTitle = label(kindLabelKey[kind] ?? "calloutNote");
1610
+ return /* @__PURE__ */ jsxs5(NodeViewWrapper, { className: "ui-prose-callout", "data-type": "callout", "data-kind": kind, role: "note", children: [
1611
+ editor.isEditable ? /* @__PURE__ */ jsxs5("div", { className: "ui-block-editor-callout-head", contentEditable: false, children: [
1612
+ /* @__PURE__ */ jsxs5(DropdownMenu2, { children: [
1613
+ /* @__PURE__ */ jsx6(DropdownMenuTrigger2, { asChild: true, children: /* @__PURE__ */ jsxs5(
1614
+ Button4,
1615
+ {
1616
+ size: "xs",
1617
+ variant: "ghost",
1618
+ "aria-label": `${label("calloutKind")}: ${defaultTitle}`,
1619
+ children: [
1620
+ defaultTitle,
1621
+ /* @__PURE__ */ jsx6(ChevronDown, { "aria-hidden": "true" })
1622
+ ]
1623
+ }
1624
+ ) }),
1625
+ /* @__PURE__ */ jsx6(DropdownMenuContent2, { align: "start", "aria-label": label("calloutKind"), children: /* @__PURE__ */ jsx6(
1626
+ DropdownMenuRadioGroup,
1627
+ {
1628
+ value: kind,
1629
+ onValueChange: (next) => updateAttributes({ kind: next }),
1630
+ children: CALLOUT_KINDS.map((k) => /* @__PURE__ */ jsx6(DropdownMenuRadioItem, { value: k, children: label(kindLabelKey[k]) }, k))
1631
+ }
1632
+ ) })
1633
+ ] }),
1634
+ /* @__PURE__ */ jsx6(
1635
+ Input2,
1636
+ {
1637
+ size: "sm",
1638
+ className: "ui-block-editor-callout-title",
1639
+ "aria-label": label("calloutTitle"),
1640
+ placeholder: defaultTitle,
1641
+ value: title,
1642
+ onValueChange: (next) => updateAttributes({ title: next.trim() === "" ? null : next })
1643
+ }
1644
+ )
1645
+ ] }) : /* @__PURE__ */ jsx6("p", { className: "ui-prose-callout-title", contentEditable: false, children: title || defaultTitle }),
1646
+ /* @__PURE__ */ jsx6(NodeViewContent, { className: "ui-block-editor-callout-body" })
1647
+ ] });
1648
+ }
1649
+ function calloutNode(base) {
1650
+ return base.extend({ addNodeView: () => ReactNodeViewRenderer(CalloutView) });
1651
+ }
1652
+ var WIDTHS = [25, 33, 50, 67, 75];
1653
+ function ColumnsView({ node, editor, getPos }) {
1654
+ const label = useLabel();
1655
+ const [current, setCurrent] = React5.useState(null);
1656
+ React5.useEffect(() => {
1657
+ const read = () => {
1658
+ const pos = getPos();
1659
+ if (typeof pos !== "number") return setCurrent(null);
1660
+ const { $from } = editor.state.selection;
1661
+ for (let depth = $from.depth; depth > 0; depth--) {
1662
+ if ($from.node(depth).type.name === "columns" && $from.before(depth) === pos) {
1663
+ return setCurrent($from.index(depth));
1664
+ }
1665
+ }
1666
+ setCurrent(null);
1667
+ };
1668
+ read();
1669
+ editor.on("selectionUpdate", read);
1670
+ editor.on("update", read);
1671
+ return () => {
1672
+ editor.off("selectionUpdate", read);
1673
+ editor.off("update", read);
1674
+ };
1675
+ }, [editor, getPos]);
1676
+ const columnPos = (index) => {
1677
+ const pos = getPos();
1678
+ if (typeof pos !== "number") return null;
1679
+ let at = pos + 1;
1680
+ for (let i = 0; i < index; i++) at += node.child(i).nodeSize;
1681
+ return at;
1682
+ };
1683
+ const addColumn = () => {
1684
+ const pos = getPos();
1685
+ if (typeof pos !== "number" || node.childCount >= 4) return;
1686
+ const end = pos + node.nodeSize - 1;
1687
+ const column = editor.schema.nodes.column.create(
1688
+ null,
1689
+ editor.schema.nodes.paragraph.create()
1690
+ );
1691
+ editor.view.dispatch(editor.state.tr.insert(end, column));
1692
+ };
1693
+ const removeColumn = () => {
1694
+ if (current == null || node.childCount <= 2) return;
1695
+ const at = columnPos(current);
1696
+ if (at == null) return;
1697
+ editor.view.dispatch(editor.state.tr.delete(at, at + node.child(current).nodeSize));
1698
+ };
1699
+ const setWidth = (value) => {
1700
+ if (current == null) return;
1701
+ const at = columnPos(current);
1702
+ if (at == null) return;
1703
+ const width2 = value === "auto" ? null : Number(value);
1704
+ editor.view.dispatch(editor.state.tr.setNodeAttribute(at, "width", width2));
1705
+ };
1706
+ const width = current == null ? null : node.child(current).attrs.width ?? null;
1707
+ return /* @__PURE__ */ jsxs5(NodeViewWrapper, { className: "ui-block-editor-columns", "data-type": "columns", children: [
1708
+ editor.isEditable && current != null ? /* @__PURE__ */ jsxs5("div", { className: "ui-block-editor-columns-bar", contentEditable: false, children: [
1709
+ /* @__PURE__ */ jsx6(Button4, { size: "xs", variant: "ghost", disabled: node.childCount >= 4, onClick: addColumn, children: label("addColumn") }),
1710
+ /* @__PURE__ */ jsx6(Button4, { size: "xs", variant: "ghost", disabled: node.childCount <= 2, onClick: removeColumn, children: label("removeColumn") }),
1711
+ /* @__PURE__ */ jsx6(
1712
+ Select,
1713
+ {
1714
+ size: "xs",
1715
+ "aria-label": label("columnWidth"),
1716
+ value: width == null ? "auto" : String(width),
1717
+ onValueChange: setWidth,
1718
+ options: [
1719
+ { value: "auto", label: label("widthAuto") },
1720
+ ...WIDTHS.map((w) => ({ value: String(w), label: `${w}%` }))
1721
+ ]
1722
+ }
1723
+ )
1724
+ ] }) : null,
1725
+ /* @__PURE__ */ jsx6(NodeViewContent, { className: "ui-block-editor-columns-content" })
1726
+ ] });
1727
+ }
1728
+ function columnsNode(base) {
1729
+ return base.extend({ addNodeView: () => ReactNodeViewRenderer(ColumnsView) });
1730
+ }
1341
1731
 
1342
1732
  // src/block-editor.tsx
1343
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1733
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1344
1734
  function useSuggestion(toMenuItem, items, pick) {
1345
1735
  const [menu, setMenu] = React6.useState(null);
1346
1736
  const menuRef = React6.useRef(menu);
@@ -1402,6 +1792,7 @@ var BlockEditor = React6.forwardRef(
1402
1792
  resolveUrl,
1403
1793
  actions = [],
1404
1794
  labels,
1795
+ appearance = "framed",
1405
1796
  disabled = false,
1406
1797
  readOnly = false,
1407
1798
  autoFocus = false,
@@ -1426,6 +1817,8 @@ var BlockEditor = React6.forwardRef(
1426
1817
  null
1427
1818
  );
1428
1819
  const fileInput = React6.useRef(null);
1820
+ const [editTarget, setEditTarget] = React6.useState(null);
1821
+ const replacing = React6.useRef(null);
1429
1822
  const live = React6.useRef({
1430
1823
  onValueChange,
1431
1824
  upload,
@@ -1547,6 +1940,65 @@ var BlockEditor = React6.forwardRef(
1547
1940
  }
1548
1941
  fileInput.current?.click();
1549
1942
  }, []);
1943
+ const linkTargetAt = React6.useCallback(
1944
+ (editor2, pos, selection) => {
1945
+ const { state } = editor2;
1946
+ const type = state.schema.marks.link;
1947
+ const $pos = state.doc.resolve(pos);
1948
+ const range = getMarkRange($pos, type);
1949
+ if (range) {
1950
+ const mark = $pos.marks().find((m) => m.type === type) ?? state.doc.nodeAt(range.from)?.marks.find((m) => m.type === type);
1951
+ return {
1952
+ kind: "link",
1953
+ from: range.from,
1954
+ to: range.to,
1955
+ text: state.doc.textBetween(range.from, range.to),
1956
+ href: mark?.attrs.href ?? "",
1957
+ title: mark?.attrs.title ?? ""
1958
+ };
1959
+ }
1960
+ if (selection && selection.from !== selection.to) {
1961
+ return {
1962
+ kind: "link",
1963
+ from: selection.from,
1964
+ to: selection.to,
1965
+ text: state.doc.textBetween(selection.from, selection.to),
1966
+ href: "",
1967
+ title: ""
1968
+ };
1969
+ }
1970
+ return null;
1971
+ },
1972
+ []
1973
+ );
1974
+ const editLinkAtSelection = React6.useCallback(() => {
1975
+ const editor2 = editorRef.current;
1976
+ if (!editor2) return;
1977
+ const { from, to } = liveSelection(editor2);
1978
+ const target = linkTargetAt(editor2, from, { from, to });
1979
+ if (target) setEditTarget(target);
1980
+ }, [linkTargetAt]);
1981
+ const replaceImage = React6.useCallback((pos) => {
1982
+ const editor2 = editorRef.current;
1983
+ if (!editor2) return;
1984
+ const pick = live.current.pickMedia;
1985
+ if (pick) {
1986
+ void pick().then((media) => {
1987
+ const node = editor2.state.doc.nodeAt(pos);
1988
+ if (!media || node?.type.name !== "image") return;
1989
+ editor2.view.dispatch(
1990
+ editor2.state.tr.setNodeMarkup(pos, void 0, {
1991
+ ...node.attrs,
1992
+ src: media.url,
1993
+ alt: media.name ?? node.attrs.alt
1994
+ })
1995
+ );
1996
+ });
1997
+ return;
1998
+ }
1999
+ replacing.current = pos;
2000
+ fileInput.current?.click();
2001
+ }, []);
1550
2002
  const canInsertImage = Boolean(pickMedia || upload);
1551
2003
  const commands = React6.useMemo(() => {
1552
2004
  const builtin = builtinCommands(
@@ -1590,7 +2042,7 @@ var BlockEditor = React6.forwardRef(
1590
2042
  value: `${s.target}#${s.heading ?? ""}`,
1591
2043
  label: s.label ?? s.target,
1592
2044
  description: s.description,
1593
- icon: /* @__PURE__ */ jsx6(FileText, {})
2045
+ icon: /* @__PURE__ */ jsx7(FileText, {})
1594
2046
  }),
1595
2047
  (query) => live.current.suggestWikilinks?.(query) ?? [],
1596
2048
  (editor2, range, item) => {
@@ -1619,10 +2071,17 @@ var BlockEditor = React6.forwardRef(
1619
2071
  },
1620
2072
  embed: embedNode(renderEmbed),
1621
2073
  rawBlock: rawBlockNode(),
2074
+ frontmatter: frontmatterNode(),
2075
+ callout: calloutNode(Callout),
2076
+ columns: columnsNode(Columns),
2077
+ toggleLabel: (open) => labelRef.current(open ? "collapseToggle" : "expandToggle"),
1622
2078
  resolveUrl
1623
2079
  }),
1624
2080
  uploadNode(uploadActions),
1625
- BlockKeys.configure({ openMenu: (start) => setMenuRequest({ start, nonce: Date.now() }) }),
2081
+ BlockKeys.configure({
2082
+ openMenu: (start) => setMenuRequest({ start, nonce: Date.now() }),
2083
+ editLink: () => editLinkRef.current()
2084
+ }),
1626
2085
  suggestionTrigger("slashMenu", "/", slash.bridge),
1627
2086
  // A Japanese keyboard types the full-width slash; it opens the same menu.
1628
2087
  suggestionTrigger("slashMenuWide", "\uFF0F", slash.bridge),
@@ -1635,6 +2094,8 @@ var BlockEditor = React6.forwardRef(
1635
2094
  // eslint-disable-next-line react-hooks/exhaustive-deps
1636
2095
  []
1637
2096
  );
2097
+ const editLinkRef = React6.useRef(editLinkAtSelection);
2098
+ editLinkRef.current = editLinkAtSelection;
1638
2099
  const editor = useEditor({
1639
2100
  extensions,
1640
2101
  content: parse2(initial),
@@ -1650,6 +2111,38 @@ var BlockEditor = React6.forwardRef(
1650
2111
  ...ariaLabelledby ? { "aria-labelledby": ariaLabelledby } : { "aria-label": ariaLabel ?? label("editor") },
1651
2112
  ...ariaDescribedby ? { "aria-describedby": ariaDescribedby } : {}
1652
2113
  },
2114
+ // While editing, a link is TEXT, not a way out: the browser follows an <a href> even inside
2115
+ // contenteditable, which left the page mid-edit and lost the work. Cancel the navigation and
2116
+ // let ProseMirror see the click (it opens the link drawer). Read-only, links navigate.
2117
+ handleDOMEvents: {
2118
+ click: (view, event) => {
2119
+ if (view.editable && event.target?.closest?.("a[href]")) {
2120
+ event.preventDefault();
2121
+ }
2122
+ return false;
2123
+ }
2124
+ },
2125
+ // A click on a link or an image opens its settings drawer (text / URL / title; src / alt /
2126
+ // title). Read-only, links behave as links.
2127
+ handleClick: (view, pos) => {
2128
+ const instance = editorRef.current;
2129
+ if (!instance || !view.editable) return false;
2130
+ const target = linkTargetAt(instance, pos);
2131
+ if (!target) return false;
2132
+ setEditTarget(target);
2133
+ return true;
2134
+ },
2135
+ handleClickOn: (view, pos, node) => {
2136
+ if (!view.editable || node.type.name !== "image") return false;
2137
+ setEditTarget({
2138
+ kind: "image",
2139
+ pos,
2140
+ src: node.attrs.src ?? "",
2141
+ alt: node.attrs.alt ?? "",
2142
+ title: node.attrs.title ?? ""
2143
+ });
2144
+ return true;
2145
+ },
1653
2146
  // Copy writes Markdown, so a block pasted into a plain-text field reads as the body does.
1654
2147
  clipboardTextSerializer: (slice) => serialize({ type: "doc", content: slice.content.toJSON() }).trimEnd(),
1655
2148
  handlePaste: (view, event) => {
@@ -1715,7 +2208,7 @@ var BlockEditor = React6.forwardRef(
1715
2208
  editor.chain().focus().insertContent("/").run();
1716
2209
  };
1717
2210
  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(
2211
+ return /* @__PURE__ */ jsx7(LabelsContext.Provider, { value: label, children: /* @__PURE__ */ jsxs6(
1719
2212
  "div",
1720
2213
  {
1721
2214
  ref: (node) => {
@@ -1723,10 +2216,11 @@ var BlockEditor = React6.forwardRef(
1723
2216
  setHost(node);
1724
2217
  },
1725
2218
  className: ["ui-block-editor", className].filter(Boolean).join(" "),
2219
+ "data-appearance": appearance,
1726
2220
  "data-disabled": disabled ? "" : void 0,
1727
2221
  children: [
1728
- editor ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1729
- /* @__PURE__ */ jsx6(
2222
+ editor ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
2223
+ /* @__PURE__ */ jsx7(
1730
2224
  BlockHandle,
1731
2225
  {
1732
2226
  editor,
@@ -1736,10 +2230,20 @@ var BlockEditor = React6.forwardRef(
1736
2230
  menuRequest
1737
2231
  }
1738
2232
  ),
1739
- /* @__PURE__ */ jsx6(FormatToolbar, { editor })
2233
+ /* @__PURE__ */ jsx7(FormatToolbar, { editor, onEditLink: editLinkAtSelection }),
2234
+ /* @__PURE__ */ jsx7(
2235
+ EditDrawer,
2236
+ {
2237
+ editor,
2238
+ target: editTarget,
2239
+ onClose: () => setEditTarget(null),
2240
+ resolveUrl,
2241
+ replaceImage: pickMedia || upload ? replaceImage : null
2242
+ }
2243
+ )
1740
2244
  ] }) : null,
1741
- /* @__PURE__ */ jsx6(EditorContent, { editor }),
1742
- /* @__PURE__ */ jsx6(
2245
+ /* @__PURE__ */ jsx7(EditorContent, { editor }),
2246
+ /* @__PURE__ */ jsx7(
1743
2247
  SuggestionMenu,
1744
2248
  {
1745
2249
  open: slash.menu != null,
@@ -1761,7 +2265,7 @@ var BlockEditor = React6.forwardRef(
1761
2265
  editable: editor?.view.dom ?? null
1762
2266
  }
1763
2267
  ),
1764
- /* @__PURE__ */ jsx6(
2268
+ /* @__PURE__ */ jsx7(
1765
2269
  SuggestionMenu,
1766
2270
  {
1767
2271
  open: wiki.menu != null,
@@ -1771,7 +2275,7 @@ var BlockEditor = React6.forwardRef(
1771
2275
  value: `${s.target}#${s.heading ?? ""}`,
1772
2276
  label: s.label ?? s.target,
1773
2277
  description: s.description,
1774
- icon: /* @__PURE__ */ jsx6(FileText, {})
2278
+ icon: /* @__PURE__ */ jsx7(FileText, {})
1775
2279
  })),
1776
2280
  active: wiki.menu?.active,
1777
2281
  onActiveChange: (active) => wiki.setMenu((m) => m ? { ...m, active } : m),
@@ -1788,8 +2292,8 @@ var BlockEditor = React6.forwardRef(
1788
2292
  editable: editor?.view.dom ?? null
1789
2293
  }
1790
2294
  ),
1791
- status ? /* @__PURE__ */ jsx6(Text2, { size: "sm", tone: "destructive", role: "alert", className: "ui-block-editor-status", children: status }) : null,
1792
- canInsertImage && !pickMedia ? /* @__PURE__ */ jsx6(
2295
+ status ? /* @__PURE__ */ jsx7(Text2, { size: "sm", tone: "destructive", role: "alert", className: "ui-block-editor-status", children: status }) : null,
2296
+ canInsertImage && !pickMedia ? /* @__PURE__ */ jsx7(
1793
2297
  "input",
1794
2298
  {
1795
2299
  ref: fileInput,
@@ -1801,7 +2305,18 @@ var BlockEditor = React6.forwardRef(
1801
2305
  onChange: (event) => {
1802
2306
  const files = [...event.currentTarget.files ?? []];
1803
2307
  event.currentTarget.value = "";
1804
- if (editor) startUploads(editor, files);
2308
+ if (!editor) return;
2309
+ const at = replacing.current;
2310
+ replacing.current = null;
2311
+ if (at != null && files.length) {
2312
+ const node = editor.state.doc.nodeAt(at);
2313
+ if (node?.type.name === "image") {
2314
+ editor.view.dispatch(editor.state.tr.delete(at, at + node.nodeSize));
2315
+ }
2316
+ startUploads(editor, files.slice(0, 1), at);
2317
+ return;
2318
+ }
2319
+ startUploads(editor, files);
1805
2320
  }
1806
2321
  }
1807
2322
  ) : null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/block-editor",
3
- "version": "31.27.0",
3
+ "version": "31.28.0",
4
4
  "description": "Block editor for GoDX apps (Notion / note.com style) on Tiptap: slash menu, block handles, floating format toolbar; reads and writes Markdown through @godxjp/markdown/codec, IME-safe, ja / en / vi.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,8 +45,8 @@
45
45
  "lucide-react": "^1.37.0"
46
46
  },
47
47
  "peerDependencies": {
48
- "@godxjp/markdown": "~31.27.0",
49
- "@godxjp/ui": "~31.27.0",
48
+ "@godxjp/markdown": "~31.28.0",
49
+ "@godxjp/ui": "~31.28.0",
50
50
  "react": ">=19.0.0",
51
51
  "react-dom": ">=19.0.0"
52
52
  },