@sendbird/actionbook-core 0.9.3 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/index.js CHANGED
@@ -14,7 +14,7 @@ var actionbookSchema = new Schema({
14
14
  }
15
15
  },
16
16
  heading: {
17
- content: "inline*",
17
+ content: "(text | jumpPoint | hardBreak)*",
18
18
  group: "block",
19
19
  attrs: { level: { default: 1 } },
20
20
  parseDOM: [
@@ -2734,6 +2734,39 @@ var ITALIC_STAR_RE = /(?:^|[^*])\*([^*]+)\*$/;
2734
2734
  var ITALIC_UNDER_RE = /(?:^|[^_])_([^_]+)_$/;
2735
2735
  var STRIKE_RE = /(?:^|[^~])~~([^~]+)~~$/;
2736
2736
  var CODE_RE = /(?:^|[^`])`([^`]+)`$/;
2737
+ function findParentList(state, pos) {
2738
+ const $pos = state.doc.resolve(pos);
2739
+ const { bulletList: blType, orderedList: olType } = actionbookSchema.nodes;
2740
+ for (let d = $pos.depth; d > 0; d--) {
2741
+ const node = $pos.node(d);
2742
+ if (node.type === blType || node.type === olType) {
2743
+ return { depth: d, node };
2744
+ }
2745
+ }
2746
+ return null;
2747
+ }
2748
+ function handleListInputRule(state, start, end, listType, attrs) {
2749
+ const resolvePos = Math.max(start, end - 1);
2750
+ const parentList = findParentList(state, resolvePos);
2751
+ if (!parentList) return null;
2752
+ const $from = state.doc.resolve(resolvePos);
2753
+ const { depth, node: listNode } = parentList;
2754
+ const paraContentSize = $from.parent.content.size;
2755
+ const matchedLen = end - start;
2756
+ if (paraContentSize !== matchedLen) return null;
2757
+ const listStart = $from.before(depth);
2758
+ const listEnd = $from.after(depth);
2759
+ if (listNode.type === listType) {
2760
+ const tr = state.tr.replaceWith(listStart, listEnd, actionbookSchema.nodes.paragraph.create());
2761
+ tr.setSelection(TextSelection.near(tr.doc.resolve(listStart)));
2762
+ return tr;
2763
+ } else {
2764
+ const tr = state.tr.delete(start, end);
2765
+ const mappedListPos = tr.mapping.map(listStart);
2766
+ tr.setNodeMarkup(mappedListPos, listType, attrs ?? null);
2767
+ return tr;
2768
+ }
2769
+ }
2737
2770
  function markInputRule(pattern, markType, markerLen) {
2738
2771
  return new InputRule(pattern, (state, match, start, end) => {
2739
2772
  const textContent2 = match[1];
@@ -2798,12 +2831,30 @@ function createInputRulesPlugin() {
2798
2831
  return state.tr.delete(start, end).insert(start, list);
2799
2832
  })
2800
2833
  );
2801
- rules.push(wrappingInputRule(BULLET_LIST_RE, actionbookSchema.nodes.bulletList));
2802
- rules.push(
2803
- wrappingInputRule(ORDERED_LIST_RE, actionbookSchema.nodes.orderedList, (match) => ({
2804
- start: +match[1]
2805
- }))
2806
- );
2834
+ {
2835
+ const blType = actionbookSchema.nodes.bulletList;
2836
+ const fallback = wrappingInputRule(BULLET_LIST_RE, blType);
2837
+ rules.push(
2838
+ new InputRule(BULLET_LIST_RE, (state, match, start, end) => {
2839
+ const toggled = handleListInputRule(state, start, end, blType);
2840
+ if (toggled) return toggled;
2841
+ const handler = fallback.handler;
2842
+ return handler(state, match, start, end);
2843
+ })
2844
+ );
2845
+ }
2846
+ {
2847
+ const olType = actionbookSchema.nodes.orderedList;
2848
+ const fallback = wrappingInputRule(ORDERED_LIST_RE, olType, (m) => ({ start: +m[1] }));
2849
+ rules.push(
2850
+ new InputRule(ORDERED_LIST_RE, (state, match, start, end) => {
2851
+ const toggled = handleListInputRule(state, start, end, olType, { start: +match[1] });
2852
+ if (toggled) return toggled;
2853
+ const handler = fallback.handler;
2854
+ return handler(state, match, start, end);
2855
+ })
2856
+ );
2857
+ }
2807
2858
  const jumpPointType = actionbookSchema.nodes.jumpPoint;
2808
2859
  rules.push(
2809
2860
  new InputRule(JUMP_POINT_RE2, (state, match, start, end) => {
@@ -2816,7 +2867,12 @@ function createInputRulesPlugin() {
2816
2867
  }
2817
2868
  });
2818
2869
  if (exists) return null;
2819
- return state.tr.delete(start, end).insert(start, jumpPointType.create({ id }));
2870
+ const tr = state.tr.delete(start, end).insert(start, jumpPointType.create({ id }));
2871
+ const afterAtom = start + 1;
2872
+ tr.insertText(" ", afterAtom);
2873
+ const cursorPos = afterAtom + 1;
2874
+ tr.setSelection(TextSelection.near(tr.doc.resolve(Math.min(cursorPos, tr.doc.content.size))));
2875
+ return tr;
2820
2876
  })
2821
2877
  );
2822
2878
  rules.push(markInputRule(BOLD_STAR_RE, actionbookSchema.marks.bold, 2));
@@ -2845,8 +2901,100 @@ function createHistoryPlugin() {
2845
2901
 
2846
2902
  // src/ui/plugin/keymapPlugin.ts
2847
2903
  import { baseKeymap, chainCommands, newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock, toggleMark, setBlockType, joinBackward } from "prosemirror-commands";
2904
+ import { TextSelection as TextSelection2 } from "prosemirror-state";
2848
2905
  import { keymap as keymap3 } from "prosemirror-keymap";
2849
2906
  import { liftListItem, sinkListItem, splitListItem, wrapInList } from "prosemirror-schema-list";
2907
+
2908
+ // src/ui/plugin/noteBlockPlugin.tsx
2909
+ import { Selection } from "prosemirror-state";
2910
+ var NoteBlockView = class {
2911
+ dom;
2912
+ contentDOM;
2913
+ constructor(_node, _view, _getPos) {
2914
+ this.dom = document.createElement("div");
2915
+ this.dom.setAttribute("data-note-block", "");
2916
+ this.dom.className = "ab-note-block";
2917
+ this.dom.style.cssText = [
2918
+ "position: relative",
2919
+ "margin: 8px 0",
2920
+ "padding: 16px 24px",
2921
+ "background: #fff",
2922
+ "border: 1px dashed #ccc",
2923
+ "border-radius: 4px",
2924
+ "display: flex",
2925
+ "flex-direction: column",
2926
+ "gap: 4px"
2927
+ ].join(";");
2928
+ const label = document.createElement("span");
2929
+ label.contentEditable = "false";
2930
+ label.textContent = "Note";
2931
+ label.style.cssText = [
2932
+ "font-family: SF Pro Text, -apple-system, BlinkMacSystemFont, sans-serif",
2933
+ "display: inline-flex",
2934
+ "align-items: center",
2935
+ "justify-content: center",
2936
+ "align-self: flex-start",
2937
+ "font-size: 12px",
2938
+ "font-weight: 400",
2939
+ "font-style: normal",
2940
+ "line-height: 16px",
2941
+ "color: #858585",
2942
+ "background: #f7f7f7",
2943
+ "border: 1px solid #e0e0e0",
2944
+ "border-radius: 4px",
2945
+ "padding: 2px 4px",
2946
+ "user-select: none",
2947
+ "height: 20px"
2948
+ ].join(";");
2949
+ this.dom.appendChild(label);
2950
+ this.contentDOM = document.createElement("div");
2951
+ this.contentDOM.className = "ab-note-block-content";
2952
+ this.contentDOM.style.cssText = [
2953
+ "font-size: 14px",
2954
+ "font-style: italic",
2955
+ "font-weight: 400",
2956
+ "line-height: 20px",
2957
+ "letter-spacing: -0.1px",
2958
+ "color: #858585"
2959
+ ].join(";");
2960
+ this.dom.appendChild(this.contentDOM);
2961
+ }
2962
+ };
2963
+ var exitNoteBlockOnEnter = (state, dispatch) => {
2964
+ const { $from } = state.selection;
2965
+ let noteBlockDepth = -1;
2966
+ for (let d = $from.depth; d > 0; d--) {
2967
+ if ($from.node(d).type.name === "noteBlock") {
2968
+ noteBlockDepth = d;
2969
+ break;
2970
+ }
2971
+ }
2972
+ if (noteBlockDepth < 0) return false;
2973
+ const parentNode = $from.parent;
2974
+ if (parentNode.type.name !== "paragraph" || parentNode.content.size !== 0) {
2975
+ return false;
2976
+ }
2977
+ if (!dispatch) return true;
2978
+ const paragraphStart = $from.before($from.depth);
2979
+ const paragraphEnd = $from.after($from.depth);
2980
+ const tr = state.tr.delete(paragraphStart, paragraphEnd);
2981
+ const noteBlockEnd = tr.mapping.map($from.after(noteBlockDepth));
2982
+ const newParagraph = state.schema.nodes.paragraph.create();
2983
+ tr.insert(noteBlockEnd, newParagraph);
2984
+ tr.setSelection(Selection.near(tr.doc.resolve(noteBlockEnd + 1)));
2985
+ dispatch(tr.scrollIntoView());
2986
+ return true;
2987
+ };
2988
+ function createNoteBlockPlugin() {
2989
+ return {
2990
+ name: "noteBlockNodeView",
2991
+ nodeViews: () => ({
2992
+ noteBlock: (node, view, getPos) => new NoteBlockView(node, view, getPos)
2993
+ })
2994
+ };
2995
+ }
2996
+
2997
+ // src/ui/plugin/keymapPlugin.ts
2850
2998
  var TAB_CHAR = " ";
2851
2999
  var { listItem, bulletList, orderedList, hardBreak, heading, paragraph, horizontalRule, blockquote } = actionbookSchema.nodes;
2852
3000
  var { bold: boldMark, italic: italicMark, underline: underlineMark, strikethrough: strikethroughMark, code: codeMark } = actionbookSchema.marks;
@@ -2893,7 +3041,38 @@ var backspaceAfterLeafBlock = (state, dispatch) => {
2893
3041
  }
2894
3042
  return true;
2895
3043
  };
3044
+ var backspaceDeleteEmptyBlock = (state, dispatch) => {
3045
+ const { $from } = state.selection;
3046
+ if (!state.selection.empty || $from.parentOffset !== 0) return false;
3047
+ if ($from.parent.type !== paragraph || $from.parent.content.size !== 0) return false;
3048
+ const deletableTypes = /* @__PURE__ */ new Set(["jinjaIfBlock", "jinjaIfBranch", "noteBlock", "blockquote"]);
3049
+ for (let d = $from.depth - 1; d > 0; d--) {
3050
+ const ancestor = $from.node(d);
3051
+ if (!deletableTypes.has(ancestor.type.name)) continue;
3052
+ if (ancestor.type.name === "jinjaIfBranch" && ancestor.childCount === 1 && ancestor.child(0).content.size === 0) {
3053
+ for (let dd = d - 1; dd > 0; dd--) {
3054
+ if ($from.node(dd).type.name === "jinjaIfBlock") {
3055
+ d = dd;
3056
+ break;
3057
+ }
3058
+ }
3059
+ }
3060
+ const blockNode = $from.node(d);
3061
+ const blockText = blockNode.textContent.trim();
3062
+ if (blockText.length > 0) return false;
3063
+ if (dispatch) {
3064
+ const blockStart = $from.before(d);
3065
+ const blockEnd = $from.after(d);
3066
+ const tr = state.tr.replaceWith(blockStart, blockEnd, paragraph.create());
3067
+ tr.setSelection(TextSelection2.near(tr.doc.resolve(blockStart)));
3068
+ dispatch(tr.scrollIntoView());
3069
+ }
3070
+ return true;
3071
+ }
3072
+ return false;
3073
+ };
2896
3074
  var backspaceCommand = chainCommands(
3075
+ backspaceDeleteEmptyBlock,
2897
3076
  backspaceAfterList,
2898
3077
  backspaceAfterLeafBlock,
2899
3078
  (state, dispatch) => {
@@ -2911,8 +3090,9 @@ var backspaceCommand = chainCommands(
2911
3090
  );
2912
3091
  var tabCommand = (state, dispatch) => {
2913
3092
  const { $from } = state.selection;
2914
- if (cursorDirectlyInListItem(state) && $from.parentOffset === 0) {
3093
+ if (cursorDirectlyInListItem(state)) {
2915
3094
  if (sinkListItem(listItem)(state, dispatch)) return true;
3095
+ return true;
2916
3096
  }
2917
3097
  if (dispatch) dispatch(state.tr.insertText(TAB_CHAR));
2918
3098
  return true;
@@ -2929,7 +3109,68 @@ var shiftTabCommand = (state, dispatch) => {
2929
3109
  }
2930
3110
  return true;
2931
3111
  };
3112
+ var blockExitEnterCount = 0;
3113
+ var blockExitLastTime = 0;
3114
+ var BLOCK_EXIT_TYPES = /* @__PURE__ */ new Set(["blockquote", "jinjaIfBranch"]);
3115
+ var exitBlockOnDoubleEnter = (state, dispatch) => {
3116
+ const { $from } = state.selection;
3117
+ let blockDepth = -1;
3118
+ for (let d = $from.depth; d > 0; d--) {
3119
+ if (BLOCK_EXIT_TYPES.has($from.node(d).type.name)) {
3120
+ blockDepth = d;
3121
+ break;
3122
+ }
3123
+ }
3124
+ if (blockDepth < 0) {
3125
+ blockExitEnterCount = 0;
3126
+ return false;
3127
+ }
3128
+ if ($from.parent.type.name !== "paragraph" || $from.parent.content.size !== 0) {
3129
+ blockExitEnterCount = 0;
3130
+ return false;
3131
+ }
3132
+ const now = Date.now();
3133
+ if (now - blockExitLastTime > 2e3) blockExitEnterCount = 0;
3134
+ blockExitLastTime = now;
3135
+ blockExitEnterCount++;
3136
+ if (blockExitEnterCount < 2) return false;
3137
+ blockExitEnterCount = 0;
3138
+ if (!dispatch) return true;
3139
+ const paraStart = $from.before($from.depth);
3140
+ const paraEnd = $from.after($from.depth);
3141
+ const tr = state.tr.delete(paraStart, paraEnd);
3142
+ const mapped = tr.mapping.map($from.before($from.depth));
3143
+ const $m = tr.doc.resolve(Math.min(mapped, tr.doc.content.size));
3144
+ for (let d = $m.depth; d > 0; d--) {
3145
+ if (BLOCK_EXIT_TYPES.has($m.node(d).type.name)) {
3146
+ const last = $m.node(d).lastChild;
3147
+ if (last?.type.name === "paragraph" && last.content.size === 0) {
3148
+ const lastPos = $m.end(d) - last.nodeSize;
3149
+ tr.delete(lastPos, lastPos + last.nodeSize);
3150
+ }
3151
+ break;
3152
+ }
3153
+ }
3154
+ let exitDepth = blockDepth;
3155
+ const blockTypeName = $from.node(blockDepth).type.name;
3156
+ if (blockTypeName === "jinjaIfBranch") {
3157
+ for (let d = blockDepth - 1; d > 0; d--) {
3158
+ if ($from.node(d).type.name === "jinjaIfBlock") {
3159
+ exitDepth = d;
3160
+ break;
3161
+ }
3162
+ }
3163
+ }
3164
+ const blockEnd = tr.mapping.map($from.after(exitDepth));
3165
+ const newPara = paragraph.create();
3166
+ tr.insert(blockEnd, newPara);
3167
+ tr.setSelection(TextSelection2.near(tr.doc.resolve(blockEnd + 1)));
3168
+ dispatch(tr.scrollIntoView());
3169
+ return true;
3170
+ };
2932
3171
  var enterCommand = chainCommands(
3172
+ exitNoteBlockOnEnter,
3173
+ exitBlockOnDoubleEnter,
2933
3174
  newlineInCode,
2934
3175
  // Split list item on Enter; lift empty list item out of list
2935
3176
  splitListItem(listItem),
@@ -4705,6 +4946,17 @@ function analyzeJinjaBlocks(doc2) {
4705
4946
 
4706
4947
  // src/tree/documentTree.ts
4707
4948
  var MAX_DEPTH6 = 128;
4949
+ var END_ACTION_RESOURCE_IDS = /* @__PURE__ */ new Set([
4950
+ "close-happy-tiger"
4951
+ // PredefinedToolKey.CloseConversation
4952
+ ]);
4953
+ var END_ACTION_TEXTS = /* @__PURE__ */ new Set([
4954
+ "end conversation",
4955
+ "end_conversation"
4956
+ ]);
4957
+ function isEndActionTag(tagType, resourceId, text2) {
4958
+ return tagType === "handoff" || END_ACTION_RESOURCE_IDS.has(resourceId) || END_ACTION_TEXTS.has(text2.toLowerCase());
4959
+ }
4708
4960
  function extractInlineItems(content, blockIndex) {
4709
4961
  const items = [];
4710
4962
  for (const node of content) {
@@ -4716,10 +4968,10 @@ function extractInlineItems(content, blockIndex) {
4716
4968
  children: []
4717
4969
  });
4718
4970
  } else if (node.type === "resourceTag") {
4719
- const isEndAction = node.tagType === "handoff";
4971
+ const endAction = isEndActionTag(node.tagType, node.resourceId, node.text);
4720
4972
  items.push({
4721
- type: isEndAction ? "endAction" : "resourceTag",
4722
- label: isEndAction ? `End ${node.text}` : node.text,
4973
+ type: endAction ? "endAction" : "resourceTag",
4974
+ label: endAction ? `End ${node.text}` : node.text,
4723
4975
  blockIndex,
4724
4976
  children: [],
4725
4977
  meta: { tagType: node.tagType, resourceId: node.resourceId }
@@ -4753,14 +5005,15 @@ function extractBlockItems(blocks, startIndex, depth) {
4753
5005
  break;
4754
5006
  }
4755
5007
  case "jinjaIfBlock": {
4756
- const branches = block.branches.map((branch) => {
5008
+ const branches = block.branches.map((branch, branchIdx) => {
4757
5009
  const branchLabel = branch.branchType === "else" ? "ELSE" : `${branch.branchType.toUpperCase()} ${branch.condition || ""}`.trim();
4758
5010
  const branchChildren = extractBlockItems(branch.content, blockIndex, depth + 1);
4759
5011
  return {
4760
5012
  type: "jinjaBranch",
4761
5013
  label: branchLabel,
4762
5014
  blockIndex,
4763
- children: branchChildren
5015
+ children: branchChildren,
5016
+ meta: { branchIndex: branchIdx }
4764
5017
  };
4765
5018
  });
4766
5019
  items.push({
@@ -4808,11 +5061,12 @@ function buildDocumentTree(doc2) {
4808
5061
  const hasStructuredJinja = items.some((n) => n.type === "jinjaIf");
4809
5062
  if (!hasStructuredJinja) {
4810
5063
  for (const s of structures) {
4811
- const branches = s.branches.map((b) => ({
5064
+ const branches = s.branches.map((b, branchIdx) => ({
4812
5065
  type: "jinjaBranch",
4813
5066
  label: b.type === "else" ? "ELSE" : `${b.type.toUpperCase()} ${b.condition || ""}`.trim(),
4814
5067
  blockIndex: b.tagBlockIndex,
4815
- children: []
5068
+ children: [],
5069
+ meta: { branchIndex: branchIdx }
4816
5070
  }));
4817
5071
  const jinjaNode = {
4818
5072
  type: "jinjaIf",
@@ -4916,6 +5170,7 @@ function textToSlice(text2, view) {
4916
5170
  return null;
4917
5171
  }
4918
5172
  }
5173
+ var URL_RE = /^https?:\/\/[^\s]+$/i;
4919
5174
  function createPlugin() {
4920
5175
  return new Plugin({
4921
5176
  key,
@@ -4930,6 +5185,14 @@ function createPlugin() {
4930
5185
  return true;
4931
5186
  }
4932
5187
  }
5188
+ if (text2 && URL_RE.test(text2.trim())) {
5189
+ const url = text2.trim();
5190
+ const linkMark2 = actionbookSchema.marks.link.create({ href: url });
5191
+ const textNode = actionbookSchema.text(url, [linkMark2]);
5192
+ const slice2 = new Slice(Fragment.from(textNode), 0, 0);
5193
+ view.dispatch(view.state.tr.replaceSelection(slice2));
5194
+ return true;
5195
+ }
4933
5196
  if (!text2) return false;
4934
5197
  const slice = textToSlice(text2, view);
4935
5198
  if (!slice) return false;
@@ -4975,7 +5238,7 @@ function createMarkdownClipboardPlugin() {
4975
5238
  }
4976
5239
 
4977
5240
  // src/ui/plugin/jumpPointPlugin.ts
4978
- import { Plugin as Plugin2, PluginKey as PluginKey2, TextSelection as TextSelection2 } from "prosemirror-state";
5241
+ import { Plugin as Plugin2, PluginKey as PluginKey2, TextSelection as TextSelection3 } from "prosemirror-state";
4979
5242
  import { Decoration, DecorationSet } from "prosemirror-view";
4980
5243
  var adjacentKey = new PluginKey2("jumpPointAdjacent");
4981
5244
  var JUMP_POINT_ADJACENT_SPEC = { jumpPointAdjacent: true };
@@ -5038,6 +5301,9 @@ function createJumpPointEditPlugin() {
5038
5301
  });
5039
5302
  if (!duplicate) {
5040
5303
  reconvertTr.replaceWith(safeFrom, safeTo, jumpPointType.create({ id }));
5304
+ const mappedCursor = reconvertTr.mapping.map(cursor);
5305
+ const clampedCursor = Math.min(mappedCursor, reconvertTr.doc.content.size);
5306
+ reconvertTr.setSelection(TextSelection3.near(reconvertTr.doc.resolve(clampedCursor)));
5041
5307
  }
5042
5308
  }
5043
5309
  }
@@ -5070,7 +5336,7 @@ function explodeOnBackspace(state) {
5070
5336
  const rawText = `^${id}`;
5071
5337
  const tr = state.tr.replaceWith(nodeStart, nodeEnd, state.schema.text(rawText));
5072
5338
  const cursorPos = Math.min(nodeStart + rawText.length, tr.doc.content.size);
5073
- tr.setSelection(TextSelection2.near(tr.doc.resolve(cursorPos)));
5339
+ tr.setSelection(TextSelection3.near(tr.doc.resolve(cursorPos)));
5074
5340
  return tr;
5075
5341
  }
5076
5342
  function explodeOnArrowLeft(state) {
@@ -5080,7 +5346,7 @@ function explodeOnArrowLeft(state) {
5080
5346
  const rawText = `^${id}^`;
5081
5347
  const tr = state.tr.replaceWith(nodeStart, nodeEnd, state.schema.text(rawText));
5082
5348
  const cursorPos = Math.min(nodeStart + 1 + id.length, tr.doc.content.size);
5083
- tr.setSelection(TextSelection2.near(tr.doc.resolve(cursorPos)));
5349
+ tr.setSelection(TextSelection3.near(tr.doc.resolve(cursorPos)));
5084
5350
  tr.setMeta(jumpPointEditKey, { from: nodeStart, to: nodeStart + rawText.length });
5085
5351
  return tr;
5086
5352
  }
@@ -5129,21 +5395,30 @@ function createJumpPointAdjacentPlugin() {
5129
5395
  // src/ui/plugin/jumpPointNodeViewPlugin.tsx
5130
5396
  import { useCallback as useCallback2, useState as useState2 } from "react";
5131
5397
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
5398
+ function IconDiamondAlert({ size = 12, fill = "#D9352C" }) {
5399
+ return /* @__PURE__ */ jsxs3("svg", { width: size, height: size, viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
5400
+ /* @__PURE__ */ jsx4(
5401
+ "path",
5402
+ {
5403
+ d: "M5.293 1.293a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414l4-4Z",
5404
+ fill
5405
+ }
5406
+ ),
5407
+ /* @__PURE__ */ jsx4("path", { d: "M6 3.5v3", stroke: "#fff", strokeWidth: "1.2", strokeLinecap: "round" }),
5408
+ /* @__PURE__ */ jsx4("circle", { cx: "6", cy: "8.25", r: "0.625", fill: "#fff" })
5409
+ ] });
5410
+ }
5132
5411
  var JUMP_POINT_STYLE = {
5133
5412
  display: "inline-flex",
5134
5413
  alignItems: "center",
5135
5414
  backgroundColor: "#FFF2B6",
5136
5415
  border: "1px solid #FFC233",
5137
5416
  color: "#AA5D04",
5138
- borderRadius: "2px",
5139
- padding: "2px",
5140
- fontSize: "12px",
5141
- lineHeight: "16px",
5142
5417
  userSelect: "none",
5143
5418
  cursor: "default",
5144
- boxSizing: "border-box",
5145
- height: "20px",
5146
- overflow: "hidden"
5419
+ boxSizing: "border-box"
5420
+ // height, padding, borderRadius, fontSize, lineHeight, overflow
5421
+ // are set via CSS class .ab-jump-point for heading-level overrides
5147
5422
  };
5148
5423
  var JUMP_POINT_ADJACENT_STYLE = {
5149
5424
  ...JUMP_POINT_STYLE,
@@ -5157,8 +5432,8 @@ var JUMP_POINT_DUPLICATE_STYLE = {
5157
5432
  color: "#9D091E"
5158
5433
  };
5159
5434
  var LABEL_STYLE = {
5160
- padding: "0 4px",
5161
5435
  whiteSpace: "nowrap"
5436
+ // padding is set via CSS .ab-jp-label for heading-level overrides
5162
5437
  };
5163
5438
  var CLOSE_BTN_STYLE = {
5164
5439
  display: "inline-flex",
@@ -5189,7 +5464,7 @@ var TOOLTIP_STYLE = {
5189
5464
  fontSize: "14px",
5190
5465
  lineHeight: "20px",
5191
5466
  color: "#0D0D0D",
5192
- whiteSpace: "nowrap",
5467
+ maxWidth: "240px",
5193
5468
  zIndex: 100,
5194
5469
  pointerEvents: "none"
5195
5470
  };
@@ -5209,13 +5484,14 @@ function JumpPointNodeViewComponent({ node, view, getPos, decorations }) {
5209
5484
  return /* @__PURE__ */ jsxs3(
5210
5485
  "span",
5211
5486
  {
5487
+ className: "ab-jump-point ab-jump-point-duplicate",
5212
5488
  style: { ...JUMP_POINT_DUPLICATE_STYLE, position: "relative" },
5213
5489
  id: `jp-${id}`,
5214
5490
  onMouseEnter: () => setShowTooltip(true),
5215
5491
  onMouseLeave: () => setShowTooltip(false),
5216
5492
  children: [
5217
- /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#9D091E", style: { paddingLeft: "2px", flexShrink: 0 } }),
5218
- /* @__PURE__ */ jsx4("span", { style: LABEL_STYLE, children: id }),
5493
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-icon", children: /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#9D091E", style: { paddingLeft: "2px", flexShrink: 0 } }) }),
5494
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-label", style: LABEL_STYLE, children: id }),
5219
5495
  /* @__PURE__ */ jsx4(
5220
5496
  "button",
5221
5497
  {
@@ -5225,8 +5501,8 @@ function JumpPointNodeViewComponent({ node, view, getPos, decorations }) {
5225
5501
  e.stopPropagation();
5226
5502
  handleDelete2();
5227
5503
  },
5228
- title: "Remove anchor",
5229
- children: "\u2715"
5504
+ title: "Remove duplicate anchor",
5505
+ children: /* @__PURE__ */ jsx4(IconDiamondAlert, { size: 12, fill: "#D9352C" })
5230
5506
  }
5231
5507
  ),
5232
5508
  showTooltip && /* @__PURE__ */ jsxs3("span", { style: TOOLTIP_STYLE, children: [
@@ -5239,14 +5515,14 @@ function JumpPointNodeViewComponent({ node, view, getPos, decorations }) {
5239
5515
  );
5240
5516
  }
5241
5517
  if (isAdjacent) {
5242
- return /* @__PURE__ */ jsxs3("span", { style: JUMP_POINT_ADJACENT_STYLE, id: `jp-${id}`, children: [
5243
- /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#AA5D04", style: { paddingLeft: "2px", flexShrink: 0 } }),
5244
- /* @__PURE__ */ jsx4("span", { style: LABEL_STYLE, children: `^${id}^` })
5518
+ return /* @__PURE__ */ jsxs3("span", { className: "ab-jump-point ab-jump-point-adjacent", style: JUMP_POINT_ADJACENT_STYLE, id: `jp-${id}`, children: [
5519
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-icon", children: /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#AA5D04", style: { paddingLeft: "2px", flexShrink: 0 } }) }),
5520
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-label", style: LABEL_STYLE, children: `^${id}^` })
5245
5521
  ] });
5246
5522
  }
5247
- return /* @__PURE__ */ jsxs3("span", { style: JUMP_POINT_STYLE, id: `jp-${id}`, children: [
5248
- /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#AA5D04", style: { paddingLeft: "2px", flexShrink: 0 } }),
5249
- /* @__PURE__ */ jsx4("span", { style: LABEL_STYLE, children: id })
5523
+ return /* @__PURE__ */ jsxs3("span", { className: "ab-jump-point", style: JUMP_POINT_STYLE, id: `jp-${id}`, children: [
5524
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-icon", children: /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#AA5D04", style: { paddingLeft: "2px", flexShrink: 0 } }) }),
5525
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-label", style: LABEL_STYLE, children: id })
5250
5526
  ] });
5251
5527
  }
5252
5528
  function createJumpPointNodeViewPlugin() {
@@ -5544,6 +5820,7 @@ function createJinjaDecorationPlugin() {
5544
5820
  // src/ui/plugin/jinjaIfBlockPlugin.tsx
5545
5821
  import { useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
5546
5822
  import { createRoot as createRoot2 } from "react-dom/client";
5823
+ import { TextSelection as TextSelection4 } from "prosemirror-state";
5547
5824
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
5548
5825
  var PLACEHOLDER_TEXT = "Describe what AI agent should do when this condition is met";
5549
5826
  var CONDITION_PLACEHOLDER = "Write a condition in natural language";
@@ -5950,7 +6227,12 @@ function insertNewBranch(view, nodePos, branchType, condition = "") {
5950
6227
  const newBranch = createBranchNode(view.state.schema, branchType, condition);
5951
6228
  if (!newBranch) return false;
5952
6229
  const insertPos = context.branchPos + context.branchNode.nodeSize;
5953
- view.dispatch(view.state.tr.insert(insertPos, newBranch).scrollIntoView());
6230
+ const tr = view.state.tr.insert(insertPos, newBranch);
6231
+ const cursorPos = insertPos + 2;
6232
+ if (cursorPos <= tr.doc.content.size) {
6233
+ tr.setSelection(TextSelection4.near(tr.doc.resolve(cursorPos)));
6234
+ }
6235
+ view.dispatch(tr.scrollIntoView());
5954
6236
  view.focus();
5955
6237
  return true;
5956
6238
  }
@@ -5997,6 +6279,7 @@ function ConditionDisplay({
5997
6279
  const [isEditing, setIsEditing] = useState3(false);
5998
6280
  const [draftValue, setDraftValue] = useState3(condition);
5999
6281
  const inputRef = useRef2(null);
6282
+ const autoFocusedRef = useRef2(false);
6000
6283
  useEffect2(() => {
6001
6284
  if (!isEditing) {
6002
6285
  setDraftValue(condition);
@@ -6008,6 +6291,12 @@ function ConditionDisplay({
6008
6291
  inputRef.current?.select();
6009
6292
  }
6010
6293
  }, [isEditing]);
6294
+ useEffect2(() => {
6295
+ if (editable && !condition && !autoFocusedRef.current) {
6296
+ autoFocusedRef.current = true;
6297
+ setIsEditing(true);
6298
+ }
6299
+ }, []);
6011
6300
  if (branchType === "else") {
6012
6301
  return /* @__PURE__ */ jsx6("span", { className: "jinja-otherwise", children: "Otherwise" });
6013
6302
  }
@@ -6303,15 +6592,25 @@ var JinjaIfBranchView = class {
6303
6592
  }
6304
6593
  render() {
6305
6594
  const nodePos = this.getPos();
6306
- if (nodePos == null) return;
6307
- const lastBranch = getLastBranchOfBlock(this.view, nodePos);
6308
- const context = getBranchContext(this.view, nodePos);
6309
- if (!lastBranch || !context) return;
6595
+ let context = null;
6596
+ let lastBranch = null;
6597
+ if (nodePos != null) {
6598
+ context = getBranchContext(this.view, nodePos);
6599
+ lastBranch = getLastBranchOfBlock(this.view, nodePos);
6600
+ let branchIdx = context?.branchIndex ?? -1;
6601
+ if (branchIdx < 0 && this.dom.parentElement) {
6602
+ const siblings = this.dom.parentElement.querySelectorAll(":scope > [data-jinja-branch]");
6603
+ branchIdx = Array.prototype.indexOf.call(siblings, this.dom);
6604
+ }
6605
+ if (branchIdx >= 0) {
6606
+ this.dom.setAttribute("data-branch-index", String(branchIdx));
6607
+ }
6608
+ }
6310
6609
  const branchType = this.node.attrs.branchType;
6311
6610
  const condition = String(this.node.attrs.condition ?? "");
6312
- const isLast = lastBranch.index === context.branchIndex;
6313
- const lastNonElseIdx = getLastNonElseBranchIndex(this.view, nodePos);
6314
- const showAddButton = context.branchIndex === lastNonElseIdx;
6611
+ const isLast = context && lastBranch ? lastBranch.index === context.branchIndex : true;
6612
+ const lastNonElseIdx = context ? getLastNonElseBranchIndex(this.view, nodePos) : 0;
6613
+ const showAddButton = context ? context.branchIndex === lastNonElseIdx : true;
6315
6614
  this.dom.classList.toggle("jinja-branch-last", isLast);
6316
6615
  this.root.render(
6317
6616
  /* @__PURE__ */ jsx6(
@@ -6321,16 +6620,19 @@ var JinjaIfBranchView = class {
6321
6620
  condition,
6322
6621
  editable: this.view.editable,
6323
6622
  isLastBranch: showAddButton,
6324
- hasElseBranch: getElseBranch(this.view, nodePos),
6623
+ hasElseBranch: context && nodePos != null ? getElseBranch(this.view, nodePos) : false,
6325
6624
  onConditionChange: (value) => {
6326
6625
  const currentPos = this.getPos();
6327
6626
  if (currentPos == null) return;
6328
- this.view.dispatch(
6329
- this.view.state.tr.setNodeMarkup(currentPos, void 0, {
6330
- ...this.node.attrs,
6331
- condition: value
6332
- })
6333
- );
6627
+ const tr = this.view.state.tr.setNodeMarkup(currentPos, void 0, {
6628
+ ...this.node.attrs,
6629
+ condition: value
6630
+ });
6631
+ const contentPos = currentPos + 2;
6632
+ if (contentPos <= tr.doc.content.size) {
6633
+ tr.setSelection(TextSelection4.near(tr.doc.resolve(contentPos)));
6634
+ }
6635
+ this.view.dispatch(tr.scrollIntoView());
6334
6636
  this.view.focus();
6335
6637
  },
6336
6638
  onDelete: () => {
@@ -6381,9 +6683,9 @@ function createJinjaIfBlockPlugin() {
6381
6683
 
6382
6684
  // src/ui/plugin/linkPlugin.ts
6383
6685
  import { InputRule as InputRule2, inputRules as inputRules2 } from "prosemirror-inputrules";
6384
- import { Plugin as Plugin5, PluginKey as PluginKey5, TextSelection as TextSelection3 } from "prosemirror-state";
6385
- var LINK_INPUT_RE = /\[([^\]]*)\]\(([^)\s]*?)(?:\s+"([^"]*)")?\)$/;
6386
- var LINK_FULL_RE = /^\[([^\]]*)\]\(([^)\s]*?)(?:\s+"([^"]*)")?\)$/;
6686
+ import { Plugin as Plugin5, PluginKey as PluginKey5, TextSelection as TextSelection5 } from "prosemirror-state";
6687
+ var LINK_INPUT_RE = /\\?\[([^\]]*?)\\?\]\(([^)\s]*?)(?:\s+"([^"]*)")?\)$/;
6688
+ var LINK_FULL_RE = /^\\?\[([^\]]*?)\\?\]\(([^)\s]*?)(?:\s+"([^"]*)")?\)$/;
6387
6689
  var linkEditKey = new PluginKey5("linkEdit");
6388
6690
  function getMarkRange($pos, type) {
6389
6691
  const { parentOffset } = $pos;
@@ -6427,7 +6729,7 @@ function explodeLinkToRaw(state) {
6427
6729
  const tr = state.tr;
6428
6730
  tr.replaceWith(from, to, schema.text(rawText));
6429
6731
  const newPos = Math.min(from + rawCursorOffset, tr.doc.content.size);
6430
- tr.setSelection(TextSelection3.near(tr.doc.resolve(newPos)));
6732
+ tr.setSelection(TextSelection5.near(tr.doc.resolve(newPos)));
6431
6733
  tr.setMeta(linkEditKey, { from, to: from + rawText.length });
6432
6734
  return tr;
6433
6735
  }
@@ -6440,8 +6742,8 @@ function createLinkEditPlugin() {
6440
6742
  const meta = tr.getMeta(linkEditKey);
6441
6743
  if (meta !== void 0) return { rawRange: meta };
6442
6744
  if (prev.rawRange) {
6443
- const from = tr.mapping.map(prev.rawRange.from);
6444
- const to = tr.mapping.map(prev.rawRange.to);
6745
+ const from = tr.mapping.map(prev.rawRange.from, -1);
6746
+ const to = tr.mapping.map(prev.rawRange.to, -1);
6445
6747
  if (from >= to) return { rawRange: null };
6446
6748
  return { rawRange: { from, to } };
6447
6749
  }
@@ -6452,7 +6754,7 @@ function createLinkEditPlugin() {
6452
6754
  const pluginState = linkEditKey.getState(newState);
6453
6755
  if (!pluginState?.rawRange) return null;
6454
6756
  const { from, to } = pluginState.rawRange;
6455
- if (newState.selection.anchor >= from && newState.selection.anchor <= to) {
6757
+ if (newState.selection.anchor >= from && newState.selection.anchor < to) {
6456
6758
  return null;
6457
6759
  }
6458
6760
  const docSize = newState.doc.content.size;
@@ -6596,12 +6898,13 @@ var STYLES = (
6596
6898
  cursor: grab;
6597
6899
  color: transparent;
6598
6900
  border-radius: var(--ab-dh-handle-radius);
6599
- transition: color var(--ab-dh-transition-duration), background var(--ab-dh-transition-duration), opacity var(--ab-dh-transition-duration);
6901
+ transition: none;
6600
6902
  user-select: none;
6601
6903
  pointer-events: auto;
6602
6904
  touch-action: none;
6603
6905
  opacity: 0;
6604
6906
  pointer-events: none;
6907
+ overflow: visible;
6605
6908
  }
6606
6909
  .ab-drag-handle.ab-dh-active {
6607
6910
  opacity: 1;
@@ -6612,6 +6915,72 @@ var STYLES = (
6612
6915
  color: var(--ab-dh-color-visible) !important;
6613
6916
  background: var(--ab-dh-bg-hover);
6614
6917
  }
6918
+ .ab-drag-handle:hover::after {
6919
+ content: 'Drag to move';
6920
+ position: absolute;
6921
+ bottom: calc(100% + 4px);
6922
+ left: 0;
6923
+ background: #2e2e2e;
6924
+ color: #fff;
6925
+ font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
6926
+ font-size: 12px;
6927
+ font-weight: 400;
6928
+ line-height: 16px;
6929
+ padding: 4px 8px;
6930
+ border-radius: 4px;
6931
+ white-space: nowrap;
6932
+ pointer-events: none;
6933
+ z-index: 30;
6934
+ }
6935
+ .ab-drag-handle:hover::before {
6936
+ content: '';
6937
+ position: absolute;
6938
+ bottom: calc(100% + 0px);
6939
+ left: 8px;
6940
+ width: 0;
6941
+ height: 0;
6942
+ border-left: 5px solid transparent;
6943
+ border-right: 5px solid transparent;
6944
+ border-top: 5px solid #2e2e2e;
6945
+ pointer-events: none;
6946
+ z-index: 31;
6947
+ }
6948
+ .ab-drag-handle.dragging::after,
6949
+ .ab-drag-handle.dragging::before {
6950
+ display: none;
6951
+ }
6952
+ .ab-dh-menu {
6953
+ position: absolute;
6954
+ top: calc(100% + 4px);
6955
+ left: 0;
6956
+ background: #fff;
6957
+ border-radius: 4px;
6958
+ box-shadow: 0px 8px 10px 1px rgba(13,13,13,0.12), 0px 3px 14px 2px rgba(13,13,13,0.08), 0px 3px 5px -3px rgba(13,13,13,0.04);
6959
+ padding: 8px 0;
6960
+ z-index: 40;
6961
+ min-width: 120px;
6962
+ }
6963
+ .ab-dh-menu-item {
6964
+ display: flex;
6965
+ align-items: center;
6966
+ width: 100%;
6967
+ padding: 6px 16px;
6968
+ border: none;
6969
+ background: #fff;
6970
+ cursor: pointer;
6971
+ font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
6972
+ font-size: 14px;
6973
+ font-weight: 400;
6974
+ line-height: 20px;
6975
+ letter-spacing: -0.1px;
6976
+ color: #0d0d0d;
6977
+ white-space: nowrap;
6978
+ text-overflow: ellipsis;
6979
+ overflow: hidden;
6980
+ }
6981
+ .ab-dh-menu-item:hover {
6982
+ background: rgba(0,0,0,0.04);
6983
+ }
6615
6984
  .ab-drag-handle:active, .ab-drag-handle.dragging {
6616
6985
  cursor: grabbing;
6617
6986
  color: var(--ab-dh-color-accent) !important;
@@ -6921,7 +7290,6 @@ var DragHandleController = class {
6921
7290
  el.setAttribute("role", "button");
6922
7291
  el.setAttribute("aria-roledescription", "drag handle");
6923
7292
  el.setAttribute("aria-label", `\uBE14\uB85D ${idx + 1} \uC774\uB3D9`);
6924
- el.setAttribute("title", "Drag to move");
6925
7293
  el.setAttribute("tabindex", "-1");
6926
7294
  el.innerHTML = GRIP_SVG;
6927
7295
  el.style.left = `${handleLeft}px`;
@@ -6934,18 +7302,85 @@ var DragHandleController = class {
6934
7302
  this.prevBlocks = newBlocks;
6935
7303
  this.updateActiveHandle();
6936
7304
  }
7305
+ activeMenu = null;
7306
+ activeMenuCleanup = null;
7307
+ dismissMenu() {
7308
+ if (this.activeMenu) {
7309
+ this.activeMenu.remove();
7310
+ this.activeMenu = null;
7311
+ }
7312
+ if (this.activeMenuCleanup) {
7313
+ this.activeMenuCleanup();
7314
+ this.activeMenuCleanup = null;
7315
+ }
7316
+ }
7317
+ showMenu(el, blockIndex) {
7318
+ this.dismissMenu();
7319
+ const menu = document.createElement("div");
7320
+ menu.className = "ab-dh-menu";
7321
+ const deleteBtn = document.createElement("button");
7322
+ deleteBtn.className = "ab-dh-menu-item";
7323
+ deleteBtn.textContent = "Delete";
7324
+ deleteBtn.addEventListener("mousedown", (e) => {
7325
+ e.preventDefault();
7326
+ e.stopPropagation();
7327
+ this.deleteBlock(blockIndex);
7328
+ this.dismissMenu();
7329
+ });
7330
+ menu.appendChild(deleteBtn);
7331
+ el.appendChild(menu);
7332
+ this.activeMenu = menu;
7333
+ const onOutside = (e) => {
7334
+ if (!menu.contains(e.target)) {
7335
+ this.dismissMenu();
7336
+ }
7337
+ };
7338
+ setTimeout(() => document.addEventListener("mousedown", onOutside, true), 0);
7339
+ this.activeMenuCleanup = () => document.removeEventListener("mousedown", onOutside, true);
7340
+ }
7341
+ deleteBlock(blockIndex) {
7342
+ const blocks = this.collectBlocks();
7343
+ if (blockIndex >= blocks.length) return;
7344
+ const block = blocks[blockIndex];
7345
+ const from = block.offset;
7346
+ const to = block.offset + block.nodeSize;
7347
+ const tr = this.view.state.tr;
7348
+ tr.replaceWith(from, to, this.view.state.schema.nodes.paragraph.create());
7349
+ this.view.dispatch(tr);
7350
+ this.view.focus();
7351
+ }
6937
7352
  bindHandleEvents(el, index) {
6938
7353
  let currentIndex = index;
7354
+ let downX = 0;
7355
+ let downY = 0;
7356
+ let didDrag = false;
7357
+ let menuWasOpen = false;
6939
7358
  const onPointerDown = (e) => {
7359
+ if (this.activeMenu && this.activeMenu.contains(e.target)) return;
6940
7360
  e.preventDefault();
6941
7361
  if (!this.view.editable) return;
7362
+ menuWasOpen = !!this.activeMenu;
7363
+ this.dismissMenu();
7364
+ downX = e.clientX;
7365
+ downY = e.clientY;
7366
+ didDrag = false;
6942
7367
  el.setPointerCapture(e.pointerId);
6943
7368
  this.prevBlocks = this.collectBlocks();
6944
7369
  this.syncHandlePositions();
6945
7370
  this.startDrag(currentIndex, el, e);
6946
7371
  };
6947
- const onPointerMove = (e) => this.onPointerMove(e);
6948
- const onPointerUp = (e) => this.onPointerUp(e);
7372
+ const onPointerMove = (e) => {
7373
+ if (Math.abs(e.clientX - downX) > 3 || Math.abs(e.clientY - downY) > 3) {
7374
+ didDrag = true;
7375
+ }
7376
+ this.onPointerMove(e);
7377
+ };
7378
+ const onPointerUp = (e) => {
7379
+ this.onPointerUp(e);
7380
+ if (!didDrag && !menuWasOpen) {
7381
+ this.showMenu(el, currentIndex);
7382
+ }
7383
+ };
6949
7384
  const onLostCapture = () => this.cancelDrag();
6950
7385
  const onPointerCancel = () => this.cancelDrag();
6951
7386
  el.addEventListener("pointerdown", onPointerDown);
@@ -7352,150 +7787,89 @@ function createTodoNodeViewPlugin() {
7352
7787
  };
7353
7788
  }
7354
7789
 
7355
- // src/ui/plugin/noteBlockPlugin.tsx
7356
- import { Selection } from "prosemirror-state";
7357
- var NoteBlockView = class {
7358
- dom;
7359
- contentDOM;
7360
- constructor(_node, _view, _getPos) {
7361
- this.dom = document.createElement("div");
7362
- this.dom.setAttribute("data-note-block", "");
7363
- this.dom.className = "ab-note-block";
7364
- this.dom.style.cssText = [
7365
- "position: relative",
7366
- "margin: 8px 0",
7367
- "padding: 16px 24px",
7368
- "background: #fff",
7369
- "border: 1px dashed #ccc",
7370
- "border-radius: 4px",
7371
- "display: flex",
7372
- "flex-direction: column",
7373
- "gap: 4px"
7374
- ].join(";");
7375
- const label = document.createElement("span");
7376
- label.contentEditable = "false";
7377
- label.textContent = "Note";
7378
- label.style.cssText = [
7379
- "display: inline-flex",
7380
- "align-items: center",
7381
- "justify-content: center",
7382
- "align-self: flex-start",
7383
- "font-size: 12px",
7384
- "font-weight: 400",
7385
- "font-style: normal",
7386
- "line-height: 16px",
7387
- "color: #858585",
7388
- "background: #f7f7f7",
7389
- "border: 1px solid #e0e0e0",
7390
- "border-radius: 4px",
7391
- "padding: 2px 4px",
7392
- "user-select: none"
7393
- ].join(";");
7394
- this.dom.appendChild(label);
7395
- this.contentDOM = document.createElement("div");
7396
- this.contentDOM.className = "ab-note-block-content";
7397
- this.contentDOM.style.cssText = [
7398
- "font-size: 14px",
7399
- "font-style: italic",
7400
- "font-weight: 400",
7401
- "line-height: 20px",
7402
- "letter-spacing: -0.1px",
7403
- "color: #858585"
7404
- ].join(";");
7405
- this.dom.appendChild(this.contentDOM);
7406
- }
7407
- };
7408
- var emptyEnterCount = 0;
7409
- var lastEnterTime = 0;
7410
- var exitNoteBlockOnEnter = (state, dispatch) => {
7411
- const { $from } = state.selection;
7412
- let noteBlockDepth = -1;
7413
- for (let d = $from.depth; d > 0; d--) {
7414
- if ($from.node(d).type.name === "noteBlock") {
7415
- noteBlockDepth = d;
7416
- break;
7417
- }
7418
- }
7419
- if (noteBlockDepth < 0) {
7420
- emptyEnterCount = 0;
7421
- return false;
7422
- }
7423
- const parentNode = $from.parent;
7424
- if (parentNode.type.name !== "paragraph" || parentNode.content.size !== 0) {
7425
- emptyEnterCount = 0;
7426
- return false;
7427
- }
7428
- const now = Date.now();
7429
- if (now - lastEnterTime > 2e3) {
7430
- emptyEnterCount = 0;
7431
- }
7432
- lastEnterTime = now;
7433
- emptyEnterCount++;
7434
- if (emptyEnterCount < 2) {
7435
- return false;
7790
+ // src/ui/plugin/placeholderPlugin.ts
7791
+ import { Plugin as Plugin7, PluginKey as PluginKey7 } from "prosemirror-state";
7792
+ import { Decoration as Decoration4, DecorationSet as DecorationSet4 } from "prosemirror-view";
7793
+ var PLACEHOLDER_TEXT2 = "Type to start writing, or press / to insert an action.";
7794
+ var pluginKey2 = new PluginKey7("placeholder");
7795
+ function buildDecorations4(state) {
7796
+ const { doc: doc2, selection } = state;
7797
+ if (!selection.empty) return DecorationSet4.empty;
7798
+ const $from = selection.$from;
7799
+ const parent = $from.parent;
7800
+ if (parent.type.name !== "paragraph" || parent.content.size !== 0) {
7801
+ return DecorationSet4.empty;
7436
7802
  }
7437
- emptyEnterCount = 0;
7438
- if (!dispatch) return true;
7439
- const paragraphStart = $from.before($from.depth);
7440
- const paragraphEnd = $from.after($from.depth);
7441
- const tr = state.tr.delete(paragraphStart, paragraphEnd);
7442
- const mappedFrom = tr.mapping.map($from.before($from.depth));
7443
- const $mapped = tr.doc.resolve(Math.min(mappedFrom, tr.doc.content.size));
7444
- for (let d = $mapped.depth; d > 0; d--) {
7445
- if ($mapped.node(d).type.name === "noteBlock") {
7446
- const lastChild = $mapped.node(d).lastChild;
7447
- if (lastChild && lastChild.type.name === "paragraph" && lastChild.content.size === 0) {
7448
- const lastChildPos = $mapped.end(d) - lastChild.nodeSize;
7449
- tr.delete(lastChildPos, lastChildPos + lastChild.nodeSize);
7450
- }
7451
- break;
7803
+ const EXCLUDED_ANCESTORS = /* @__PURE__ */ new Set(["noteBlock", "blockquote", "jinjaIfBranch"]);
7804
+ for (let d = $from.depth - 1; d > 0; d--) {
7805
+ if (EXCLUDED_ANCESTORS.has($from.node(d).type.name)) {
7806
+ return DecorationSet4.empty;
7452
7807
  }
7453
7808
  }
7454
- const noteBlockEnd = tr.mapping.map($from.after(noteBlockDepth));
7455
- const newParagraph = state.schema.nodes.paragraph.create();
7456
- tr.insert(noteBlockEnd, newParagraph);
7457
- tr.setSelection(Selection.near(tr.doc.resolve(noteBlockEnd + 1)));
7458
- dispatch(tr.scrollIntoView());
7459
- return true;
7460
- };
7461
- function createNoteBlockPlugin() {
7809
+ const pos = $from.before($from.depth);
7810
+ const deco = Decoration4.node(pos, pos + parent.nodeSize, {
7811
+ class: "ab-empty-paragraph",
7812
+ "data-placeholder": PLACEHOLDER_TEXT2
7813
+ });
7814
+ return DecorationSet4.create(doc2, [deco]);
7815
+ }
7816
+ function createPlaceholderPlugin() {
7462
7817
  return {
7463
- name: "noteBlockNodeView",
7464
- keymap: () => ({
7465
- Enter: exitNoteBlockOnEnter
7466
- }),
7467
- nodeViews: () => ({
7468
- noteBlock: (node, view, getPos) => new NoteBlockView(node, view, getPos)
7469
- })
7818
+ name: "placeholder",
7819
+ plugins: () => [
7820
+ new Plugin7({
7821
+ key: pluginKey2,
7822
+ state: {
7823
+ init(_, state) {
7824
+ return buildDecorations4(state);
7825
+ },
7826
+ apply(tr, oldSet, _oldState, newState) {
7827
+ if (tr.docChanged || tr.selectionSet) {
7828
+ return buildDecorations4(newState);
7829
+ }
7830
+ return oldSet;
7831
+ }
7832
+ },
7833
+ props: {
7834
+ decorations(state) {
7835
+ return pluginKey2.getState(state) ?? DecorationSet4.empty;
7836
+ }
7837
+ }
7838
+ })
7839
+ ]
7470
7840
  };
7471
7841
  }
7472
7842
 
7473
7843
  // src/ui/plugin/slashCommandPlugin.ts
7474
- import { Plugin as Plugin7, PluginKey as PluginKey7 } from "prosemirror-state";
7475
- var slashCommandKey = new PluginKey7("slashCommand");
7844
+ import { Plugin as Plugin8, PluginKey as PluginKey8 } from "prosemirror-state";
7845
+ var slashCommandKey = new PluginKey8("slashCommand");
7476
7846
  var TRIGGER_RE = /(?:^|\s)(\/[^\s]*)$/;
7477
7847
  function deriveState(state, dismissedFrom) {
7848
+ const inactive = { active: false, range: null, query: "", parentType: "" };
7478
7849
  const { selection } = state;
7479
- if (!selection.empty) return { active: false, range: null, query: "" };
7850
+ if (!selection.empty) return inactive;
7480
7851
  const { $from } = selection;
7481
7852
  const parentType = $from.parent.type.name;
7482
- if (parentType === "heading") return { active: false, range: null, query: "" };
7853
+ for (let d = $from.depth; d > 0; d--) {
7854
+ if ($from.node(d).type.name === "noteBlock") return inactive;
7855
+ }
7483
7856
  const textBefore = $from.parent.textBetween(0, $from.parentOffset, "\n", "\0");
7484
7857
  const match = TRIGGER_RE.exec(textBefore);
7485
- if (!match) return { active: false, range: null, query: "" };
7858
+ if (!match) return inactive;
7486
7859
  const slashText = match[1];
7487
7860
  const slashStartInParent = textBefore.lastIndexOf(slashText);
7488
7861
  const from = $from.start() + slashStartInParent;
7489
7862
  const to = $from.pos;
7490
- if (dismissedFrom === from) return { active: false, range: null, query: "" };
7863
+ if (dismissedFrom === from) return inactive;
7491
7864
  return {
7492
7865
  active: true,
7493
7866
  range: { from, to },
7494
- query: slashText.slice(1)
7867
+ query: slashText.slice(1),
7868
+ parentType
7495
7869
  };
7496
7870
  }
7497
7871
  function createSlashCommandPlugin() {
7498
- const plugin = new Plugin7({
7872
+ const plugin = new Plugin8({
7499
7873
  key: slashCommandKey,
7500
7874
  state: {
7501
7875
  init(_, state) {
@@ -8013,6 +8387,7 @@ function DocumentTreeView({ doc: doc2, className, onNavigate }) {
8013
8387
  // src/ui/components/FloatingMenu.tsx
8014
8388
  import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
8015
8389
  import { createPortal as createPortal2 } from "react-dom";
8390
+ import { TextSelection as TextSelection6 } from "prosemirror-state";
8016
8391
  import { setBlockType as setBlockType2, toggleMark as toggleMark2, wrapIn } from "prosemirror-commands";
8017
8392
  import { wrapInList as wrapInList2 } from "prosemirror-schema-list";
8018
8393
  import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
@@ -8035,6 +8410,56 @@ function hasMark(state, type) {
8035
8410
  if (empty) return false;
8036
8411
  return state.doc.rangeHasMark(from, to, type);
8037
8412
  }
8413
+ function getMarkRange2($pos, type) {
8414
+ const { parentOffset } = $pos;
8415
+ let child = $pos.parent.childAfter(parentOffset);
8416
+ if (parentOffset === child.offset && child.offset !== 0) {
8417
+ child = $pos.parent.childBefore(parentOffset);
8418
+ }
8419
+ if (!child.node) return null;
8420
+ const mark = child.node.marks.find((candidate) => candidate.type === type);
8421
+ if (!mark) return null;
8422
+ const parentStart = $pos.start();
8423
+ let { index: startIndex } = child;
8424
+ let startPos = parentStart + child.offset;
8425
+ let endIndex = startIndex + 1;
8426
+ let endPos = startPos + child.node.nodeSize;
8427
+ while (startIndex > 0 && mark.isInSet($pos.parent.child(startIndex - 1).marks)) {
8428
+ startIndex -= 1;
8429
+ startPos -= $pos.parent.child(startIndex).nodeSize;
8430
+ }
8431
+ while (endIndex < $pos.parent.childCount && mark.isInSet($pos.parent.child(endIndex).marks)) {
8432
+ endPos += $pos.parent.child(endIndex).nodeSize;
8433
+ endIndex += 1;
8434
+ }
8435
+ return { from: startPos, to: endPos, mark };
8436
+ }
8437
+ function getLinkAtCursor(state) {
8438
+ const { selection, doc: doc2 } = state;
8439
+ if (!selection.empty) return null;
8440
+ const range = getMarkRange2(doc2.resolve(selection.from), linkMark);
8441
+ if (!range) return null;
8442
+ return {
8443
+ href: range.mark.attrs.href || "",
8444
+ text: doc2.textBetween(range.from, range.to),
8445
+ from: range.from,
8446
+ to: range.to
8447
+ };
8448
+ }
8449
+ function normalizeLinkHref(href) {
8450
+ const trimmedHref = href.trim();
8451
+ if (!trimmedHref) return "";
8452
+ return trimmedHref.startsWith("#") || trimmedHref.startsWith("/") || /^https?:\/\//i.test(trimmedHref) ? trimmedHref : `https://${trimmedHref}`;
8453
+ }
8454
+ function getNonLinkMarksInRange(state, from, to) {
8455
+ let marks = [];
8456
+ state.doc.nodesBetween(from, to, (node) => {
8457
+ if (!node.isText) return;
8458
+ marks = node.marks.filter((mark) => mark.type !== linkMark);
8459
+ return false;
8460
+ });
8461
+ return marks;
8462
+ }
8038
8463
  function activeHeadingLevel(state) {
8039
8464
  const { $from } = state.selection;
8040
8465
  const node = $from.parent;
@@ -8497,6 +8922,408 @@ function SelectionToolbar({ view, state, selectionRect }) {
8497
8922
  }
8498
8923
  );
8499
8924
  }
8925
+ function LinkHoverTooltip({ view, link: link2, onEdit }) {
8926
+ const [hover, setHover] = useState7(false);
8927
+ const startCoords = view.coordsAtPos(link2.from);
8928
+ const endCoords = view.coordsAtPos(link2.to);
8929
+ const anchorTop = Math.min(startCoords.top, endCoords.top);
8930
+ const linkLeft = Math.min(startCoords.left, endCoords.left);
8931
+ const linkRight = Math.max(startCoords.right, endCoords.right);
8932
+ const tooltipHalfW = Math.min(174, Math.max(0, (window.innerWidth - VPORT_MARGIN2 * 2) / 2));
8933
+ const safeCenterX = Math.max(
8934
+ VPORT_MARGIN2 + tooltipHalfW,
8935
+ Math.min((linkLeft + linkRight) / 2, window.innerWidth - VPORT_MARGIN2 - tooltipHalfW)
8936
+ );
8937
+ return createPortal2(
8938
+ /* @__PURE__ */ jsxs9(
8939
+ "div",
8940
+ {
8941
+ ...{ [FLOAT_ATTR]: "" },
8942
+ style: {
8943
+ position: "fixed",
8944
+ left: safeCenterX,
8945
+ top: Math.max(64, anchorTop - 4),
8946
+ transform: "translate(-50%, -100%)",
8947
+ zIndex: 1e3,
8948
+ display: "flex",
8949
+ flexDirection: "column",
8950
+ alignItems: "center",
8951
+ animation: "ab-float-in 0.12s ease",
8952
+ pointerEvents: "auto"
8953
+ },
8954
+ children: [
8955
+ /* @__PURE__ */ jsxs9(
8956
+ "div",
8957
+ {
8958
+ ...{ [FLOAT_ATTR]: "" },
8959
+ style: {
8960
+ display: "flex",
8961
+ alignItems: "center",
8962
+ gap: 4,
8963
+ minHeight: 28,
8964
+ maxWidth: "min(348px, calc(100vw - 16px))",
8965
+ padding: "6px 8px",
8966
+ background: "#2E2E2E",
8967
+ borderRadius: 4,
8968
+ boxSizing: "border-box"
8969
+ },
8970
+ children: [
8971
+ /* @__PURE__ */ jsx10(
8972
+ "span",
8973
+ {
8974
+ ...{ [FLOAT_ATTR]: "" },
8975
+ style: {
8976
+ color: "#FFFFFF",
8977
+ fontSize: 12,
8978
+ lineHeight: "16px",
8979
+ fontFamily: '"SF Pro Text", "SF Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
8980
+ whiteSpace: "normal",
8981
+ wordBreak: "break-word",
8982
+ overflowWrap: "anywhere",
8983
+ maxWidth: 300
8984
+ },
8985
+ children: link2.href
8986
+ }
8987
+ ),
8988
+ /* @__PURE__ */ jsx10(
8989
+ "div",
8990
+ {
8991
+ ...{ [FLOAT_ATTR]: "" },
8992
+ style: {
8993
+ width: 1,
8994
+ height: 16,
8995
+ background: "#424242",
8996
+ flexShrink: 0
8997
+ }
8998
+ }
8999
+ ),
9000
+ /* @__PURE__ */ jsx10(
9001
+ "button",
9002
+ {
9003
+ type: "button",
9004
+ ...{ [FLOAT_ATTR]: "" },
9005
+ onMouseDown: (e) => {
9006
+ e.preventDefault();
9007
+ onEdit(link2);
9008
+ },
9009
+ onMouseEnter: () => setHover(true),
9010
+ onMouseLeave: () => setHover(false),
9011
+ style: {
9012
+ ...BTN_RESET2,
9013
+ padding: "0 2px",
9014
+ color: "#FFFFFF",
9015
+ fontSize: 12,
9016
+ lineHeight: "16px",
9017
+ fontWeight: 600,
9018
+ fontFamily: '"SF Pro Text", "SF Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
9019
+ opacity: hover ? 0.78 : 1,
9020
+ flexShrink: 0
9021
+ },
9022
+ children: "Edit"
9023
+ }
9024
+ )
9025
+ ]
9026
+ }
9027
+ ),
9028
+ /* @__PURE__ */ jsx10(
9029
+ "div",
9030
+ {
9031
+ ...{ [FLOAT_ATTR]: "" },
9032
+ style: {
9033
+ width: 0,
9034
+ height: 0,
9035
+ borderLeft: "6px solid transparent",
9036
+ borderRight: "6px solid transparent",
9037
+ borderTop: "6px solid #2E2E2E",
9038
+ marginTop: -1
9039
+ }
9040
+ }
9041
+ )
9042
+ ]
9043
+ }
9044
+ ),
9045
+ document.body
9046
+ );
9047
+ }
9048
+ function LinkEditDialog({ view, link: link2, onClose }) {
9049
+ const [textValue, setTextValue] = useState7(link2.text);
9050
+ const [hrefValue, setHrefValue] = useState7(link2.href);
9051
+ const textInputRef = useRef4(null);
9052
+ useEffect4(() => {
9053
+ setTextValue(link2.text);
9054
+ setHrefValue(link2.href);
9055
+ }, [link2.from, link2.href, link2.text, link2.to]);
9056
+ useEffect4(() => {
9057
+ setTimeout(() => textInputRef.current?.focus(), 0);
9058
+ }, [link2.from, link2.to]);
9059
+ useEffect4(() => {
9060
+ const onDown = (e) => {
9061
+ if (!e.target.closest(`[${FLOAT_ATTR}]`)) {
9062
+ onClose();
9063
+ }
9064
+ };
9065
+ document.addEventListener("mousedown", onDown, true);
9066
+ return () => document.removeEventListener("mousedown", onDown, true);
9067
+ }, [onClose]);
9068
+ const startCoords = view.coordsAtPos(link2.from);
9069
+ const endCoords = view.coordsAtPos(link2.to);
9070
+ const anchorTop = Math.min(startCoords.top, endCoords.top);
9071
+ const anchorBottom = Math.max(startCoords.bottom, endCoords.bottom);
9072
+ const linkLeft = Math.min(startCoords.left, endCoords.left);
9073
+ const linkRight = Math.max(startCoords.right, endCoords.right);
9074
+ const dialogW = 280;
9075
+ const estimatedDialogH = 188;
9076
+ const centerX = (linkLeft + linkRight) / 2;
9077
+ const left = Math.max(
9078
+ VPORT_MARGIN2,
9079
+ Math.min(centerX - dialogW / 2, window.innerWidth - dialogW - VPORT_MARGIN2)
9080
+ );
9081
+ let top = anchorTop - estimatedDialogH - 12;
9082
+ if (top < VPORT_MARGIN2) {
9083
+ top = Math.min(anchorBottom + 12, window.innerHeight - estimatedDialogH - VPORT_MARGIN2);
9084
+ }
9085
+ const finalHref = normalizeLinkHref(hrefValue);
9086
+ const finalText = textValue.trim() ? textValue : finalHref;
9087
+ const canSave = Boolean(finalHref) && (finalHref !== link2.href || finalText !== link2.text);
9088
+ const currentLink = () => {
9089
+ const activeLink = getLinkAtCursor(view.state);
9090
+ if (activeLink && activeLink.from === link2.from && activeLink.to === link2.to) {
9091
+ return activeLink;
9092
+ }
9093
+ return link2;
9094
+ };
9095
+ const saveLink = () => {
9096
+ const activeLink = currentLink();
9097
+ const nextHref = normalizeLinkHref(hrefValue);
9098
+ const nextText = textValue.trim() ? textValue : nextHref;
9099
+ if (!nextHref) return;
9100
+ if (nextHref === activeLink.href && nextText === activeLink.text) return;
9101
+ const cursorOffset = Math.max(0, view.state.selection.from - activeLink.from);
9102
+ const tr = view.state.tr;
9103
+ if (nextText === activeLink.text) {
9104
+ tr.removeMark(activeLink.from, activeLink.to, linkMark);
9105
+ tr.addMark(activeLink.from, activeLink.to, linkMark.create({ href: nextHref }));
9106
+ const nextCursor = Math.min(activeLink.from + cursorOffset, activeLink.to);
9107
+ tr.setSelection(TextSelection6.near(tr.doc.resolve(nextCursor)));
9108
+ } else {
9109
+ const preservedMarks = getNonLinkMarksInRange(view.state, activeLink.from, activeLink.to);
9110
+ tr.insertText(nextText, activeLink.from, activeLink.to);
9111
+ const nextTo = activeLink.from + nextText.length;
9112
+ tr.removeMark(activeLink.from, nextTo, linkMark);
9113
+ for (const mark of preservedMarks) {
9114
+ tr.addMark(activeLink.from, nextTo, mark);
9115
+ }
9116
+ tr.addMark(activeLink.from, nextTo, linkMark.create({ href: nextHref }));
9117
+ const nextCursor = Math.min(activeLink.from + cursorOffset, nextTo);
9118
+ tr.setSelection(TextSelection6.near(tr.doc.resolve(nextCursor)));
9119
+ }
9120
+ view.dispatch(tr);
9121
+ onClose();
9122
+ view.focus();
9123
+ };
9124
+ const removeLink = () => {
9125
+ const activeLink = currentLink();
9126
+ view.dispatch(view.state.tr.removeMark(activeLink.from, activeLink.to, linkMark));
9127
+ onClose();
9128
+ view.focus();
9129
+ };
9130
+ const handleKeyDown = (e) => {
9131
+ if (e.key === "Escape") {
9132
+ e.preventDefault();
9133
+ onClose();
9134
+ view.focus();
9135
+ return;
9136
+ }
9137
+ if (e.key === "Enter") {
9138
+ e.preventDefault();
9139
+ saveLink();
9140
+ }
9141
+ };
9142
+ return createPortal2(
9143
+ /* @__PURE__ */ jsxs9(
9144
+ "div",
9145
+ {
9146
+ ...{ [FLOAT_ATTR]: "" },
9147
+ style: {
9148
+ position: "fixed",
9149
+ left,
9150
+ top,
9151
+ zIndex: 1001,
9152
+ width: dialogW,
9153
+ display: "flex",
9154
+ flexDirection: "column",
9155
+ gap: 16,
9156
+ padding: 16,
9157
+ background: "#FFFFFF",
9158
+ borderRadius: 4,
9159
+ boxShadow: "0px 8px 10px rgba(13,13,13,0.12), 0px 3px 14px rgba(13,13,13,0.08), 0px 3px 5px rgba(13,13,13,0.04)",
9160
+ boxSizing: "border-box",
9161
+ animation: "ab-float-in 0.12s ease"
9162
+ },
9163
+ children: [
9164
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
9165
+ /* @__PURE__ */ jsxs9(
9166
+ "div",
9167
+ {
9168
+ ...{ [FLOAT_ATTR]: "" },
9169
+ style: {
9170
+ fontSize: 12,
9171
+ lineHeight: "16px",
9172
+ fontWeight: 600,
9173
+ color: "#0D0D0D"
9174
+ },
9175
+ children: [
9176
+ "Text ",
9177
+ /* @__PURE__ */ jsx10("span", { style: { color: "#858585" }, children: "(optional)" })
9178
+ ]
9179
+ }
9180
+ ),
9181
+ /* @__PURE__ */ jsx10(
9182
+ "input",
9183
+ {
9184
+ ref: textInputRef,
9185
+ ...{ [FLOAT_ATTR]: "" },
9186
+ value: textValue,
9187
+ onChange: (e) => setTextValue(e.target.value),
9188
+ onKeyDown: handleKeyDown,
9189
+ style: {
9190
+ height: 32,
9191
+ border: "1px solid #CCCCCC",
9192
+ borderRadius: 4,
9193
+ padding: "0 16px",
9194
+ fontSize: 14,
9195
+ color: "#0D0D0D",
9196
+ outline: "none",
9197
+ boxSizing: "border-box"
9198
+ }
9199
+ }
9200
+ )
9201
+ ] }),
9202
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
9203
+ /* @__PURE__ */ jsx10(
9204
+ "div",
9205
+ {
9206
+ ...{ [FLOAT_ATTR]: "" },
9207
+ style: {
9208
+ fontSize: 12,
9209
+ lineHeight: "16px",
9210
+ fontWeight: 600,
9211
+ color: "#0D0D0D"
9212
+ },
9213
+ children: "Link"
9214
+ }
9215
+ ),
9216
+ /* @__PURE__ */ jsx10(
9217
+ "input",
9218
+ {
9219
+ ...{ [FLOAT_ATTR]: "" },
9220
+ value: hrefValue,
9221
+ onChange: (e) => setHrefValue(e.target.value),
9222
+ onKeyDown: handleKeyDown,
9223
+ style: {
9224
+ height: 32,
9225
+ border: "1px solid #CCCCCC",
9226
+ borderRadius: 4,
9227
+ padding: "0 16px",
9228
+ fontSize: 14,
9229
+ color: "#0D0D0D",
9230
+ outline: "none",
9231
+ boxSizing: "border-box"
9232
+ }
9233
+ }
9234
+ )
9235
+ ] }),
9236
+ /* @__PURE__ */ jsxs9(
9237
+ "div",
9238
+ {
9239
+ ...{ [FLOAT_ATTR]: "" },
9240
+ style: {
9241
+ display: "flex",
9242
+ alignItems: "center",
9243
+ justifyContent: "space-between",
9244
+ gap: 12
9245
+ },
9246
+ children: [
9247
+ /* @__PURE__ */ jsx10(
9248
+ "button",
9249
+ {
9250
+ type: "button",
9251
+ ...{ [FLOAT_ATTR]: "" },
9252
+ onMouseDown: (e) => {
9253
+ e.preventDefault();
9254
+ removeLink();
9255
+ },
9256
+ style: {
9257
+ ...BTN_RESET2,
9258
+ fontSize: 14,
9259
+ lineHeight: "20px",
9260
+ fontWeight: 600,
9261
+ color: "#0D0D0D"
9262
+ },
9263
+ children: "Remove link"
9264
+ }
9265
+ ),
9266
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { display: "flex", alignItems: "center", gap: 8 }, children: [
9267
+ /* @__PURE__ */ jsx10(
9268
+ "button",
9269
+ {
9270
+ type: "button",
9271
+ ...{ [FLOAT_ATTR]: "" },
9272
+ onMouseDown: (e) => {
9273
+ e.preventDefault();
9274
+ onClose();
9275
+ view.focus();
9276
+ },
9277
+ style: {
9278
+ ...BTN_RESET2,
9279
+ height: 32,
9280
+ padding: "0 13px",
9281
+ border: "1px solid #CCCCCC",
9282
+ borderRadius: 4,
9283
+ fontSize: 14,
9284
+ lineHeight: "20px",
9285
+ fontWeight: 600,
9286
+ color: "#0D0D0D"
9287
+ },
9288
+ children: "Cancel"
9289
+ }
9290
+ ),
9291
+ /* @__PURE__ */ jsx10(
9292
+ "button",
9293
+ {
9294
+ type: "button",
9295
+ disabled: !canSave,
9296
+ ...{ [FLOAT_ATTR]: "" },
9297
+ onMouseDown: (e) => {
9298
+ e.preventDefault();
9299
+ if (!canSave) return;
9300
+ saveLink();
9301
+ },
9302
+ style: {
9303
+ ...BTN_RESET2,
9304
+ height: 32,
9305
+ padding: "0 13px",
9306
+ borderRadius: 4,
9307
+ fontSize: 14,
9308
+ lineHeight: "20px",
9309
+ fontWeight: 600,
9310
+ background: canSave ? "#6210CC" : "#ECECEC",
9311
+ color: canSave ? "#FFFFFF" : "#A6A6A6",
9312
+ cursor: canSave ? "pointer" : "default"
9313
+ },
9314
+ children: "Save"
9315
+ }
9316
+ )
9317
+ ] })
9318
+ ]
9319
+ }
9320
+ )
9321
+ ]
9322
+ }
9323
+ ),
9324
+ document.body
9325
+ );
9326
+ }
8500
9327
  var BLOCK_ITEMS = [
8501
9328
  {
8502
9329
  shortLabel: "\xB6",
@@ -8715,6 +9542,7 @@ function BlockMenuItem({
8715
9542
  }
8716
9543
  function FloatingMenu({ view, editorState }) {
8717
9544
  const [showEmptyHandle, setShowEmptyHandle] = useState7(false);
9545
+ const [editingLink, setEditingLink] = useState7(null);
8718
9546
  const dwellTimerRef = useRef4(null);
8719
9547
  const lastEmptyPosRef = useRef4(null);
8720
9548
  const [, setScrollTick] = useState7(0);
@@ -8740,9 +9568,16 @@ function FloatingMenu({ view, editorState }) {
8740
9568
  `;
8741
9569
  document.head.appendChild(style);
8742
9570
  }, []);
9571
+ const emptyPos = editorState ? emptyBlockCursorPos(editorState) : null;
9572
+ const hasSelection = editorState ? !editorState.selection.empty : false;
9573
+ const linkAtCursor = editorState && !hasSelection ? getLinkAtCursor(editorState) : null;
9574
+ useEffect4(() => {
9575
+ if (!editingLink) return;
9576
+ if (!linkAtCursor || linkAtCursor.from !== editingLink.from || linkAtCursor.to !== editingLink.to) {
9577
+ setEditingLink(null);
9578
+ }
9579
+ }, [editingLink, linkAtCursor]);
8743
9580
  if (!view || !editorState) return null;
8744
- const emptyPos = emptyBlockCursorPos(editorState);
8745
- const hasSelection = !editorState.selection.empty;
8746
9581
  if (emptyPos !== lastEmptyPosRef.current) {
8747
9582
  lastEmptyPosRef.current = emptyPos;
8748
9583
  if (dwellTimerRef.current) {
@@ -8766,23 +9601,39 @@ function FloatingMenu({ view, editorState }) {
8766
9601
  selectionRect
8767
9602
  }
8768
9603
  ),
8769
- !hasSelection && showEmptyHandle && emptyPos !== null && /* @__PURE__ */ jsx10(EmptyParaHandle, { view, cursorPos: emptyPos })
9604
+ !hasSelection && linkAtCursor && !editingLink && /* @__PURE__ */ jsx10(
9605
+ LinkHoverTooltip,
9606
+ {
9607
+ view,
9608
+ link: linkAtCursor,
9609
+ onEdit: (link2) => setEditingLink(link2)
9610
+ }
9611
+ ),
9612
+ !hasSelection && editingLink && /* @__PURE__ */ jsx10(
9613
+ LinkEditDialog,
9614
+ {
9615
+ view,
9616
+ link: editingLink,
9617
+ onClose: () => setEditingLink(null)
9618
+ }
9619
+ ),
9620
+ !hasSelection && !linkAtCursor && showEmptyHandle && emptyPos !== null && /* @__PURE__ */ jsx10(EmptyParaHandle, { view, cursorPos: emptyPos })
8770
9621
  ] }),
8771
9622
  document.body
8772
9623
  );
8773
9624
  }
8774
9625
 
8775
9626
  // src/ui/plugin/inlineSuggestPlugin.ts
8776
- import { Plugin as Plugin8, PluginKey as PluginKey8 } from "prosemirror-state";
8777
- import { Decoration as Decoration4, DecorationSet as DecorationSet4 } from "prosemirror-view";
8778
- var inlineSuggestKey = new PluginKey8("inlineSuggest");
9627
+ import { Plugin as Plugin9, PluginKey as PluginKey9 } from "prosemirror-state";
9628
+ import { Decoration as Decoration5, DecorationSet as DecorationSet5 } from "prosemirror-view";
9629
+ var inlineSuggestKey = new PluginKey9("inlineSuggest");
8779
9630
  var DEBOUNCE_MS = 600;
8780
9631
  function createInlineSuggestPlugin(provider, endpoint, options) {
8781
9632
  const { onContextChange } = options ?? {};
8782
9633
  return {
8783
9634
  name: "inlineSuggest",
8784
9635
  plugins: () => [
8785
- new Plugin8({
9636
+ new Plugin9({
8786
9637
  key: inlineSuggestKey,
8787
9638
  state: {
8788
9639
  init: () => ({ suggestion: null, anchorPos: null }),
@@ -8798,13 +9649,13 @@ function createInlineSuggestPlugin(provider, endpoint, options) {
8798
9649
  props: {
8799
9650
  decorations(state) {
8800
9651
  const ps = inlineSuggestKey.getState(state);
8801
- if (!ps?.suggestion || !state.selection.empty) return DecorationSet4.empty;
9652
+ if (!ps?.suggestion || !state.selection.empty) return DecorationSet5.empty;
8802
9653
  const ghost = document.createElement("span");
8803
9654
  ghost.textContent = ps.suggestion;
8804
9655
  ghost.style.cssText = "color: rgba(0,0,0,0.3); pointer-events: none; user-select: none;";
8805
9656
  ghost.setAttribute("aria-hidden", "true");
8806
- return DecorationSet4.create(state.doc, [
8807
- Decoration4.widget(state.selection.from, ghost, {
9657
+ return DecorationSet5.create(state.doc, [
9658
+ Decoration5.widget(state.selection.from, ghost, {
8808
9659
  side: 1,
8809
9660
  key: "inline-suggest"
8810
9661
  })
@@ -8959,6 +9810,7 @@ export {
8959
9810
  createLinkPlugin,
8960
9811
  createMarkdownClipboardPlugin,
8961
9812
  createNoteBlockPlugin,
9813
+ createPlaceholderPlugin,
8962
9814
  createPluginArray,
8963
9815
  createReactNodeView,
8964
9816
  createSlashCommandPlugin,