@sendbird/actionbook-core 0.9.3 → 0.9.8

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,64 @@ 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
+ var NOOP = /* @__PURE__ */ Symbol("noop");
2749
+ function handleListInputRule(state, start, end, listType, attrs) {
2750
+ const resolvePos = Math.max(start, end - 1);
2751
+ const parentList = findParentList(state, resolvePos);
2752
+ if (!parentList) return null;
2753
+ const $from = state.doc.resolve(resolvePos);
2754
+ const { depth: listDepth, node: listNode } = parentList;
2755
+ const paraContentSize = $from.parent.content.size;
2756
+ const matchedLen = end - start;
2757
+ if (paraContentSize !== matchedLen) return null;
2758
+ if (listNode.type === listType) {
2759
+ return NOOP;
2760
+ }
2761
+ const { listItem: liType } = actionbookSchema.nodes;
2762
+ const tr = state.tr.delete(start, end);
2763
+ const $pos = tr.doc.resolve(tr.mapping.map(resolvePos));
2764
+ let liDepth = -1;
2765
+ for (let d = $pos.depth; d > 0; d--) {
2766
+ if ($pos.node(d).type === liType) {
2767
+ liDepth = d;
2768
+ break;
2769
+ }
2770
+ }
2771
+ if (liDepth < 0) return null;
2772
+ const parentListAfterDelete = $pos.node(liDepth - 1);
2773
+ const liIndex = $pos.index(liDepth - 1);
2774
+ const liNode = parentListAfterDelete.child(liIndex);
2775
+ const parentListStart = $pos.before(liDepth - 1);
2776
+ const parentListEnd = $pos.after(liDepth - 1);
2777
+ const fragments = [];
2778
+ if (liIndex > 0) {
2779
+ const beforeItems = [];
2780
+ for (let i = 0; i < liIndex; i++) beforeItems.push(parentListAfterDelete.child(i));
2781
+ fragments.push(parentListAfterDelete.type.create(parentListAfterDelete.attrs, beforeItems));
2782
+ }
2783
+ fragments.push(listType.create(attrs ?? null, liNode));
2784
+ if (liIndex < parentListAfterDelete.childCount - 1) {
2785
+ const afterItems = [];
2786
+ for (let i = liIndex + 1; i < parentListAfterDelete.childCount; i++) {
2787
+ afterItems.push(parentListAfterDelete.child(i));
2788
+ }
2789
+ fragments.push(parentListAfterDelete.type.create(parentListAfterDelete.attrs, afterItems));
2790
+ }
2791
+ tr.replaceWith(parentListStart, parentListEnd, fragments);
2792
+ tr.setSelection(TextSelection.near(tr.doc.resolve(tr.mapping.map(start))));
2793
+ return tr;
2794
+ }
2737
2795
  function markInputRule(pattern, markType, markerLen) {
2738
2796
  return new InputRule(pattern, (state, match, start, end) => {
2739
2797
  const textContent2 = match[1];
@@ -2798,12 +2856,32 @@ function createInputRulesPlugin() {
2798
2856
  return state.tr.delete(start, end).insert(start, list);
2799
2857
  })
2800
2858
  );
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
- );
2859
+ {
2860
+ const blType = actionbookSchema.nodes.bulletList;
2861
+ const fallback = wrappingInputRule(BULLET_LIST_RE, blType);
2862
+ rules.push(
2863
+ new InputRule(BULLET_LIST_RE, (state, match, start, end) => {
2864
+ const result = handleListInputRule(state, start, end, blType);
2865
+ if (result === NOOP) return null;
2866
+ if (result) return result;
2867
+ const handler = fallback.handler;
2868
+ return handler(state, match, start, end);
2869
+ })
2870
+ );
2871
+ }
2872
+ {
2873
+ const olType = actionbookSchema.nodes.orderedList;
2874
+ const fallback = wrappingInputRule(ORDERED_LIST_RE, olType, (m) => ({ start: +m[1] }));
2875
+ rules.push(
2876
+ new InputRule(ORDERED_LIST_RE, (state, match, start, end) => {
2877
+ const result = handleListInputRule(state, start, end, olType, { start: +match[1] });
2878
+ if (result === NOOP) return null;
2879
+ if (result) return result;
2880
+ const handler = fallback.handler;
2881
+ return handler(state, match, start, end);
2882
+ })
2883
+ );
2884
+ }
2807
2885
  const jumpPointType = actionbookSchema.nodes.jumpPoint;
2808
2886
  rules.push(
2809
2887
  new InputRule(JUMP_POINT_RE2, (state, match, start, end) => {
@@ -2816,7 +2894,12 @@ function createInputRulesPlugin() {
2816
2894
  }
2817
2895
  });
2818
2896
  if (exists) return null;
2819
- return state.tr.delete(start, end).insert(start, jumpPointType.create({ id }));
2897
+ const tr = state.tr.delete(start, end).insert(start, jumpPointType.create({ id }));
2898
+ const afterAtom = start + 1;
2899
+ tr.insertText(" ", afterAtom);
2900
+ const cursorPos = afterAtom + 1;
2901
+ tr.setSelection(TextSelection.near(tr.doc.resolve(Math.min(cursorPos, tr.doc.content.size))));
2902
+ return tr;
2820
2903
  })
2821
2904
  );
2822
2905
  rules.push(markInputRule(BOLD_STAR_RE, actionbookSchema.marks.bold, 2));
@@ -2845,8 +2928,100 @@ function createHistoryPlugin() {
2845
2928
 
2846
2929
  // src/ui/plugin/keymapPlugin.ts
2847
2930
  import { baseKeymap, chainCommands, newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock, toggleMark, setBlockType, joinBackward } from "prosemirror-commands";
2931
+ import { TextSelection as TextSelection2 } from "prosemirror-state";
2848
2932
  import { keymap as keymap3 } from "prosemirror-keymap";
2849
2933
  import { liftListItem, sinkListItem, splitListItem, wrapInList } from "prosemirror-schema-list";
2934
+
2935
+ // src/ui/plugin/noteBlockPlugin.tsx
2936
+ import { Selection } from "prosemirror-state";
2937
+ var NoteBlockView = class {
2938
+ dom;
2939
+ contentDOM;
2940
+ constructor(_node, _view, _getPos) {
2941
+ this.dom = document.createElement("div");
2942
+ this.dom.setAttribute("data-note-block", "");
2943
+ this.dom.className = "ab-note-block";
2944
+ this.dom.style.cssText = [
2945
+ "position: relative",
2946
+ "margin: 8px 0",
2947
+ "padding: 16px 24px",
2948
+ "background: #fff",
2949
+ "border: 1px dashed #ccc",
2950
+ "border-radius: 4px",
2951
+ "display: flex",
2952
+ "flex-direction: column",
2953
+ "gap: 4px"
2954
+ ].join(";");
2955
+ const label = document.createElement("span");
2956
+ label.contentEditable = "false";
2957
+ label.textContent = "Note";
2958
+ label.style.cssText = [
2959
+ "font-family: SF Pro Text, -apple-system, BlinkMacSystemFont, sans-serif",
2960
+ "display: inline-flex",
2961
+ "align-items: center",
2962
+ "justify-content: center",
2963
+ "align-self: flex-start",
2964
+ "font-size: 12px",
2965
+ "font-weight: 400",
2966
+ "font-style: normal",
2967
+ "line-height: 16px",
2968
+ "color: #858585",
2969
+ "background: #f7f7f7",
2970
+ "border: 1px solid #e0e0e0",
2971
+ "border-radius: 4px",
2972
+ "padding: 2px 4px",
2973
+ "user-select: none",
2974
+ "height: 20px"
2975
+ ].join(";");
2976
+ this.dom.appendChild(label);
2977
+ this.contentDOM = document.createElement("div");
2978
+ this.contentDOM.className = "ab-note-block-content";
2979
+ this.contentDOM.style.cssText = [
2980
+ "font-size: 14px",
2981
+ "font-style: italic",
2982
+ "font-weight: 400",
2983
+ "line-height: 20px",
2984
+ "letter-spacing: -0.1px",
2985
+ "color: #858585"
2986
+ ].join(";");
2987
+ this.dom.appendChild(this.contentDOM);
2988
+ }
2989
+ };
2990
+ var exitNoteBlockOnEnter = (state, dispatch) => {
2991
+ const { $from } = state.selection;
2992
+ let noteBlockDepth = -1;
2993
+ for (let d = $from.depth; d > 0; d--) {
2994
+ if ($from.node(d).type.name === "noteBlock") {
2995
+ noteBlockDepth = d;
2996
+ break;
2997
+ }
2998
+ }
2999
+ if (noteBlockDepth < 0) return false;
3000
+ const parentNode = $from.parent;
3001
+ if (parentNode.type.name !== "paragraph" || parentNode.content.size !== 0) {
3002
+ return false;
3003
+ }
3004
+ if (!dispatch) return true;
3005
+ const paragraphStart = $from.before($from.depth);
3006
+ const paragraphEnd = $from.after($from.depth);
3007
+ const tr = state.tr.delete(paragraphStart, paragraphEnd);
3008
+ const noteBlockEnd = tr.mapping.map($from.after(noteBlockDepth));
3009
+ const newParagraph = state.schema.nodes.paragraph.create();
3010
+ tr.insert(noteBlockEnd, newParagraph);
3011
+ tr.setSelection(Selection.near(tr.doc.resolve(noteBlockEnd + 1)));
3012
+ dispatch(tr.scrollIntoView());
3013
+ return true;
3014
+ };
3015
+ function createNoteBlockPlugin() {
3016
+ return {
3017
+ name: "noteBlockNodeView",
3018
+ nodeViews: () => ({
3019
+ noteBlock: (node, view, getPos) => new NoteBlockView(node, view, getPos)
3020
+ })
3021
+ };
3022
+ }
3023
+
3024
+ // src/ui/plugin/keymapPlugin.ts
2850
3025
  var TAB_CHAR = " ";
2851
3026
  var { listItem, bulletList, orderedList, hardBreak, heading, paragraph, horizontalRule, blockquote } = actionbookSchema.nodes;
2852
3027
  var { bold: boldMark, italic: italicMark, underline: underlineMark, strikethrough: strikethroughMark, code: codeMark } = actionbookSchema.marks;
@@ -2893,7 +3068,101 @@ var backspaceAfterLeafBlock = (state, dispatch) => {
2893
3068
  }
2894
3069
  return true;
2895
3070
  };
3071
+ var backspaceDeleteEmptyBlock = (state, dispatch) => {
3072
+ const { $from } = state.selection;
3073
+ if (!state.selection.empty || $from.parentOffset !== 0) return false;
3074
+ if ($from.parent.type !== paragraph || $from.parent.content.size !== 0) return false;
3075
+ const deletableTypes = /* @__PURE__ */ new Set(["jinjaIfBlock", "jinjaIfBranch", "noteBlock", "blockquote"]);
3076
+ for (let d = $from.depth - 1; d > 0; d--) {
3077
+ const ancestor = $from.node(d);
3078
+ if (!deletableTypes.has(ancestor.type.name)) continue;
3079
+ if (ancestor.type.name === "jinjaIfBranch" && ancestor.childCount === 1 && ancestor.child(0).content.size === 0) {
3080
+ for (let dd = d - 1; dd > 0; dd--) {
3081
+ if ($from.node(dd).type.name === "jinjaIfBlock") {
3082
+ d = dd;
3083
+ break;
3084
+ }
3085
+ }
3086
+ }
3087
+ const blockNode = $from.node(d);
3088
+ const blockText = blockNode.textContent.trim();
3089
+ if (blockText.length > 0) return false;
3090
+ if (dispatch) {
3091
+ const blockStart = $from.before(d);
3092
+ const blockEnd = $from.after(d);
3093
+ const tr = state.tr.replaceWith(blockStart, blockEnd, paragraph.create());
3094
+ tr.setSelection(TextSelection2.near(tr.doc.resolve(blockStart)));
3095
+ dispatch(tr.scrollIntoView());
3096
+ }
3097
+ return true;
3098
+ }
3099
+ return false;
3100
+ };
3101
+ var joinListItemBackward = (state, dispatch) => {
3102
+ const { $from } = state.selection;
3103
+ if (!state.selection.empty) return false;
3104
+ if ($from.parentOffset !== 0) return false;
3105
+ if (!cursorDirectlyInListItem(state)) return false;
3106
+ if ($from.index($from.depth - 1) !== 0) return false;
3107
+ const liDepth = $from.depth - 1;
3108
+ const listDepth = liDepth - 1;
3109
+ if (listDepth < 0) return false;
3110
+ const listNode = $from.node(listDepth);
3111
+ if (listNode.type !== bulletList && listNode.type !== orderedList) return false;
3112
+ const liIndex = $from.index(listDepth);
3113
+ if (liIndex > 0) {
3114
+ if (dispatch) {
3115
+ const joinPos = $from.before(liDepth);
3116
+ const tr = state.tr.join(joinPos);
3117
+ const mappedPos = tr.mapping.map(joinPos);
3118
+ if (tr.doc.resolve(mappedPos).nodeBefore?.isTextblock && tr.doc.resolve(mappedPos).nodeAfter?.isTextblock) {
3119
+ tr.join(mappedPos);
3120
+ }
3121
+ dispatch(tr.scrollIntoView());
3122
+ }
3123
+ return true;
3124
+ }
3125
+ const listStart = $from.before(listDepth);
3126
+ const parentOfList = $from.node(listDepth - 1);
3127
+ const listIndexInParent = $from.index(listDepth - 1);
3128
+ if (listIndexInParent > 0) {
3129
+ const prevBlock = parentOfList.child(listIndexInParent - 1);
3130
+ if (prevBlock.isTextblock) {
3131
+ if (dispatch) {
3132
+ const liNode = listNode.child(0);
3133
+ const firstPara = liNode.firstChild;
3134
+ const liStart = $from.before(liDepth);
3135
+ const liEnd = $from.after(liDepth);
3136
+ const tr = state.tr;
3137
+ if (listNode.childCount === 1) {
3138
+ tr.delete(listStart, $from.after(listDepth));
3139
+ } else {
3140
+ tr.delete(liStart, liEnd);
3141
+ }
3142
+ const prevBlockEnd = tr.mapping.map(listStart) - 1;
3143
+ if (firstPara.content.size > 0) {
3144
+ tr.insert(prevBlockEnd, firstPara.content);
3145
+ }
3146
+ if (liNode.childCount > 1) {
3147
+ const nestedContent = [];
3148
+ for (let i = 1; i < liNode.childCount; i++) {
3149
+ nestedContent.push(liNode.child(i));
3150
+ }
3151
+ const insertPos = tr.mapping.map(listStart);
3152
+ for (const node of nestedContent) {
3153
+ tr.insert(insertPos, node);
3154
+ }
3155
+ }
3156
+ tr.setSelection(TextSelection2.near(tr.doc.resolve(prevBlockEnd)));
3157
+ dispatch(tr.scrollIntoView());
3158
+ }
3159
+ return true;
3160
+ }
3161
+ }
3162
+ return liftListItem(listItem)(state, dispatch);
3163
+ };
2896
3164
  var backspaceCommand = chainCommands(
3165
+ backspaceDeleteEmptyBlock,
2897
3166
  backspaceAfterList,
2898
3167
  backspaceAfterLeafBlock,
2899
3168
  (state, dispatch) => {
@@ -2903,7 +3172,13 @@ var backspaceCommand = chainCommands(
2903
3172
  if ($from.parent.type === heading) {
2904
3173
  return setBlockType(paragraph)(state, dispatch);
2905
3174
  }
3175
+ return false;
3176
+ },
3177
+ joinListItemBackward,
3178
+ (state, dispatch) => {
2906
3179
  if (!cursorDirectlyInListItem(state)) return false;
3180
+ const { $from } = state.selection;
3181
+ if ($from.parentOffset !== 0) return false;
2907
3182
  if ($from.index($from.depth - 1) !== 0) return false;
2908
3183
  return liftListItem(listItem)(state, dispatch);
2909
3184
  },
@@ -2911,8 +3186,9 @@ var backspaceCommand = chainCommands(
2911
3186
  );
2912
3187
  var tabCommand = (state, dispatch) => {
2913
3188
  const { $from } = state.selection;
2914
- if (cursorDirectlyInListItem(state) && $from.parentOffset === 0) {
3189
+ if (cursorDirectlyInListItem(state)) {
2915
3190
  if (sinkListItem(listItem)(state, dispatch)) return true;
3191
+ return true;
2916
3192
  }
2917
3193
  if (dispatch) dispatch(state.tr.insertText(TAB_CHAR));
2918
3194
  return true;
@@ -2929,7 +3205,86 @@ var shiftTabCommand = (state, dispatch) => {
2929
3205
  }
2930
3206
  return true;
2931
3207
  };
3208
+ var blockExitEnterCount = 0;
3209
+ var blockExitLastTime = 0;
3210
+ var BLOCK_EXIT_TYPES = /* @__PURE__ */ new Set(["blockquote", "jinjaIfBranch"]);
3211
+ var exitBlockOnDoubleEnter = (state, dispatch) => {
3212
+ const { $from } = state.selection;
3213
+ let blockDepth = -1;
3214
+ for (let d = $from.depth; d > 0; d--) {
3215
+ if (BLOCK_EXIT_TYPES.has($from.node(d).type.name)) {
3216
+ blockDepth = d;
3217
+ break;
3218
+ }
3219
+ }
3220
+ if (blockDepth < 0) {
3221
+ blockExitEnterCount = 0;
3222
+ return false;
3223
+ }
3224
+ if ($from.parent.type.name !== "paragraph" || $from.parent.content.size !== 0) {
3225
+ blockExitEnterCount = 0;
3226
+ return false;
3227
+ }
3228
+ const now = Date.now();
3229
+ if (now - blockExitLastTime > 2e3) blockExitEnterCount = 0;
3230
+ blockExitLastTime = now;
3231
+ blockExitEnterCount++;
3232
+ if (blockExitEnterCount < 2) return false;
3233
+ blockExitEnterCount = 0;
3234
+ if (!dispatch) return true;
3235
+ const paraStart = $from.before($from.depth);
3236
+ const paraEnd = $from.after($from.depth);
3237
+ const tr = state.tr.delete(paraStart, paraEnd);
3238
+ const mapped = tr.mapping.map($from.before($from.depth));
3239
+ const $m = tr.doc.resolve(Math.min(mapped, tr.doc.content.size));
3240
+ for (let d = $m.depth; d > 0; d--) {
3241
+ if (BLOCK_EXIT_TYPES.has($m.node(d).type.name)) {
3242
+ const last = $m.node(d).lastChild;
3243
+ if (last?.type.name === "paragraph" && last.content.size === 0) {
3244
+ const lastPos = $m.end(d) - last.nodeSize;
3245
+ tr.delete(lastPos, lastPos + last.nodeSize);
3246
+ }
3247
+ break;
3248
+ }
3249
+ }
3250
+ let exitDepth = blockDepth;
3251
+ const blockTypeName = $from.node(blockDepth).type.name;
3252
+ if (blockTypeName === "jinjaIfBranch") {
3253
+ for (let d = blockDepth - 1; d > 0; d--) {
3254
+ if ($from.node(d).type.name === "jinjaIfBlock") {
3255
+ exitDepth = d;
3256
+ break;
3257
+ }
3258
+ }
3259
+ }
3260
+ const blockEnd = tr.mapping.map($from.after(exitDepth));
3261
+ const newPara = paragraph.create();
3262
+ tr.insert(blockEnd, newPara);
3263
+ tr.setSelection(TextSelection2.near(tr.doc.resolve(blockEnd + 1)));
3264
+ dispatch(tr.scrollIntoView());
3265
+ return true;
3266
+ };
3267
+ var exitCodeBlockOnEnter = (state, dispatch) => {
3268
+ const { $from } = state.selection;
3269
+ if ($from.parent.type.name !== "codeBlock") return false;
3270
+ const codeBlock = $from.parent;
3271
+ const text2 = codeBlock.textContent;
3272
+ if ($from.parentOffset !== text2.length) return false;
3273
+ if (!text2.endsWith("\n")) return false;
3274
+ if (dispatch) {
3275
+ const codeBlockPos = $from.before($from.depth);
3276
+ const tr = state.tr.delete($from.pos - 1, $from.pos);
3277
+ const blockEnd = tr.mapping.map(codeBlockPos + codeBlock.nodeSize);
3278
+ tr.insert(blockEnd, paragraph.create());
3279
+ tr.setSelection(TextSelection2.near(tr.doc.resolve(blockEnd + 1)));
3280
+ dispatch(tr.scrollIntoView());
3281
+ }
3282
+ return true;
3283
+ };
2932
3284
  var enterCommand = chainCommands(
3285
+ exitNoteBlockOnEnter,
3286
+ exitBlockOnDoubleEnter,
3287
+ exitCodeBlockOnEnter,
2933
3288
  newlineInCode,
2934
3289
  // Split list item on Enter; lift empty list item out of list
2935
3290
  splitListItem(listItem),
@@ -4705,6 +5060,17 @@ function analyzeJinjaBlocks(doc2) {
4705
5060
 
4706
5061
  // src/tree/documentTree.ts
4707
5062
  var MAX_DEPTH6 = 128;
5063
+ var END_ACTION_RESOURCE_IDS = /* @__PURE__ */ new Set([
5064
+ "close-happy-tiger"
5065
+ // PredefinedToolKey.CloseConversation
5066
+ ]);
5067
+ var END_ACTION_TEXTS = /* @__PURE__ */ new Set([
5068
+ "end conversation",
5069
+ "end_conversation"
5070
+ ]);
5071
+ function isEndActionTag(tagType, resourceId, text2) {
5072
+ return tagType === "handoff" || END_ACTION_RESOURCE_IDS.has(resourceId) || END_ACTION_TEXTS.has(text2.toLowerCase());
5073
+ }
4708
5074
  function extractInlineItems(content, blockIndex) {
4709
5075
  const items = [];
4710
5076
  for (const node of content) {
@@ -4716,10 +5082,10 @@ function extractInlineItems(content, blockIndex) {
4716
5082
  children: []
4717
5083
  });
4718
5084
  } else if (node.type === "resourceTag") {
4719
- const isEndAction = node.tagType === "handoff";
5085
+ const endAction = isEndActionTag(node.tagType, node.resourceId, node.text);
4720
5086
  items.push({
4721
- type: isEndAction ? "endAction" : "resourceTag",
4722
- label: isEndAction ? `End ${node.text}` : node.text,
5087
+ type: endAction ? "endAction" : "resourceTag",
5088
+ label: endAction ? `End ${node.text}` : node.text,
4723
5089
  blockIndex,
4724
5090
  children: [],
4725
5091
  meta: { tagType: node.tagType, resourceId: node.resourceId }
@@ -4753,14 +5119,15 @@ function extractBlockItems(blocks, startIndex, depth) {
4753
5119
  break;
4754
5120
  }
4755
5121
  case "jinjaIfBlock": {
4756
- const branches = block.branches.map((branch) => {
5122
+ const branches = block.branches.map((branch, branchIdx) => {
4757
5123
  const branchLabel = branch.branchType === "else" ? "ELSE" : `${branch.branchType.toUpperCase()} ${branch.condition || ""}`.trim();
4758
5124
  const branchChildren = extractBlockItems(branch.content, blockIndex, depth + 1);
4759
5125
  return {
4760
5126
  type: "jinjaBranch",
4761
5127
  label: branchLabel,
4762
5128
  blockIndex,
4763
- children: branchChildren
5129
+ children: branchChildren,
5130
+ meta: { branchIndex: branchIdx }
4764
5131
  };
4765
5132
  });
4766
5133
  items.push({
@@ -4808,11 +5175,12 @@ function buildDocumentTree(doc2) {
4808
5175
  const hasStructuredJinja = items.some((n) => n.type === "jinjaIf");
4809
5176
  if (!hasStructuredJinja) {
4810
5177
  for (const s of structures) {
4811
- const branches = s.branches.map((b) => ({
5178
+ const branches = s.branches.map((b, branchIdx) => ({
4812
5179
  type: "jinjaBranch",
4813
5180
  label: b.type === "else" ? "ELSE" : `${b.type.toUpperCase()} ${b.condition || ""}`.trim(),
4814
5181
  blockIndex: b.tagBlockIndex,
4815
- children: []
5182
+ children: [],
5183
+ meta: { branchIndex: branchIdx }
4816
5184
  }));
4817
5185
  const jinjaNode = {
4818
5186
  type: "jinjaIf",
@@ -4916,6 +5284,7 @@ function textToSlice(text2, view) {
4916
5284
  return null;
4917
5285
  }
4918
5286
  }
5287
+ var URL_RE = /^https?:\/\/[^\s]+$/i;
4919
5288
  function createPlugin() {
4920
5289
  return new Plugin({
4921
5290
  key,
@@ -4930,6 +5299,14 @@ function createPlugin() {
4930
5299
  return true;
4931
5300
  }
4932
5301
  }
5302
+ if (text2 && URL_RE.test(text2.trim())) {
5303
+ const url = text2.trim();
5304
+ const linkMark2 = actionbookSchema.marks.link.create({ href: url });
5305
+ const textNode = actionbookSchema.text(url, [linkMark2]);
5306
+ const slice2 = new Slice(Fragment.from(textNode), 0, 0);
5307
+ view.dispatch(view.state.tr.replaceSelection(slice2));
5308
+ return true;
5309
+ }
4933
5310
  if (!text2) return false;
4934
5311
  const slice = textToSlice(text2, view);
4935
5312
  if (!slice) return false;
@@ -4975,7 +5352,7 @@ function createMarkdownClipboardPlugin() {
4975
5352
  }
4976
5353
 
4977
5354
  // src/ui/plugin/jumpPointPlugin.ts
4978
- import { Plugin as Plugin2, PluginKey as PluginKey2, TextSelection as TextSelection2 } from "prosemirror-state";
5355
+ import { Plugin as Plugin2, PluginKey as PluginKey2, TextSelection as TextSelection3 } from "prosemirror-state";
4979
5356
  import { Decoration, DecorationSet } from "prosemirror-view";
4980
5357
  var adjacentKey = new PluginKey2("jumpPointAdjacent");
4981
5358
  var JUMP_POINT_ADJACENT_SPEC = { jumpPointAdjacent: true };
@@ -5038,6 +5415,9 @@ function createJumpPointEditPlugin() {
5038
5415
  });
5039
5416
  if (!duplicate) {
5040
5417
  reconvertTr.replaceWith(safeFrom, safeTo, jumpPointType.create({ id }));
5418
+ const mappedCursor = reconvertTr.mapping.map(cursor);
5419
+ const clampedCursor = Math.min(mappedCursor, reconvertTr.doc.content.size);
5420
+ reconvertTr.setSelection(TextSelection3.near(reconvertTr.doc.resolve(clampedCursor)));
5041
5421
  }
5042
5422
  }
5043
5423
  }
@@ -5070,7 +5450,7 @@ function explodeOnBackspace(state) {
5070
5450
  const rawText = `^${id}`;
5071
5451
  const tr = state.tr.replaceWith(nodeStart, nodeEnd, state.schema.text(rawText));
5072
5452
  const cursorPos = Math.min(nodeStart + rawText.length, tr.doc.content.size);
5073
- tr.setSelection(TextSelection2.near(tr.doc.resolve(cursorPos)));
5453
+ tr.setSelection(TextSelection3.near(tr.doc.resolve(cursorPos)));
5074
5454
  return tr;
5075
5455
  }
5076
5456
  function explodeOnArrowLeft(state) {
@@ -5080,7 +5460,7 @@ function explodeOnArrowLeft(state) {
5080
5460
  const rawText = `^${id}^`;
5081
5461
  const tr = state.tr.replaceWith(nodeStart, nodeEnd, state.schema.text(rawText));
5082
5462
  const cursorPos = Math.min(nodeStart + 1 + id.length, tr.doc.content.size);
5083
- tr.setSelection(TextSelection2.near(tr.doc.resolve(cursorPos)));
5463
+ tr.setSelection(TextSelection3.near(tr.doc.resolve(cursorPos)));
5084
5464
  tr.setMeta(jumpPointEditKey, { from: nodeStart, to: nodeStart + rawText.length });
5085
5465
  return tr;
5086
5466
  }
@@ -5129,21 +5509,30 @@ function createJumpPointAdjacentPlugin() {
5129
5509
  // src/ui/plugin/jumpPointNodeViewPlugin.tsx
5130
5510
  import { useCallback as useCallback2, useState as useState2 } from "react";
5131
5511
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
5512
+ function IconDiamondAlert({ size = 12, fill = "#D9352C" }) {
5513
+ return /* @__PURE__ */ jsxs3("svg", { width: size, height: size, viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
5514
+ /* @__PURE__ */ jsx4(
5515
+ "path",
5516
+ {
5517
+ 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",
5518
+ fill
5519
+ }
5520
+ ),
5521
+ /* @__PURE__ */ jsx4("path", { d: "M6 3.5v3", stroke: "#fff", strokeWidth: "1.2", strokeLinecap: "round" }),
5522
+ /* @__PURE__ */ jsx4("circle", { cx: "6", cy: "8.25", r: "0.625", fill: "#fff" })
5523
+ ] });
5524
+ }
5132
5525
  var JUMP_POINT_STYLE = {
5133
5526
  display: "inline-flex",
5134
5527
  alignItems: "center",
5135
5528
  backgroundColor: "#FFF2B6",
5136
5529
  border: "1px solid #FFC233",
5137
5530
  color: "#AA5D04",
5138
- borderRadius: "2px",
5139
- padding: "2px",
5140
- fontSize: "12px",
5141
- lineHeight: "16px",
5142
5531
  userSelect: "none",
5143
5532
  cursor: "default",
5144
- boxSizing: "border-box",
5145
- height: "20px",
5146
- overflow: "hidden"
5533
+ boxSizing: "border-box"
5534
+ // height, padding, borderRadius, fontSize, lineHeight, overflow
5535
+ // are set via CSS class .ab-jump-point for heading-level overrides
5147
5536
  };
5148
5537
  var JUMP_POINT_ADJACENT_STYLE = {
5149
5538
  ...JUMP_POINT_STYLE,
@@ -5157,8 +5546,8 @@ var JUMP_POINT_DUPLICATE_STYLE = {
5157
5546
  color: "#9D091E"
5158
5547
  };
5159
5548
  var LABEL_STYLE = {
5160
- padding: "0 4px",
5161
5549
  whiteSpace: "nowrap"
5550
+ // padding is set via CSS .ab-jp-label for heading-level overrides
5162
5551
  };
5163
5552
  var CLOSE_BTN_STYLE = {
5164
5553
  display: "inline-flex",
@@ -5189,7 +5578,7 @@ var TOOLTIP_STYLE = {
5189
5578
  fontSize: "14px",
5190
5579
  lineHeight: "20px",
5191
5580
  color: "#0D0D0D",
5192
- whiteSpace: "nowrap",
5581
+ maxWidth: "240px",
5193
5582
  zIndex: 100,
5194
5583
  pointerEvents: "none"
5195
5584
  };
@@ -5209,13 +5598,14 @@ function JumpPointNodeViewComponent({ node, view, getPos, decorations }) {
5209
5598
  return /* @__PURE__ */ jsxs3(
5210
5599
  "span",
5211
5600
  {
5601
+ className: "ab-jump-point ab-jump-point-duplicate",
5212
5602
  style: { ...JUMP_POINT_DUPLICATE_STYLE, position: "relative" },
5213
5603
  id: `jp-${id}`,
5214
5604
  onMouseEnter: () => setShowTooltip(true),
5215
5605
  onMouseLeave: () => setShowTooltip(false),
5216
5606
  children: [
5217
- /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#9D091E", style: { paddingLeft: "2px", flexShrink: 0 } }),
5218
- /* @__PURE__ */ jsx4("span", { style: LABEL_STYLE, children: id }),
5607
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-icon", children: /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#9D091E", style: { paddingLeft: "2px", flexShrink: 0 } }) }),
5608
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-label", style: LABEL_STYLE, children: id }),
5219
5609
  /* @__PURE__ */ jsx4(
5220
5610
  "button",
5221
5611
  {
@@ -5225,8 +5615,8 @@ function JumpPointNodeViewComponent({ node, view, getPos, decorations }) {
5225
5615
  e.stopPropagation();
5226
5616
  handleDelete2();
5227
5617
  },
5228
- title: "Remove anchor",
5229
- children: "\u2715"
5618
+ title: "Remove duplicate anchor",
5619
+ children: /* @__PURE__ */ jsx4(IconDiamondAlert, { size: 12, fill: "#D9352C" })
5230
5620
  }
5231
5621
  ),
5232
5622
  showTooltip && /* @__PURE__ */ jsxs3("span", { style: TOOLTIP_STYLE, children: [
@@ -5239,14 +5629,14 @@ function JumpPointNodeViewComponent({ node, view, getPos, decorations }) {
5239
5629
  );
5240
5630
  }
5241
5631
  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}^` })
5632
+ return /* @__PURE__ */ jsxs3("span", { className: "ab-jump-point ab-jump-point-adjacent", style: JUMP_POINT_ADJACENT_STYLE, id: `jp-${id}`, children: [
5633
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-icon", children: /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#AA5D04", style: { paddingLeft: "2px", flexShrink: 0 } }) }),
5634
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-label", style: LABEL_STYLE, children: `^${id}^` })
5245
5635
  ] });
5246
5636
  }
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 })
5637
+ return /* @__PURE__ */ jsxs3("span", { className: "ab-jump-point", style: JUMP_POINT_STYLE, id: `jp-${id}`, children: [
5638
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-icon", children: /* @__PURE__ */ jsx4(IconAnchor, { size: 12, fill: "#AA5D04", style: { paddingLeft: "2px", flexShrink: 0 } }) }),
5639
+ /* @__PURE__ */ jsx4("span", { className: "ab-jp-label", style: LABEL_STYLE, children: id })
5250
5640
  ] });
5251
5641
  }
5252
5642
  function createJumpPointNodeViewPlugin() {
@@ -5544,6 +5934,7 @@ function createJinjaDecorationPlugin() {
5544
5934
  // src/ui/plugin/jinjaIfBlockPlugin.tsx
5545
5935
  import { useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
5546
5936
  import { createRoot as createRoot2 } from "react-dom/client";
5937
+ import { TextSelection as TextSelection4 } from "prosemirror-state";
5547
5938
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
5548
5939
  var PLACEHOLDER_TEXT = "Describe what AI agent should do when this condition is met";
5549
5940
  var CONDITION_PLACEHOLDER = "Write a condition in natural language";
@@ -5950,7 +6341,12 @@ function insertNewBranch(view, nodePos, branchType, condition = "") {
5950
6341
  const newBranch = createBranchNode(view.state.schema, branchType, condition);
5951
6342
  if (!newBranch) return false;
5952
6343
  const insertPos = context.branchPos + context.branchNode.nodeSize;
5953
- view.dispatch(view.state.tr.insert(insertPos, newBranch).scrollIntoView());
6344
+ const tr = view.state.tr.insert(insertPos, newBranch);
6345
+ const cursorPos = insertPos + 2;
6346
+ if (cursorPos <= tr.doc.content.size) {
6347
+ tr.setSelection(TextSelection4.near(tr.doc.resolve(cursorPos)));
6348
+ }
6349
+ view.dispatch(tr.scrollIntoView());
5954
6350
  view.focus();
5955
6351
  return true;
5956
6352
  }
@@ -5997,6 +6393,7 @@ function ConditionDisplay({
5997
6393
  const [isEditing, setIsEditing] = useState3(false);
5998
6394
  const [draftValue, setDraftValue] = useState3(condition);
5999
6395
  const inputRef = useRef2(null);
6396
+ const autoFocusedRef = useRef2(false);
6000
6397
  useEffect2(() => {
6001
6398
  if (!isEditing) {
6002
6399
  setDraftValue(condition);
@@ -6008,6 +6405,12 @@ function ConditionDisplay({
6008
6405
  inputRef.current?.select();
6009
6406
  }
6010
6407
  }, [isEditing]);
6408
+ useEffect2(() => {
6409
+ if (editable && !condition && !autoFocusedRef.current) {
6410
+ autoFocusedRef.current = true;
6411
+ setIsEditing(true);
6412
+ }
6413
+ }, []);
6011
6414
  if (branchType === "else") {
6012
6415
  return /* @__PURE__ */ jsx6("span", { className: "jinja-otherwise", children: "Otherwise" });
6013
6416
  }
@@ -6197,7 +6600,7 @@ function JinjaBranchHeader({
6197
6600
  ) : null
6198
6601
  ] }) : null
6199
6602
  ] }),
6200
- isLastBranch ? /* @__PURE__ */ jsx6("div", { className: "jinja-add-footer", children: /* @__PURE__ */ jsx6("div", { className: "jinja-add-footer-col", children: /* @__PURE__ */ jsxs5("div", { className: "jinja-add-footer-actions", ref: footerRef, children: [
6603
+ isLastBranch && editable ? /* @__PURE__ */ jsx6("div", { className: "jinja-add-footer", children: /* @__PURE__ */ jsx6("div", { className: "jinja-add-footer-col", children: /* @__PURE__ */ jsxs5("div", { className: "jinja-add-footer-actions", ref: footerRef, children: [
6201
6604
  /* @__PURE__ */ jsx6(
6202
6605
  "button",
6203
6606
  {
@@ -6303,15 +6706,25 @@ var JinjaIfBranchView = class {
6303
6706
  }
6304
6707
  render() {
6305
6708
  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;
6709
+ let context = null;
6710
+ let lastBranch = null;
6711
+ if (nodePos != null) {
6712
+ context = getBranchContext(this.view, nodePos);
6713
+ lastBranch = getLastBranchOfBlock(this.view, nodePos);
6714
+ let branchIdx = context?.branchIndex ?? -1;
6715
+ if (branchIdx < 0 && this.dom.parentElement) {
6716
+ const siblings = this.dom.parentElement.querySelectorAll(":scope > [data-jinja-branch]");
6717
+ branchIdx = Array.prototype.indexOf.call(siblings, this.dom);
6718
+ }
6719
+ if (branchIdx >= 0) {
6720
+ this.dom.setAttribute("data-branch-index", String(branchIdx));
6721
+ }
6722
+ }
6310
6723
  const branchType = this.node.attrs.branchType;
6311
6724
  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;
6725
+ const isLast = context && lastBranch ? lastBranch.index === context.branchIndex : true;
6726
+ const lastNonElseIdx = context ? getLastNonElseBranchIndex(this.view, nodePos) : 0;
6727
+ const showAddButton = context ? context.branchIndex === lastNonElseIdx : true;
6315
6728
  this.dom.classList.toggle("jinja-branch-last", isLast);
6316
6729
  this.root.render(
6317
6730
  /* @__PURE__ */ jsx6(
@@ -6321,16 +6734,19 @@ var JinjaIfBranchView = class {
6321
6734
  condition,
6322
6735
  editable: this.view.editable,
6323
6736
  isLastBranch: showAddButton,
6324
- hasElseBranch: getElseBranch(this.view, nodePos),
6737
+ hasElseBranch: context && nodePos != null ? getElseBranch(this.view, nodePos) : false,
6325
6738
  onConditionChange: (value) => {
6326
6739
  const currentPos = this.getPos();
6327
6740
  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
- );
6741
+ const tr = this.view.state.tr.setNodeMarkup(currentPos, void 0, {
6742
+ ...this.node.attrs,
6743
+ condition: value
6744
+ });
6745
+ const contentPos = currentPos + 2;
6746
+ if (contentPos <= tr.doc.content.size) {
6747
+ tr.setSelection(TextSelection4.near(tr.doc.resolve(contentPos)));
6748
+ }
6749
+ this.view.dispatch(tr.scrollIntoView());
6334
6750
  this.view.focus();
6335
6751
  },
6336
6752
  onDelete: () => {
@@ -6359,7 +6775,7 @@ var JinjaIfBranchView = class {
6359
6775
  return target != null && this.headerContainer.contains(target);
6360
6776
  }
6361
6777
  ignoreMutation(mutation) {
6362
- return this.headerContainer.contains(mutation.target);
6778
+ return mutation.target === this.dom || this.headerContainer.contains(mutation.target);
6363
6779
  }
6364
6780
  destroy() {
6365
6781
  if (this._onSiblingsChanged) {
@@ -6381,9 +6797,9 @@ function createJinjaIfBlockPlugin() {
6381
6797
 
6382
6798
  // src/ui/plugin/linkPlugin.ts
6383
6799
  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+"([^"]*)")?\)$/;
6800
+ import { Plugin as Plugin5, PluginKey as PluginKey5, TextSelection as TextSelection5 } from "prosemirror-state";
6801
+ var LINK_INPUT_RE = /\\?\[([^\]]*?)\\?\]\(([^)\s]*?)(?:\s+"([^"]*)")?\)$/;
6802
+ var LINK_FULL_RE = /^\\?\[([^\]]*?)\\?\]\(([^)\s]*?)(?:\s+"([^"]*)")?\)$/;
6387
6803
  var linkEditKey = new PluginKey5("linkEdit");
6388
6804
  function getMarkRange($pos, type) {
6389
6805
  const { parentOffset } = $pos;
@@ -6427,7 +6843,7 @@ function explodeLinkToRaw(state) {
6427
6843
  const tr = state.tr;
6428
6844
  tr.replaceWith(from, to, schema.text(rawText));
6429
6845
  const newPos = Math.min(from + rawCursorOffset, tr.doc.content.size);
6430
- tr.setSelection(TextSelection3.near(tr.doc.resolve(newPos)));
6846
+ tr.setSelection(TextSelection5.near(tr.doc.resolve(newPos)));
6431
6847
  tr.setMeta(linkEditKey, { from, to: from + rawText.length });
6432
6848
  return tr;
6433
6849
  }
@@ -6440,8 +6856,8 @@ function createLinkEditPlugin() {
6440
6856
  const meta = tr.getMeta(linkEditKey);
6441
6857
  if (meta !== void 0) return { rawRange: meta };
6442
6858
  if (prev.rawRange) {
6443
- const from = tr.mapping.map(prev.rawRange.from);
6444
- const to = tr.mapping.map(prev.rawRange.to);
6859
+ const from = tr.mapping.map(prev.rawRange.from, -1);
6860
+ const to = tr.mapping.map(prev.rawRange.to, -1);
6445
6861
  if (from >= to) return { rawRange: null };
6446
6862
  return { rawRange: { from, to } };
6447
6863
  }
@@ -6452,7 +6868,7 @@ function createLinkEditPlugin() {
6452
6868
  const pluginState = linkEditKey.getState(newState);
6453
6869
  if (!pluginState?.rawRange) return null;
6454
6870
  const { from, to } = pluginState.rawRange;
6455
- if (newState.selection.anchor >= from && newState.selection.anchor <= to) {
6871
+ if (newState.selection.anchor >= from && newState.selection.anchor < to) {
6456
6872
  return null;
6457
6873
  }
6458
6874
  const docSize = newState.doc.content.size;
@@ -6596,12 +7012,13 @@ var STYLES = (
6596
7012
  cursor: grab;
6597
7013
  color: transparent;
6598
7014
  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);
7015
+ transition: none;
6600
7016
  user-select: none;
6601
7017
  pointer-events: auto;
6602
7018
  touch-action: none;
6603
7019
  opacity: 0;
6604
7020
  pointer-events: none;
7021
+ overflow: visible;
6605
7022
  }
6606
7023
  .ab-drag-handle.ab-dh-active {
6607
7024
  opacity: 1;
@@ -6612,6 +7029,72 @@ var STYLES = (
6612
7029
  color: var(--ab-dh-color-visible) !important;
6613
7030
  background: var(--ab-dh-bg-hover);
6614
7031
  }
7032
+ .ab-drag-handle:hover::after {
7033
+ content: 'Drag to move';
7034
+ position: absolute;
7035
+ bottom: calc(100% + 4px);
7036
+ left: 0;
7037
+ background: #2e2e2e;
7038
+ color: #fff;
7039
+ font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
7040
+ font-size: 12px;
7041
+ font-weight: 400;
7042
+ line-height: 16px;
7043
+ padding: 4px 8px;
7044
+ border-radius: 4px;
7045
+ white-space: nowrap;
7046
+ pointer-events: none;
7047
+ z-index: 30;
7048
+ }
7049
+ .ab-drag-handle:hover::before {
7050
+ content: '';
7051
+ position: absolute;
7052
+ bottom: calc(100% + 0px);
7053
+ left: 8px;
7054
+ width: 0;
7055
+ height: 0;
7056
+ border-left: 5px solid transparent;
7057
+ border-right: 5px solid transparent;
7058
+ border-top: 5px solid #2e2e2e;
7059
+ pointer-events: none;
7060
+ z-index: 31;
7061
+ }
7062
+ .ab-drag-handle.dragging::after,
7063
+ .ab-drag-handle.dragging::before {
7064
+ display: none;
7065
+ }
7066
+ .ab-dh-menu {
7067
+ position: absolute;
7068
+ top: calc(100% + 4px);
7069
+ left: 0;
7070
+ background: #fff;
7071
+ border-radius: 4px;
7072
+ 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);
7073
+ padding: 8px 0;
7074
+ z-index: 40;
7075
+ min-width: 120px;
7076
+ }
7077
+ .ab-dh-menu-item {
7078
+ display: flex;
7079
+ align-items: center;
7080
+ width: 100%;
7081
+ padding: 6px 16px;
7082
+ border: none;
7083
+ background: #fff;
7084
+ cursor: pointer;
7085
+ font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
7086
+ font-size: 14px;
7087
+ font-weight: 400;
7088
+ line-height: 20px;
7089
+ letter-spacing: -0.1px;
7090
+ color: #0d0d0d;
7091
+ white-space: nowrap;
7092
+ text-overflow: ellipsis;
7093
+ overflow: hidden;
7094
+ }
7095
+ .ab-dh-menu-item:hover {
7096
+ background: rgba(0,0,0,0.04);
7097
+ }
6615
7098
  .ab-drag-handle:active, .ab-drag-handle.dragging {
6616
7099
  cursor: grabbing;
6617
7100
  color: var(--ab-dh-color-accent) !important;
@@ -6921,7 +7404,6 @@ var DragHandleController = class {
6921
7404
  el.setAttribute("role", "button");
6922
7405
  el.setAttribute("aria-roledescription", "drag handle");
6923
7406
  el.setAttribute("aria-label", `\uBE14\uB85D ${idx + 1} \uC774\uB3D9`);
6924
- el.setAttribute("title", "Drag to move");
6925
7407
  el.setAttribute("tabindex", "-1");
6926
7408
  el.innerHTML = GRIP_SVG;
6927
7409
  el.style.left = `${handleLeft}px`;
@@ -6934,18 +7416,85 @@ var DragHandleController = class {
6934
7416
  this.prevBlocks = newBlocks;
6935
7417
  this.updateActiveHandle();
6936
7418
  }
7419
+ activeMenu = null;
7420
+ activeMenuCleanup = null;
7421
+ dismissMenu() {
7422
+ if (this.activeMenu) {
7423
+ this.activeMenu.remove();
7424
+ this.activeMenu = null;
7425
+ }
7426
+ if (this.activeMenuCleanup) {
7427
+ this.activeMenuCleanup();
7428
+ this.activeMenuCleanup = null;
7429
+ }
7430
+ }
7431
+ showMenu(el, blockIndex) {
7432
+ this.dismissMenu();
7433
+ const menu = document.createElement("div");
7434
+ menu.className = "ab-dh-menu";
7435
+ const deleteBtn = document.createElement("button");
7436
+ deleteBtn.className = "ab-dh-menu-item";
7437
+ deleteBtn.textContent = "Delete";
7438
+ deleteBtn.addEventListener("mousedown", (e) => {
7439
+ e.preventDefault();
7440
+ e.stopPropagation();
7441
+ this.deleteBlock(blockIndex);
7442
+ this.dismissMenu();
7443
+ });
7444
+ menu.appendChild(deleteBtn);
7445
+ el.appendChild(menu);
7446
+ this.activeMenu = menu;
7447
+ const onOutside = (e) => {
7448
+ if (!menu.contains(e.target)) {
7449
+ this.dismissMenu();
7450
+ }
7451
+ };
7452
+ setTimeout(() => document.addEventListener("mousedown", onOutside, true), 0);
7453
+ this.activeMenuCleanup = () => document.removeEventListener("mousedown", onOutside, true);
7454
+ }
7455
+ deleteBlock(blockIndex) {
7456
+ const blocks = this.collectBlocks();
7457
+ if (blockIndex >= blocks.length) return;
7458
+ const block = blocks[blockIndex];
7459
+ const from = block.offset;
7460
+ const to = block.offset + block.nodeSize;
7461
+ const tr = this.view.state.tr;
7462
+ tr.replaceWith(from, to, this.view.state.schema.nodes.paragraph.create());
7463
+ this.view.dispatch(tr);
7464
+ this.view.focus();
7465
+ }
6937
7466
  bindHandleEvents(el, index) {
6938
7467
  let currentIndex = index;
7468
+ let downX = 0;
7469
+ let downY = 0;
7470
+ let didDrag = false;
7471
+ let menuWasOpen = false;
6939
7472
  const onPointerDown = (e) => {
7473
+ if (this.activeMenu && this.activeMenu.contains(e.target)) return;
6940
7474
  e.preventDefault();
6941
7475
  if (!this.view.editable) return;
7476
+ menuWasOpen = !!this.activeMenu;
7477
+ this.dismissMenu();
7478
+ downX = e.clientX;
7479
+ downY = e.clientY;
7480
+ didDrag = false;
6942
7481
  el.setPointerCapture(e.pointerId);
6943
7482
  this.prevBlocks = this.collectBlocks();
6944
7483
  this.syncHandlePositions();
6945
7484
  this.startDrag(currentIndex, el, e);
6946
7485
  };
6947
- const onPointerMove = (e) => this.onPointerMove(e);
6948
- const onPointerUp = (e) => this.onPointerUp(e);
7486
+ const onPointerMove = (e) => {
7487
+ if (Math.abs(e.clientX - downX) > 3 || Math.abs(e.clientY - downY) > 3) {
7488
+ didDrag = true;
7489
+ }
7490
+ this.onPointerMove(e);
7491
+ };
7492
+ const onPointerUp = (e) => {
7493
+ this.onPointerUp(e);
7494
+ if (!didDrag && !menuWasOpen) {
7495
+ this.showMenu(el, currentIndex);
7496
+ }
7497
+ };
6949
7498
  const onLostCapture = () => this.cancelDrag();
6950
7499
  const onPointerCancel = () => this.cancelDrag();
6951
7500
  el.addEventListener("pointerdown", onPointerDown);
@@ -7352,150 +7901,89 @@ function createTodoNodeViewPlugin() {
7352
7901
  };
7353
7902
  }
7354
7903
 
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;
7904
+ // src/ui/plugin/placeholderPlugin.ts
7905
+ import { Plugin as Plugin7, PluginKey as PluginKey7 } from "prosemirror-state";
7906
+ import { Decoration as Decoration4, DecorationSet as DecorationSet4 } from "prosemirror-view";
7907
+ var PLACEHOLDER_TEXT2 = "Type to start writing, or press / to insert an action.";
7908
+ var pluginKey2 = new PluginKey7("placeholder");
7909
+ function buildDecorations4(state) {
7910
+ const { doc: doc2, selection } = state;
7911
+ if (!selection.empty) return DecorationSet4.empty;
7912
+ const $from = selection.$from;
7913
+ const parent = $from.parent;
7914
+ if (parent.type.name !== "paragraph" || parent.content.size !== 0) {
7915
+ return DecorationSet4.empty;
7436
7916
  }
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;
7917
+ const EXCLUDED_ANCESTORS = /* @__PURE__ */ new Set(["noteBlock", "blockquote", "jinjaIfBranch"]);
7918
+ for (let d = $from.depth - 1; d > 0; d--) {
7919
+ if (EXCLUDED_ANCESTORS.has($from.node(d).type.name)) {
7920
+ return DecorationSet4.empty;
7452
7921
  }
7453
7922
  }
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() {
7923
+ const pos = $from.before($from.depth);
7924
+ const deco = Decoration4.node(pos, pos + parent.nodeSize, {
7925
+ class: "ab-empty-paragraph",
7926
+ "data-placeholder": PLACEHOLDER_TEXT2
7927
+ });
7928
+ return DecorationSet4.create(doc2, [deco]);
7929
+ }
7930
+ function createPlaceholderPlugin() {
7462
7931
  return {
7463
- name: "noteBlockNodeView",
7464
- keymap: () => ({
7465
- Enter: exitNoteBlockOnEnter
7466
- }),
7467
- nodeViews: () => ({
7468
- noteBlock: (node, view, getPos) => new NoteBlockView(node, view, getPos)
7469
- })
7932
+ name: "placeholder",
7933
+ plugins: () => [
7934
+ new Plugin7({
7935
+ key: pluginKey2,
7936
+ state: {
7937
+ init(_, state) {
7938
+ return buildDecorations4(state);
7939
+ },
7940
+ apply(tr, oldSet, _oldState, newState) {
7941
+ if (tr.docChanged || tr.selectionSet) {
7942
+ return buildDecorations4(newState);
7943
+ }
7944
+ return oldSet;
7945
+ }
7946
+ },
7947
+ props: {
7948
+ decorations(state) {
7949
+ return pluginKey2.getState(state) ?? DecorationSet4.empty;
7950
+ }
7951
+ }
7952
+ })
7953
+ ]
7470
7954
  };
7471
7955
  }
7472
7956
 
7473
7957
  // src/ui/plugin/slashCommandPlugin.ts
7474
- import { Plugin as Plugin7, PluginKey as PluginKey7 } from "prosemirror-state";
7475
- var slashCommandKey = new PluginKey7("slashCommand");
7958
+ import { Plugin as Plugin8, PluginKey as PluginKey8 } from "prosemirror-state";
7959
+ var slashCommandKey = new PluginKey8("slashCommand");
7476
7960
  var TRIGGER_RE = /(?:^|\s)(\/[^\s]*)$/;
7477
7961
  function deriveState(state, dismissedFrom) {
7962
+ const inactive = { active: false, range: null, query: "", parentType: "" };
7478
7963
  const { selection } = state;
7479
- if (!selection.empty) return { active: false, range: null, query: "" };
7964
+ if (!selection.empty) return inactive;
7480
7965
  const { $from } = selection;
7481
7966
  const parentType = $from.parent.type.name;
7482
- if (parentType === "heading") return { active: false, range: null, query: "" };
7967
+ for (let d = $from.depth; d > 0; d--) {
7968
+ if ($from.node(d).type.name === "noteBlock") return inactive;
7969
+ }
7483
7970
  const textBefore = $from.parent.textBetween(0, $from.parentOffset, "\n", "\0");
7484
7971
  const match = TRIGGER_RE.exec(textBefore);
7485
- if (!match) return { active: false, range: null, query: "" };
7972
+ if (!match) return inactive;
7486
7973
  const slashText = match[1];
7487
7974
  const slashStartInParent = textBefore.lastIndexOf(slashText);
7488
7975
  const from = $from.start() + slashStartInParent;
7489
7976
  const to = $from.pos;
7490
- if (dismissedFrom === from) return { active: false, range: null, query: "" };
7977
+ if (dismissedFrom === from) return inactive;
7491
7978
  return {
7492
7979
  active: true,
7493
7980
  range: { from, to },
7494
- query: slashText.slice(1)
7981
+ query: slashText.slice(1),
7982
+ parentType
7495
7983
  };
7496
7984
  }
7497
7985
  function createSlashCommandPlugin() {
7498
- const plugin = new Plugin7({
7986
+ const plugin = new Plugin8({
7499
7987
  key: slashCommandKey,
7500
7988
  state: {
7501
7989
  init(_, state) {
@@ -8013,6 +8501,7 @@ function DocumentTreeView({ doc: doc2, className, onNavigate }) {
8013
8501
  // src/ui/components/FloatingMenu.tsx
8014
8502
  import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
8015
8503
  import { createPortal as createPortal2 } from "react-dom";
8504
+ import { TextSelection as TextSelection6 } from "prosemirror-state";
8016
8505
  import { setBlockType as setBlockType2, toggleMark as toggleMark2, wrapIn } from "prosemirror-commands";
8017
8506
  import { wrapInList as wrapInList2 } from "prosemirror-schema-list";
8018
8507
  import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
@@ -8035,6 +8524,56 @@ function hasMark(state, type) {
8035
8524
  if (empty) return false;
8036
8525
  return state.doc.rangeHasMark(from, to, type);
8037
8526
  }
8527
+ function getMarkRange2($pos, type) {
8528
+ const { parentOffset } = $pos;
8529
+ let child = $pos.parent.childAfter(parentOffset);
8530
+ if (parentOffset === child.offset && child.offset !== 0) {
8531
+ child = $pos.parent.childBefore(parentOffset);
8532
+ }
8533
+ if (!child.node) return null;
8534
+ const mark = child.node.marks.find((candidate) => candidate.type === type);
8535
+ if (!mark) return null;
8536
+ const parentStart = $pos.start();
8537
+ let { index: startIndex } = child;
8538
+ let startPos = parentStart + child.offset;
8539
+ let endIndex = startIndex + 1;
8540
+ let endPos = startPos + child.node.nodeSize;
8541
+ while (startIndex > 0 && mark.isInSet($pos.parent.child(startIndex - 1).marks)) {
8542
+ startIndex -= 1;
8543
+ startPos -= $pos.parent.child(startIndex).nodeSize;
8544
+ }
8545
+ while (endIndex < $pos.parent.childCount && mark.isInSet($pos.parent.child(endIndex).marks)) {
8546
+ endPos += $pos.parent.child(endIndex).nodeSize;
8547
+ endIndex += 1;
8548
+ }
8549
+ return { from: startPos, to: endPos, mark };
8550
+ }
8551
+ function getLinkAtCursor(state) {
8552
+ const { selection, doc: doc2 } = state;
8553
+ if (!selection.empty) return null;
8554
+ const range = getMarkRange2(doc2.resolve(selection.from), linkMark);
8555
+ if (!range) return null;
8556
+ return {
8557
+ href: range.mark.attrs.href || "",
8558
+ text: doc2.textBetween(range.from, range.to),
8559
+ from: range.from,
8560
+ to: range.to
8561
+ };
8562
+ }
8563
+ function normalizeLinkHref(href) {
8564
+ const trimmedHref = href.trim();
8565
+ if (!trimmedHref) return "";
8566
+ return trimmedHref.startsWith("#") || trimmedHref.startsWith("/") || /^https?:\/\//i.test(trimmedHref) ? trimmedHref : `https://${trimmedHref}`;
8567
+ }
8568
+ function getNonLinkMarksInRange(state, from, to) {
8569
+ let marks = [];
8570
+ state.doc.nodesBetween(from, to, (node) => {
8571
+ if (!node.isText) return;
8572
+ marks = node.marks.filter((mark) => mark.type !== linkMark);
8573
+ return false;
8574
+ });
8575
+ return marks;
8576
+ }
8038
8577
  function activeHeadingLevel(state) {
8039
8578
  const { $from } = state.selection;
8040
8579
  const node = $from.parent;
@@ -8497,6 +9036,408 @@ function SelectionToolbar({ view, state, selectionRect }) {
8497
9036
  }
8498
9037
  );
8499
9038
  }
9039
+ function LinkHoverTooltip({ view, link: link2, onEdit }) {
9040
+ const [hover, setHover] = useState7(false);
9041
+ const startCoords = view.coordsAtPos(link2.from);
9042
+ const endCoords = view.coordsAtPos(link2.to);
9043
+ const anchorTop = Math.min(startCoords.top, endCoords.top);
9044
+ const linkLeft = Math.min(startCoords.left, endCoords.left);
9045
+ const linkRight = Math.max(startCoords.right, endCoords.right);
9046
+ const tooltipHalfW = Math.min(174, Math.max(0, (window.innerWidth - VPORT_MARGIN2 * 2) / 2));
9047
+ const safeCenterX = Math.max(
9048
+ VPORT_MARGIN2 + tooltipHalfW,
9049
+ Math.min((linkLeft + linkRight) / 2, window.innerWidth - VPORT_MARGIN2 - tooltipHalfW)
9050
+ );
9051
+ return createPortal2(
9052
+ /* @__PURE__ */ jsxs9(
9053
+ "div",
9054
+ {
9055
+ ...{ [FLOAT_ATTR]: "" },
9056
+ style: {
9057
+ position: "fixed",
9058
+ left: safeCenterX,
9059
+ top: Math.max(64, anchorTop - 4),
9060
+ transform: "translate(-50%, -100%)",
9061
+ zIndex: 1e3,
9062
+ display: "flex",
9063
+ flexDirection: "column",
9064
+ alignItems: "center",
9065
+ animation: "ab-float-in 0.12s ease",
9066
+ pointerEvents: "auto"
9067
+ },
9068
+ children: [
9069
+ /* @__PURE__ */ jsxs9(
9070
+ "div",
9071
+ {
9072
+ ...{ [FLOAT_ATTR]: "" },
9073
+ style: {
9074
+ display: "flex",
9075
+ alignItems: "center",
9076
+ gap: 4,
9077
+ minHeight: 28,
9078
+ maxWidth: "min(348px, calc(100vw - 16px))",
9079
+ padding: "6px 8px",
9080
+ background: "#2E2E2E",
9081
+ borderRadius: 4,
9082
+ boxSizing: "border-box"
9083
+ },
9084
+ children: [
9085
+ /* @__PURE__ */ jsx10(
9086
+ "span",
9087
+ {
9088
+ ...{ [FLOAT_ATTR]: "" },
9089
+ style: {
9090
+ color: "#FFFFFF",
9091
+ fontSize: 12,
9092
+ lineHeight: "16px",
9093
+ fontFamily: '"SF Pro Text", "SF Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
9094
+ whiteSpace: "normal",
9095
+ wordBreak: "break-word",
9096
+ overflowWrap: "anywhere",
9097
+ maxWidth: 300
9098
+ },
9099
+ children: link2.href
9100
+ }
9101
+ ),
9102
+ /* @__PURE__ */ jsx10(
9103
+ "div",
9104
+ {
9105
+ ...{ [FLOAT_ATTR]: "" },
9106
+ style: {
9107
+ width: 1,
9108
+ height: 16,
9109
+ background: "#424242",
9110
+ flexShrink: 0
9111
+ }
9112
+ }
9113
+ ),
9114
+ /* @__PURE__ */ jsx10(
9115
+ "button",
9116
+ {
9117
+ type: "button",
9118
+ ...{ [FLOAT_ATTR]: "" },
9119
+ onMouseDown: (e) => {
9120
+ e.preventDefault();
9121
+ onEdit(link2);
9122
+ },
9123
+ onMouseEnter: () => setHover(true),
9124
+ onMouseLeave: () => setHover(false),
9125
+ style: {
9126
+ ...BTN_RESET2,
9127
+ padding: "0 2px",
9128
+ color: "#FFFFFF",
9129
+ fontSize: 12,
9130
+ lineHeight: "16px",
9131
+ fontWeight: 600,
9132
+ fontFamily: '"SF Pro Text", "SF Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
9133
+ opacity: hover ? 0.78 : 1,
9134
+ flexShrink: 0
9135
+ },
9136
+ children: "Edit"
9137
+ }
9138
+ )
9139
+ ]
9140
+ }
9141
+ ),
9142
+ /* @__PURE__ */ jsx10(
9143
+ "div",
9144
+ {
9145
+ ...{ [FLOAT_ATTR]: "" },
9146
+ style: {
9147
+ width: 0,
9148
+ height: 0,
9149
+ borderLeft: "6px solid transparent",
9150
+ borderRight: "6px solid transparent",
9151
+ borderTop: "6px solid #2E2E2E",
9152
+ marginTop: -1
9153
+ }
9154
+ }
9155
+ )
9156
+ ]
9157
+ }
9158
+ ),
9159
+ document.body
9160
+ );
9161
+ }
9162
+ function LinkEditDialog({ view, link: link2, onClose }) {
9163
+ const [textValue, setTextValue] = useState7(link2.text);
9164
+ const [hrefValue, setHrefValue] = useState7(link2.href);
9165
+ const textInputRef = useRef4(null);
9166
+ useEffect4(() => {
9167
+ setTextValue(link2.text);
9168
+ setHrefValue(link2.href);
9169
+ }, [link2.from, link2.href, link2.text, link2.to]);
9170
+ useEffect4(() => {
9171
+ setTimeout(() => textInputRef.current?.focus(), 0);
9172
+ }, [link2.from, link2.to]);
9173
+ useEffect4(() => {
9174
+ const onDown = (e) => {
9175
+ if (!e.target.closest(`[${FLOAT_ATTR}]`)) {
9176
+ onClose();
9177
+ }
9178
+ };
9179
+ document.addEventListener("mousedown", onDown, true);
9180
+ return () => document.removeEventListener("mousedown", onDown, true);
9181
+ }, [onClose]);
9182
+ const startCoords = view.coordsAtPos(link2.from);
9183
+ const endCoords = view.coordsAtPos(link2.to);
9184
+ const anchorTop = Math.min(startCoords.top, endCoords.top);
9185
+ const anchorBottom = Math.max(startCoords.bottom, endCoords.bottom);
9186
+ const linkLeft = Math.min(startCoords.left, endCoords.left);
9187
+ const linkRight = Math.max(startCoords.right, endCoords.right);
9188
+ const dialogW = 280;
9189
+ const estimatedDialogH = 188;
9190
+ const centerX = (linkLeft + linkRight) / 2;
9191
+ const left = Math.max(
9192
+ VPORT_MARGIN2,
9193
+ Math.min(centerX - dialogW / 2, window.innerWidth - dialogW - VPORT_MARGIN2)
9194
+ );
9195
+ let top = anchorTop - estimatedDialogH - 12;
9196
+ if (top < VPORT_MARGIN2) {
9197
+ top = Math.min(anchorBottom + 12, window.innerHeight - estimatedDialogH - VPORT_MARGIN2);
9198
+ }
9199
+ const finalHref = normalizeLinkHref(hrefValue);
9200
+ const finalText = textValue.trim() ? textValue : finalHref;
9201
+ const canSave = Boolean(finalHref) && (finalHref !== link2.href || finalText !== link2.text);
9202
+ const currentLink = () => {
9203
+ const activeLink = getLinkAtCursor(view.state);
9204
+ if (activeLink && activeLink.from === link2.from && activeLink.to === link2.to) {
9205
+ return activeLink;
9206
+ }
9207
+ return link2;
9208
+ };
9209
+ const saveLink = () => {
9210
+ const activeLink = currentLink();
9211
+ const nextHref = normalizeLinkHref(hrefValue);
9212
+ const nextText = textValue.trim() ? textValue : nextHref;
9213
+ if (!nextHref) return;
9214
+ if (nextHref === activeLink.href && nextText === activeLink.text) return;
9215
+ const cursorOffset = Math.max(0, view.state.selection.from - activeLink.from);
9216
+ const tr = view.state.tr;
9217
+ if (nextText === activeLink.text) {
9218
+ tr.removeMark(activeLink.from, activeLink.to, linkMark);
9219
+ tr.addMark(activeLink.from, activeLink.to, linkMark.create({ href: nextHref }));
9220
+ const nextCursor = Math.min(activeLink.from + cursorOffset, activeLink.to);
9221
+ tr.setSelection(TextSelection6.near(tr.doc.resolve(nextCursor)));
9222
+ } else {
9223
+ const preservedMarks = getNonLinkMarksInRange(view.state, activeLink.from, activeLink.to);
9224
+ tr.insertText(nextText, activeLink.from, activeLink.to);
9225
+ const nextTo = activeLink.from + nextText.length;
9226
+ tr.removeMark(activeLink.from, nextTo, linkMark);
9227
+ for (const mark of preservedMarks) {
9228
+ tr.addMark(activeLink.from, nextTo, mark);
9229
+ }
9230
+ tr.addMark(activeLink.from, nextTo, linkMark.create({ href: nextHref }));
9231
+ const nextCursor = Math.min(activeLink.from + cursorOffset, nextTo);
9232
+ tr.setSelection(TextSelection6.near(tr.doc.resolve(nextCursor)));
9233
+ }
9234
+ view.dispatch(tr);
9235
+ onClose();
9236
+ view.focus();
9237
+ };
9238
+ const removeLink = () => {
9239
+ const activeLink = currentLink();
9240
+ view.dispatch(view.state.tr.removeMark(activeLink.from, activeLink.to, linkMark));
9241
+ onClose();
9242
+ view.focus();
9243
+ };
9244
+ const handleKeyDown = (e) => {
9245
+ if (e.key === "Escape") {
9246
+ e.preventDefault();
9247
+ onClose();
9248
+ view.focus();
9249
+ return;
9250
+ }
9251
+ if (e.key === "Enter") {
9252
+ e.preventDefault();
9253
+ saveLink();
9254
+ }
9255
+ };
9256
+ return createPortal2(
9257
+ /* @__PURE__ */ jsxs9(
9258
+ "div",
9259
+ {
9260
+ ...{ [FLOAT_ATTR]: "" },
9261
+ style: {
9262
+ position: "fixed",
9263
+ left,
9264
+ top,
9265
+ zIndex: 1001,
9266
+ width: dialogW,
9267
+ display: "flex",
9268
+ flexDirection: "column",
9269
+ gap: 16,
9270
+ padding: 16,
9271
+ background: "#FFFFFF",
9272
+ borderRadius: 4,
9273
+ 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)",
9274
+ boxSizing: "border-box",
9275
+ animation: "ab-float-in 0.12s ease"
9276
+ },
9277
+ children: [
9278
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
9279
+ /* @__PURE__ */ jsxs9(
9280
+ "div",
9281
+ {
9282
+ ...{ [FLOAT_ATTR]: "" },
9283
+ style: {
9284
+ fontSize: 12,
9285
+ lineHeight: "16px",
9286
+ fontWeight: 600,
9287
+ color: "#0D0D0D"
9288
+ },
9289
+ children: [
9290
+ "Text ",
9291
+ /* @__PURE__ */ jsx10("span", { style: { color: "#858585" }, children: "(optional)" })
9292
+ ]
9293
+ }
9294
+ ),
9295
+ /* @__PURE__ */ jsx10(
9296
+ "input",
9297
+ {
9298
+ ref: textInputRef,
9299
+ ...{ [FLOAT_ATTR]: "" },
9300
+ value: textValue,
9301
+ onChange: (e) => setTextValue(e.target.value),
9302
+ onKeyDown: handleKeyDown,
9303
+ style: {
9304
+ height: 32,
9305
+ border: "1px solid #CCCCCC",
9306
+ borderRadius: 4,
9307
+ padding: "0 16px",
9308
+ fontSize: 14,
9309
+ color: "#0D0D0D",
9310
+ outline: "none",
9311
+ boxSizing: "border-box"
9312
+ }
9313
+ }
9314
+ )
9315
+ ] }),
9316
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
9317
+ /* @__PURE__ */ jsx10(
9318
+ "div",
9319
+ {
9320
+ ...{ [FLOAT_ATTR]: "" },
9321
+ style: {
9322
+ fontSize: 12,
9323
+ lineHeight: "16px",
9324
+ fontWeight: 600,
9325
+ color: "#0D0D0D"
9326
+ },
9327
+ children: "Link"
9328
+ }
9329
+ ),
9330
+ /* @__PURE__ */ jsx10(
9331
+ "input",
9332
+ {
9333
+ ...{ [FLOAT_ATTR]: "" },
9334
+ value: hrefValue,
9335
+ onChange: (e) => setHrefValue(e.target.value),
9336
+ onKeyDown: handleKeyDown,
9337
+ style: {
9338
+ height: 32,
9339
+ border: "1px solid #CCCCCC",
9340
+ borderRadius: 4,
9341
+ padding: "0 16px",
9342
+ fontSize: 14,
9343
+ color: "#0D0D0D",
9344
+ outline: "none",
9345
+ boxSizing: "border-box"
9346
+ }
9347
+ }
9348
+ )
9349
+ ] }),
9350
+ /* @__PURE__ */ jsxs9(
9351
+ "div",
9352
+ {
9353
+ ...{ [FLOAT_ATTR]: "" },
9354
+ style: {
9355
+ display: "flex",
9356
+ alignItems: "center",
9357
+ justifyContent: "space-between",
9358
+ gap: 12
9359
+ },
9360
+ children: [
9361
+ /* @__PURE__ */ jsx10(
9362
+ "button",
9363
+ {
9364
+ type: "button",
9365
+ ...{ [FLOAT_ATTR]: "" },
9366
+ onMouseDown: (e) => {
9367
+ e.preventDefault();
9368
+ removeLink();
9369
+ },
9370
+ style: {
9371
+ ...BTN_RESET2,
9372
+ fontSize: 14,
9373
+ lineHeight: "20px",
9374
+ fontWeight: 600,
9375
+ color: "#0D0D0D"
9376
+ },
9377
+ children: "Remove link"
9378
+ }
9379
+ ),
9380
+ /* @__PURE__ */ jsxs9("div", { ...{ [FLOAT_ATTR]: "" }, style: { display: "flex", alignItems: "center", gap: 8 }, children: [
9381
+ /* @__PURE__ */ jsx10(
9382
+ "button",
9383
+ {
9384
+ type: "button",
9385
+ ...{ [FLOAT_ATTR]: "" },
9386
+ onMouseDown: (e) => {
9387
+ e.preventDefault();
9388
+ onClose();
9389
+ view.focus();
9390
+ },
9391
+ style: {
9392
+ ...BTN_RESET2,
9393
+ height: 32,
9394
+ padding: "0 13px",
9395
+ border: "1px solid #CCCCCC",
9396
+ borderRadius: 4,
9397
+ fontSize: 14,
9398
+ lineHeight: "20px",
9399
+ fontWeight: 600,
9400
+ color: "#0D0D0D"
9401
+ },
9402
+ children: "Cancel"
9403
+ }
9404
+ ),
9405
+ /* @__PURE__ */ jsx10(
9406
+ "button",
9407
+ {
9408
+ type: "button",
9409
+ disabled: !canSave,
9410
+ ...{ [FLOAT_ATTR]: "" },
9411
+ onMouseDown: (e) => {
9412
+ e.preventDefault();
9413
+ if (!canSave) return;
9414
+ saveLink();
9415
+ },
9416
+ style: {
9417
+ ...BTN_RESET2,
9418
+ height: 32,
9419
+ padding: "0 13px",
9420
+ borderRadius: 4,
9421
+ fontSize: 14,
9422
+ lineHeight: "20px",
9423
+ fontWeight: 600,
9424
+ background: canSave ? "#6210CC" : "#ECECEC",
9425
+ color: canSave ? "#FFFFFF" : "#A6A6A6",
9426
+ cursor: canSave ? "pointer" : "default"
9427
+ },
9428
+ children: "Save"
9429
+ }
9430
+ )
9431
+ ] })
9432
+ ]
9433
+ }
9434
+ )
9435
+ ]
9436
+ }
9437
+ ),
9438
+ document.body
9439
+ );
9440
+ }
8500
9441
  var BLOCK_ITEMS = [
8501
9442
  {
8502
9443
  shortLabel: "\xB6",
@@ -8715,6 +9656,7 @@ function BlockMenuItem({
8715
9656
  }
8716
9657
  function FloatingMenu({ view, editorState }) {
8717
9658
  const [showEmptyHandle, setShowEmptyHandle] = useState7(false);
9659
+ const [editingLink, setEditingLink] = useState7(null);
8718
9660
  const dwellTimerRef = useRef4(null);
8719
9661
  const lastEmptyPosRef = useRef4(null);
8720
9662
  const [, setScrollTick] = useState7(0);
@@ -8740,9 +9682,16 @@ function FloatingMenu({ view, editorState }) {
8740
9682
  `;
8741
9683
  document.head.appendChild(style);
8742
9684
  }, []);
9685
+ const emptyPos = editorState ? emptyBlockCursorPos(editorState) : null;
9686
+ const hasSelection = editorState ? !editorState.selection.empty : false;
9687
+ const linkAtCursor = editorState && !hasSelection ? getLinkAtCursor(editorState) : null;
9688
+ useEffect4(() => {
9689
+ if (!editingLink) return;
9690
+ if (!linkAtCursor || linkAtCursor.from !== editingLink.from || linkAtCursor.to !== editingLink.to) {
9691
+ setEditingLink(null);
9692
+ }
9693
+ }, [editingLink, linkAtCursor]);
8743
9694
  if (!view || !editorState) return null;
8744
- const emptyPos = emptyBlockCursorPos(editorState);
8745
- const hasSelection = !editorState.selection.empty;
8746
9695
  if (emptyPos !== lastEmptyPosRef.current) {
8747
9696
  lastEmptyPosRef.current = emptyPos;
8748
9697
  if (dwellTimerRef.current) {
@@ -8766,23 +9715,39 @@ function FloatingMenu({ view, editorState }) {
8766
9715
  selectionRect
8767
9716
  }
8768
9717
  ),
8769
- !hasSelection && showEmptyHandle && emptyPos !== null && /* @__PURE__ */ jsx10(EmptyParaHandle, { view, cursorPos: emptyPos })
9718
+ !hasSelection && linkAtCursor && !editingLink && /* @__PURE__ */ jsx10(
9719
+ LinkHoverTooltip,
9720
+ {
9721
+ view,
9722
+ link: linkAtCursor,
9723
+ onEdit: (link2) => setEditingLink(link2)
9724
+ }
9725
+ ),
9726
+ !hasSelection && editingLink && /* @__PURE__ */ jsx10(
9727
+ LinkEditDialog,
9728
+ {
9729
+ view,
9730
+ link: editingLink,
9731
+ onClose: () => setEditingLink(null)
9732
+ }
9733
+ ),
9734
+ !hasSelection && !linkAtCursor && showEmptyHandle && emptyPos !== null && /* @__PURE__ */ jsx10(EmptyParaHandle, { view, cursorPos: emptyPos })
8770
9735
  ] }),
8771
9736
  document.body
8772
9737
  );
8773
9738
  }
8774
9739
 
8775
9740
  // 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");
9741
+ import { Plugin as Plugin9, PluginKey as PluginKey9 } from "prosemirror-state";
9742
+ import { Decoration as Decoration5, DecorationSet as DecorationSet5 } from "prosemirror-view";
9743
+ var inlineSuggestKey = new PluginKey9("inlineSuggest");
8779
9744
  var DEBOUNCE_MS = 600;
8780
9745
  function createInlineSuggestPlugin(provider, endpoint, options) {
8781
9746
  const { onContextChange } = options ?? {};
8782
9747
  return {
8783
9748
  name: "inlineSuggest",
8784
9749
  plugins: () => [
8785
- new Plugin8({
9750
+ new Plugin9({
8786
9751
  key: inlineSuggestKey,
8787
9752
  state: {
8788
9753
  init: () => ({ suggestion: null, anchorPos: null }),
@@ -8798,13 +9763,13 @@ function createInlineSuggestPlugin(provider, endpoint, options) {
8798
9763
  props: {
8799
9764
  decorations(state) {
8800
9765
  const ps = inlineSuggestKey.getState(state);
8801
- if (!ps?.suggestion || !state.selection.empty) return DecorationSet4.empty;
9766
+ if (!ps?.suggestion || !state.selection.empty) return DecorationSet5.empty;
8802
9767
  const ghost = document.createElement("span");
8803
9768
  ghost.textContent = ps.suggestion;
8804
9769
  ghost.style.cssText = "color: rgba(0,0,0,0.3); pointer-events: none; user-select: none;";
8805
9770
  ghost.setAttribute("aria-hidden", "true");
8806
- return DecorationSet4.create(state.doc, [
8807
- Decoration4.widget(state.selection.from, ghost, {
9771
+ return DecorationSet5.create(state.doc, [
9772
+ Decoration5.widget(state.selection.from, ghost, {
8808
9773
  side: 1,
8809
9774
  key: "inline-suggest"
8810
9775
  })
@@ -8959,6 +9924,7 @@ export {
8959
9924
  createLinkPlugin,
8960
9925
  createMarkdownClipboardPlugin,
8961
9926
  createNoteBlockPlugin,
9927
+ createPlaceholderPlugin,
8962
9928
  createPluginArray,
8963
9929
  createReactNodeView,
8964
9930
  createSlashCommandPlugin,