@tutti-os/ui-rich-text 0.0.11 → 0.0.13

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.
@@ -2,12 +2,12 @@ import {
2
2
  createDefaultRichTextI18nRuntime
3
3
  } from "../chunk-VQHCWUBH.js";
4
4
  import {
5
- createRichTextAtRegistry,
6
5
  createRichTextMentionAttrs,
6
+ createRichTextTriggerRegistry,
7
7
  getRichTextMentionDisplayText,
8
- renderRichTextAtInsertResult,
8
+ renderRichTextTriggerInsertResult,
9
9
  resolveRichTextMentionView
10
- } from "../chunk-K5POY2YJ.js";
10
+ } from "../chunk-QK6NMNYN.js";
11
11
  import {
12
12
  findRichTextMarkdownLinks,
13
13
  isRichTextMentionHref,
@@ -18,9 +18,9 @@ import {
18
18
  parseRichTextMentionHref,
19
19
  serializeRichTextDocumentToContent,
20
20
  workspaceReferenceNodeName
21
- } from "../chunk-52PIIFZA.js";
21
+ } from "../chunk-QOVFZUZN.js";
22
22
 
23
- // src/editor/RichTextAtEditor.tsx
23
+ // src/editor/RichTextTriggerEditor.tsx
24
24
  import {
25
25
  useEffect as useEffect2,
26
26
  useLayoutEffect,
@@ -36,12 +36,18 @@ import { EditorContent, useEditor } from "@tiptap/react";
36
36
  import { ViewportMenuSurface } from "@tutti-os/ui-system/components";
37
37
  import { cn } from "@tutti-os/ui-system/utils";
38
38
 
39
- // src/editor/richTextAtQuery.ts
40
- function isTriggerPrefixBoundary(character) {
39
+ // src/editor/richTextTriggerQuery.ts
40
+ function isRichTextTriggerPrefixBoundary(character, boundary) {
41
+ if (boundary === "whitespace") {
42
+ return /\s/.test(character);
43
+ }
41
44
  return /[\s,;:!?<>{}|\\'"`~()[\]]/.test(character);
42
45
  }
43
- function findRichTextAtQuery(value, caret) {
46
+ function findRichTextTriggerQuery(value, caret, triggerConfigs) {
44
47
  const cursor = Math.max(0, Math.min(caret, value.length));
48
+ if (triggerConfigs.length === 0) {
49
+ return null;
50
+ }
45
51
  let segmentStart = cursor;
46
52
  while (segmentStart > 0) {
47
53
  const previous = value[segmentStart - 1] ?? "";
@@ -51,9 +57,18 @@ function findRichTextAtQuery(value, caret) {
51
57
  segmentStart -= 1;
52
58
  }
53
59
  const segment = value.slice(segmentStart, cursor);
54
- for (let index = segment.lastIndexOf("@"); index >= 0; index = segment.lastIndexOf("@", index - 1)) {
60
+ for (let index = segment.length - 1; index >= 0; index -= 1) {
61
+ const trigger = segment[index];
62
+ const matchingConfigs = triggerConfigs.filter(
63
+ (config) => config.trigger === trigger
64
+ );
65
+ if (matchingConfigs.length === 0) {
66
+ continue;
67
+ }
55
68
  const previous = segment[index - 1] ?? "";
56
- if (index > 0 && !isTriggerPrefixBoundary(previous)) {
69
+ if (index > 0 && !matchingConfigs.some(
70
+ (config) => isRichTextTriggerPrefixBoundary(previous, config.boundary)
71
+ )) {
57
72
  continue;
58
73
  }
59
74
  const candidate = segment.slice(index);
@@ -63,12 +78,13 @@ function findRichTextAtQuery(value, caret) {
63
78
  return {
64
79
  from: segmentStart + index,
65
80
  to: cursor,
81
+ trigger,
66
82
  keyword: candidate.slice(1)
67
83
  };
68
84
  }
69
85
  return null;
70
86
  }
71
- async function queryRichTextAtMatches(registry, input) {
87
+ async function queryRichTextTriggerMatches(registry, input) {
72
88
  try {
73
89
  return await registry.query(input);
74
90
  } catch {
@@ -86,16 +102,74 @@ function isRichTextImeComposing(event) {
86
102
  return keyCode === 229 || which === 229;
87
103
  }
88
104
 
89
- // src/editor/richTextAtText.ts
105
+ // src/editor/richTextTriggerText.ts
90
106
  var defaultRichTextI18n = createDefaultRichTextI18nRuntime();
91
- function resolveRichTextAtText(overrides, removeDecorationAriaLabel, i18n = defaultRichTextI18n) {
107
+ function resolveRichTextTriggerText(overrides, removeDecorationAriaLabel, i18n = defaultRichTextI18n) {
92
108
  return {
93
109
  loadingLabel: overrides?.loadingLabel?.trim() || i18n.t("richTextAt.loading"),
94
110
  noMatchesLabel: overrides?.noMatchesLabel?.trim() || i18n.t("richTextAt.noMatches"),
95
111
  removeReferenceActionLabel: removeDecorationAriaLabel?.trim() || overrides?.removeReferenceActionLabel?.trim() || i18n.t("richTextAt.removeReferenceActionLabel")
96
112
  };
97
113
  }
98
- var defaultRichTextAtText = resolveRichTextAtText();
114
+ var defaultRichTextTriggerText = resolveRichTextTriggerText();
115
+
116
+ // src/editor/RichTextTriggerMenuItem.tsx
117
+ import { jsx, jsxs } from "react/jsx-runtime";
118
+ function RichTextTriggerMenuItem({
119
+ label,
120
+ selected,
121
+ subtitle,
122
+ thumbnailUrl,
123
+ onSelect
124
+ }) {
125
+ return /* @__PURE__ */ jsxs(
126
+ "button",
127
+ {
128
+ "aria-selected": selected,
129
+ className: [
130
+ "flex w-full cursor-pointer items-center gap-2 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-[var(--text-primary)] outline-0 transition-colors duration-100",
131
+ selected ? "bg-[var(--transparency-block)]" : "",
132
+ "hover:bg-[var(--transparency-block)]"
133
+ ].join(" "),
134
+ type: "button",
135
+ onMouseDown: (event) => {
136
+ event.preventDefault();
137
+ onSelect();
138
+ },
139
+ children: [
140
+ /* @__PURE__ */ jsx(RichTextTriggerMenuThumbnail, { thumbnailUrl }),
141
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-auto flex-col items-start gap-0.5", children: [
142
+ /* @__PURE__ */ jsx("span", { className: "w-full overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-5 font-semibold", children: label }),
143
+ subtitle ? /* @__PURE__ */ jsx("span", { className: "w-full overflow-hidden text-ellipsis whitespace-nowrap text-[11px] leading-4 text-[var(--text-secondary)]", children: subtitle }) : null
144
+ ] })
145
+ ]
146
+ }
147
+ );
148
+ }
149
+ function RichTextTriggerMenuThumbnail({
150
+ thumbnailUrl
151
+ }) {
152
+ const normalizedThumbnailUrl = thumbnailUrl?.trim() ?? "";
153
+ return /* @__PURE__ */ jsx(
154
+ "span",
155
+ {
156
+ "aria-hidden": "true",
157
+ className: "inline-grid size-4 flex-none place-items-center overflow-hidden rounded bg-[var(--bg-block,var(--transparency-block))]",
158
+ "data-rich-text-trigger-thumbnail": "true",
159
+ children: normalizedThumbnailUrl ? /* @__PURE__ */ jsx(
160
+ "img",
161
+ {
162
+ alt: "",
163
+ className: "block size-full object-cover object-center",
164
+ decoding: "async",
165
+ draggable: false,
166
+ loading: "lazy",
167
+ src: normalizedThumbnailUrl
168
+ }
169
+ ) : /* @__PURE__ */ jsx("span", { className: "block size-3 rounded-[3px] bg-[var(--transparency-block)]" })
170
+ }
171
+ );
172
+ }
99
173
 
100
174
  // src/extensions/mentionReference.ts
101
175
  import { mergeAttributes, Node } from "@tiptap/core";
@@ -103,26 +177,60 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
103
177
 
104
178
  // src/extensions/MentionReferenceNodeView.tsx
105
179
  import { NodeViewWrapper } from "@tiptap/react";
106
- import { jsx, jsxs } from "react/jsx-runtime";
180
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
181
+ function readMentionPresentationUrl(presentation) {
182
+ if (!presentation || typeof presentation !== "object") {
183
+ return null;
184
+ }
185
+ const value = presentation.iconUrl ?? presentation.thumbnailUrl;
186
+ if (typeof value !== "string") {
187
+ return null;
188
+ }
189
+ const trimmed = value.trim();
190
+ return trimmed || null;
191
+ }
107
192
  function MentionReferenceNodeView({
108
193
  node,
109
194
  selected
110
195
  }) {
111
- const label = typeof node.attrs.label === "string" ? node.attrs.label.trim() : "";
112
- return /* @__PURE__ */ jsx(
196
+ const label = typeof node.attrs.label === "string" ? node.attrs.label.trim().replace(/^@+/, "").trim() : "";
197
+ const iconUrl = readMentionPresentationUrl(node.attrs.presentation);
198
+ return /* @__PURE__ */ jsx2(
113
199
  NodeViewWrapper,
114
200
  {
115
201
  as: "span",
116
- className: `inline-flex max-w-full align-baseline ${selected ? "is-selected" : ""}`,
202
+ className: selected ? "is-selected" : void 0,
117
203
  contentEditable: false,
118
204
  "data-rich-text-mention-reference": "true",
119
- children: /* @__PURE__ */ jsxs(
205
+ children: /* @__PURE__ */ jsxs2(
120
206
  "span",
121
207
  {
122
- className: `inline-flex max-w-full items-center overflow-hidden rounded-md px-1.5 py-0.5 text-[13px] font-medium ${selected ? "bg-[var(--background-fronted)] text-[var(--text-primary)] shadow-soft" : "bg-transparency-block text-[var(--text-primary)]"}`,
208
+ className: [
209
+ "inline-flex max-w-full items-center gap-1 overflow-hidden rounded-md px-[7px] py-0.5 text-[13px] leading-[18px] font-semibold align-baseline text-[var(--text-primary)]",
210
+ selected ? "bg-[var(--background-fronted)] shadow-[var(--shadow-soft,0_1px_2px_rgb(0_0_0/8%))]" : "bg-[var(--transparency-block)]"
211
+ ].join(" "),
212
+ "data-rich-text-mention-chip": "true",
123
213
  children: [
124
- "@",
125
- label
214
+ iconUrl ? /* @__PURE__ */ jsx2(
215
+ "span",
216
+ {
217
+ "aria-hidden": "true",
218
+ className: "inline-grid size-4 flex-none place-items-center overflow-hidden rounded",
219
+ "data-rich-text-mention-icon": "true",
220
+ children: /* @__PURE__ */ jsx2(
221
+ "img",
222
+ {
223
+ alt: "",
224
+ className: "block size-full object-cover object-center",
225
+ decoding: "async",
226
+ draggable: false,
227
+ loading: "lazy",
228
+ src: iconUrl
229
+ }
230
+ )
231
+ }
232
+ ) : null,
233
+ label ? iconUrl ? label : `@${label}` : ""
126
234
  ]
127
235
  }
128
236
  )
@@ -142,26 +250,20 @@ var MentionReference = Node.create({
142
250
  entityId: {
143
251
  default: ""
144
252
  },
145
- href: {
146
- default: null
147
- },
148
- kind: {
149
- default: null
150
- },
151
253
  label: {
152
254
  default: ""
153
255
  },
154
- meta: {
256
+ presentation: {
155
257
  default: null
156
258
  },
157
- plugin: {
259
+ providerId: {
158
260
  default: ""
159
261
  },
262
+ scope: {
263
+ default: null
264
+ },
160
265
  trigger: {
161
266
  default: "@"
162
- },
163
- version: {
164
- default: null
165
267
  }
166
268
  };
167
269
  },
@@ -170,13 +272,14 @@ var MentionReference = Node.create({
170
272
  },
171
273
  renderHTML({ HTMLAttributes }) {
172
274
  const label = typeof HTMLAttributes.label === "string" ? HTMLAttributes.label : "";
275
+ const displayLabel = label.trim().replace(/^@+/, "").trim();
173
276
  return [
174
277
  "span",
175
278
  mergeAttributes(HTMLAttributes, {
176
279
  "data-rich-text-mention-reference": "true",
177
280
  class: "inline-flex max-w-full items-center overflow-hidden rounded-md bg-transparency-block px-1.5 py-0.5 align-baseline text-[13px] font-medium text-[var(--text-primary)]"
178
281
  }),
179
- `@${label}`
282
+ displayLabel ? `@${displayLabel}` : ""
180
283
  ];
181
284
  },
182
285
  addNodeView() {
@@ -239,7 +342,7 @@ function isPathRoot(path) {
239
342
  }
240
343
 
241
344
  // src/extensions/WorkspaceReferenceNodeView.tsx
242
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
345
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
243
346
  var richTextWorkspaceReferencePillClassName = "max-w-[18rem]";
244
347
  function WorkspaceReferenceNodeView({
245
348
  deleteNode,
@@ -255,7 +358,7 @@ function WorkspaceReferenceNodeView({
255
358
  const label = typeof attrs.label === "string" ? attrs.label : "";
256
359
  const path = typeof attrs.path === "string" ? attrs.path : "";
257
360
  const presentation = getWorkspaceReferencePresentation(label, path);
258
- const removeActionAriaLabel = typeof extensionOptions.removeActionAriaLabel === "string" ? extensionOptions.removeActionAriaLabel : defaultRichTextAtText.removeReferenceActionLabel;
361
+ const removeActionAriaLabel = typeof extensionOptions.removeActionAriaLabel === "string" ? extensionOptions.removeActionAriaLabel : defaultRichTextTriggerText.removeReferenceActionLabel;
259
362
  useEffect(() => {
260
363
  const syncEditable = () => {
261
364
  setIsEditable(editor.isEditable);
@@ -276,15 +379,15 @@ function WorkspaceReferenceNodeView({
276
379
  }
277
380
  deleteNode();
278
381
  };
279
- return /* @__PURE__ */ jsx2(
382
+ return /* @__PURE__ */ jsx3(
280
383
  NodeViewWrapper2,
281
384
  {
282
385
  as: "span",
283
386
  className: `inline-flex max-w-full align-baseline ${selected ? "is-selected" : ""}`,
284
387
  contentEditable: false,
285
388
  "data-rich-text-workspace-reference": "true",
286
- children: /* @__PURE__ */ jsxs2(Tooltip, { children: [
287
- /* @__PURE__ */ jsx2(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx2(
389
+ children: /* @__PURE__ */ jsxs3(Tooltip, { children: [
390
+ /* @__PURE__ */ jsx3(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx3(
288
391
  MentionPill,
289
392
  {
290
393
  className: richTextWorkspaceReferencePillClassName,
@@ -298,7 +401,7 @@ function WorkspaceReferenceNodeView({
298
401
  } : void 0
299
402
  }
300
403
  ) }),
301
- /* @__PURE__ */ jsx2(
404
+ /* @__PURE__ */ jsx3(
302
405
  TooltipContent,
303
406
  {
304
407
  className: "max-w-md whitespace-normal break-all",
@@ -316,7 +419,7 @@ var WorkspaceReference = Node2.create({
316
419
  name: workspaceReferenceNodeName,
317
420
  addOptions() {
318
421
  return {
319
- removeActionAriaLabel: defaultRichTextAtText.removeReferenceActionLabel
422
+ removeActionAriaLabel: defaultRichTextTriggerText.removeReferenceActionLabel
320
423
  };
321
424
  },
322
425
  group: "inline",
@@ -381,12 +484,12 @@ var WorkspaceReference = Node2.create({
381
484
  }
382
485
  });
383
486
 
384
- // src/editor/RichTextAtEditor.tsx
385
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
386
- function RichTextAtEditor({
487
+ // src/editor/RichTextTriggerEditor.tsx
488
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
489
+ function RichTextTriggerEditor({
387
490
  value,
388
491
  onChange,
389
- providers = [],
492
+ triggerProviders = [],
390
493
  placeholder,
391
494
  disabled = false,
392
495
  className,
@@ -398,11 +501,12 @@ function RichTextAtEditor({
398
501
  i18n,
399
502
  textOverrides,
400
503
  overlay,
401
- focusSignal
504
+ focusSignal,
505
+ menuZIndex
402
506
  }) {
403
507
  const menuOffset = 6;
404
508
  const normalizedValue = normalizeRichTextContent(value);
405
- const text = resolveRichTextAtText(
509
+ const text = resolveRichTextTriggerText(
406
510
  textOverrides,
407
511
  removeDecorationAriaLabel,
408
512
  i18n
@@ -410,14 +514,23 @@ function RichTextAtEditor({
410
514
  const latestOnChangeRef = useRef(onChange);
411
515
  const lastSerializedValueRef = useRef(normalizedValue);
412
516
  const lastFocusSignalRef = useRef(focusSignal);
517
+ const mentionHydrationRequestRef = useRef(0);
413
518
  const containerRef = useRef(null);
414
519
  const registry = useMemo(
415
- () => createRichTextAtRegistry(providers),
416
- [providers]
520
+ () => createRichTextTriggerRegistry(triggerProviders),
521
+ [triggerProviders]
522
+ );
523
+ const activeTriggerConfigs = useMemo(
524
+ () => registry.listTriggerConfigs(),
525
+ [registry]
417
526
  );
418
527
  const [isFocused, setIsFocused] = useState2(false);
419
- const [query, setQuery] = useState2(null);
420
- const [matches, setMatches] = useState2([]);
528
+ const [query, setQuery] = useState2(
529
+ null
530
+ );
531
+ const [matches, setMatches] = useState2(
532
+ []
533
+ );
421
534
  const [activeIndex, setActiveIndex] = useState2(0);
422
535
  const [isLoading, setIsLoading] = useState2(false);
423
536
  const [menuPoint, setMenuPoint] = useState2(
@@ -449,6 +562,7 @@ function RichTextAtEditor({
449
562
  onBlur() {
450
563
  window.setTimeout(() => {
451
564
  setIsFocused(false);
565
+ setQuery(null);
452
566
  setMatches([]);
453
567
  setActiveIndex(0);
454
568
  setIsLoading(false);
@@ -486,6 +600,33 @@ function RichTextAtEditor({
486
600
  );
487
601
  lastSerializedValueRef.current = normalizedValue;
488
602
  }, [editor, normalizedValue]);
603
+ useEffect2(() => {
604
+ if (!editor) {
605
+ return;
606
+ }
607
+ const requestId = mentionHydrationRequestRef.current + 1;
608
+ mentionHydrationRequestRef.current = requestId;
609
+ const mentions = collectHydratableMentionNodes(editor);
610
+ if (mentions.length === 0) {
611
+ return;
612
+ }
613
+ for (const mention of mentions) {
614
+ const provider = registry.getProvider(mention.attrs.providerId);
615
+ if (!provider?.resolveMention) {
616
+ continue;
617
+ }
618
+ void Promise.resolve(provider.resolveMention(mention.attrs)).then((resolved) => {
619
+ if (!resolved || mentionHydrationRequestRef.current !== requestId || editor.isDestroyed) {
620
+ return;
621
+ }
622
+ applyResolvedMentionAttrs(editor, mention.pos, mention.attrs, {
623
+ label: resolved.label,
624
+ presentation: resolved.presentation
625
+ });
626
+ }).catch(() => {
627
+ });
628
+ }
629
+ }, [editor, registry, normalizedValue]);
489
630
  useEffect2(() => {
490
631
  if (!editor) {
491
632
  return;
@@ -510,11 +651,26 @@ function RichTextAtEditor({
510
651
  return;
511
652
  }
512
653
  const updateQueryState = () => {
513
- const nextQuery = isFocused ? findEditorAtQuery(editor) : null;
654
+ const nextQuery = findEditorAtQuery(editor, activeTriggerConfigs);
655
+ logRichTextTriggerDebug("query-state", {
656
+ activeTriggerConfigs,
657
+ focused: editor.isFocused,
658
+ query: nextQuery,
659
+ selection: {
660
+ empty: editor.state.selection.empty,
661
+ from: editor.state.selection.from,
662
+ to: editor.state.selection.to
663
+ },
664
+ textBeforeCursor: readEditorTextBeforeCursor(editor)
665
+ });
514
666
  setQuery(nextQuery);
515
667
  };
516
668
  const updateFocus = () => {
517
- setIsFocused(editor.isFocused);
669
+ const nextFocused = editor.isFocused;
670
+ logRichTextTriggerDebug("focus-state", {
671
+ focused: nextFocused
672
+ });
673
+ setIsFocused(nextFocused);
518
674
  updateQueryState();
519
675
  };
520
676
  updateQueryState();
@@ -528,15 +684,20 @@ function RichTextAtEditor({
528
684
  editor.off("focus", updateFocus);
529
685
  editor.off("blur", updateFocus);
530
686
  };
531
- }, [editor, isFocused]);
687
+ }, [activeTriggerConfigs, editor]);
532
688
  useEffect2(() => {
533
- if (!editor || !query || !isFocused || providers.length === 0) {
689
+ if (!editor || !query || activeTriggerConfigs.length === 0) {
534
690
  setMatches([]);
535
691
  setActiveIndex(0);
536
692
  setIsLoading(false);
537
693
  return;
538
694
  }
539
695
  if (query.keyword.length < minQueryLength) {
696
+ logRichTextTriggerDebug("query-skipped", {
697
+ minQueryLength,
698
+ query,
699
+ reason: "keyword-too-short"
700
+ });
540
701
  setMatches([]);
541
702
  setActiveIndex(0);
542
703
  setIsLoading(false);
@@ -544,10 +705,16 @@ function RichTextAtEditor({
544
705
  }
545
706
  const abortController = new AbortController();
546
707
  setIsLoading(true);
547
- void queryRichTextAtMatches(registry, {
708
+ logRichTextTriggerDebug("query-start", {
709
+ maxResults,
710
+ minQueryLength,
711
+ query
712
+ });
713
+ void queryRichTextTriggerMatches(registry, {
548
714
  abortSignal: abortController.signal,
549
715
  keyword: query.keyword,
550
716
  maxResults,
717
+ trigger: query.trigger,
551
718
  context: {
552
719
  blockText: editor.state.selection.$from.parent.textBetween(
553
720
  0,
@@ -561,6 +728,16 @@ function RichTextAtEditor({
561
728
  if (abortController.signal.aborted) {
562
729
  return;
563
730
  }
731
+ logRichTextTriggerDebug("query-result", {
732
+ matchCount: nextMatches.length,
733
+ matches: nextMatches.map((match) => ({
734
+ key: match.key,
735
+ label: match.label,
736
+ providerId: match.providerId,
737
+ trigger: match.trigger
738
+ })),
739
+ query
740
+ });
564
741
  setMatches(nextMatches);
565
742
  setActiveIndex(
566
743
  (current) => nextMatches.length === 0 ? 0 : Math.max(0, Math.min(current, nextMatches.length - 1))
@@ -575,24 +752,38 @@ function RichTextAtEditor({
575
752
  };
576
753
  }, [
577
754
  editor,
578
- isFocused,
579
755
  maxResults,
580
756
  minQueryLength,
581
- providers.length,
757
+ activeTriggerConfigs.length,
582
758
  query,
583
759
  registry
584
760
  ]);
585
761
  useLayoutEffect(() => {
586
- if (!editor || !query || !isFocused) {
762
+ if (!editor || !query) {
763
+ logRichTextTriggerDebug("menu-point-cleared", {
764
+ hasEditor: Boolean(editor),
765
+ query
766
+ });
587
767
  setMenuPoint(null);
588
768
  return;
589
769
  }
590
770
  const updateMenuPoint = () => {
591
771
  const coords = editor.view.coordsAtPos(editor.state.selection.from);
592
- setMenuPoint({
772
+ const nextMenuPoint = {
593
773
  x: coords.left,
594
774
  y: coords.bottom + menuOffset
775
+ };
776
+ logRichTextTriggerDebug("menu-point", {
777
+ coords: {
778
+ bottom: coords.bottom,
779
+ left: coords.left,
780
+ right: coords.right,
781
+ top: coords.top
782
+ },
783
+ menuPoint: nextMenuPoint,
784
+ query
595
785
  });
786
+ setMenuPoint(nextMenuPoint);
596
787
  };
597
788
  updateMenuPoint();
598
789
  window.addEventListener("resize", updateMenuPoint);
@@ -604,9 +795,31 @@ function RichTextAtEditor({
604
795
  window.removeEventListener("resize", updateMenuPoint);
605
796
  window.removeEventListener("scroll", updateMenuPoint, true);
606
797
  };
607
- }, [editor, isFocused, menuOffset, query]);
608
- const isMenuOpen = isFocused && !!query && (isLoading || matches.length > 0);
798
+ }, [editor, menuOffset, query]);
799
+ const canQueryTrigger = !!query && activeTriggerConfigs.length > 0 && query.keyword.length >= minQueryLength;
800
+ const isMenuOpen = canQueryTrigger && (isFocused || !!menuPoint);
609
801
  const isEmpty = !editor || serializeRichTextDocumentToContent(editor.getJSON()).trim().length === 0;
802
+ useEffect2(() => {
803
+ logRichTextTriggerDebug("menu-state", {
804
+ activeIndex,
805
+ canQueryTrigger,
806
+ isFocused,
807
+ isLoading,
808
+ isMenuOpen,
809
+ matchesLength: matches.length,
810
+ menuPoint,
811
+ query
812
+ });
813
+ }, [
814
+ activeIndex,
815
+ canQueryTrigger,
816
+ isFocused,
817
+ isLoading,
818
+ isMenuOpen,
819
+ matches.length,
820
+ menuPoint,
821
+ query
822
+ ]);
610
823
  const applyMatch = (match) => {
611
824
  if (!editor || !query) {
612
825
  return;
@@ -628,7 +841,18 @@ function RichTextAtEditor({
628
841
  if (isRichTextImeComposing(event)) {
629
842
  return;
630
843
  }
631
- if (!isMenuOpen || matches.length === 0) {
844
+ if (!isMenuOpen) {
845
+ return;
846
+ }
847
+ if (event.key === "Escape") {
848
+ event.preventDefault();
849
+ setMatches([]);
850
+ setActiveIndex(0);
851
+ setIsLoading(false);
852
+ setMenuPoint(null);
853
+ return;
854
+ }
855
+ if (matches.length === 0) {
632
856
  return;
633
857
  }
634
858
  if (event.key === "ArrowDown") {
@@ -652,22 +876,15 @@ function RichTextAtEditor({
652
876
  applyMatch(match);
653
877
  return;
654
878
  }
655
- if (event.key === "Escape") {
656
- event.preventDefault();
657
- setMatches([]);
658
- setActiveIndex(0);
659
- setIsLoading(false);
660
- setMenuPoint(null);
661
- }
662
879
  };
663
- return /* @__PURE__ */ jsxs3(
880
+ return /* @__PURE__ */ jsxs4(
664
881
  "div",
665
882
  {
666
883
  className: cn("relative min-w-0 w-full", className),
667
884
  ref: containerRef,
668
885
  children: [
669
- /* @__PURE__ */ jsx3("div", { className: "w-full min-w-0", onKeyDownCapture: handleKeyDown, children: /* @__PURE__ */ jsx3(EditorContent, { editor }) }),
670
- isEmpty && placeholder ? /* @__PURE__ */ jsx3("div", { className: "pointer-events-none absolute top-0 right-0 left-0 px-0 py-0 text-[var(--text-placeholder)]", children: /* @__PURE__ */ jsx3(
886
+ /* @__PURE__ */ jsx4("div", { className: "w-full min-w-0", onKeyDownCapture: handleKeyDown, children: /* @__PURE__ */ jsx4(EditorContent, { editor }) }),
887
+ isEmpty && placeholder ? /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute top-0 right-0 left-0 px-0 py-0 text-[var(--text-placeholder)]", children: /* @__PURE__ */ jsx4(
671
888
  "div",
672
889
  {
673
890
  className: cn(
@@ -679,7 +896,7 @@ function RichTextAtEditor({
679
896
  }
680
897
  ) }) : null,
681
898
  overlay,
682
- isMenuOpen && menuPoint ? /* @__PURE__ */ jsx3(
899
+ isMenuOpen && menuPoint ? /* @__PURE__ */ jsx4(
683
900
  ViewportMenuSurface,
684
901
  {
685
902
  open: true,
@@ -694,33 +911,47 @@ function RichTextAtEditor({
694
911
  height: 256
695
912
  }
696
913
  },
697
- children: matches.length > 0 ? matches.map((match, index) => /* @__PURE__ */ jsxs3(
698
- "button",
914
+ style: menuZIndex === void 0 ? void 0 : { zIndex: menuZIndex },
915
+ children: matches.length > 0 ? matches.map((match, index) => /* @__PURE__ */ jsx4(
916
+ RichTextTriggerMenuItem,
699
917
  {
700
- "aria-selected": index === activeIndex,
701
- className: cn(
702
- "flex w-full cursor-pointer flex-col items-start gap-0.5 rounded-md px-2.5 py-2 text-left outline-none transition-colors",
703
- index === activeIndex ? "bg-transparency-block text-[var(--text-primary)]" : "text-[var(--text-primary)] hover:bg-transparency-block"
704
- ),
705
- type: "button",
706
- onMouseDown: (event) => {
707
- event.preventDefault();
708
- applyMatch(match);
709
- },
710
- children: [
711
- /* @__PURE__ */ jsx3("div", { className: "text-[13px] leading-5 font-medium", children: match.label }),
712
- match.subtitle ? /* @__PURE__ */ jsx3("div", { className: "text-[11px] leading-4 text-[var(--text-secondary)]", children: match.subtitle }) : null
713
- ]
918
+ label: match.label,
919
+ selected: index === activeIndex,
920
+ subtitle: match.subtitle,
921
+ thumbnailUrl: match.thumbnailUrl,
922
+ onSelect: () => applyMatch(match)
714
923
  },
715
924
  `${match.providerId}:${match.key}`
716
- )) : /* @__PURE__ */ jsx3("div", { className: "px-3 py-2 text-[11px] leading-4 text-[var(--text-secondary)]", children: isLoading ? text.loadingLabel : text.noMatchesLabel })
925
+ )) : /* @__PURE__ */ jsx4("div", { className: "px-3 py-2 text-[11px] leading-4 text-[var(--text-secondary)]", children: isLoading ? text.loadingLabel : text.noMatchesLabel })
717
926
  }
718
927
  ) : null
719
928
  ]
720
929
  }
721
930
  );
722
931
  }
723
- function findEditorAtQuery(editor) {
932
+ function findEditorAtQuery(editor, triggers) {
933
+ const query = findEditorTriggerQuery(editor, triggers);
934
+ if (!query) {
935
+ return null;
936
+ }
937
+ return query;
938
+ }
939
+ function readEditorTextBeforeCursor(editor) {
940
+ const { selection } = editor.state;
941
+ if (!selection.empty) {
942
+ return null;
943
+ }
944
+ const { $from } = selection;
945
+ if (!$from.parent.isTextblock) {
946
+ return null;
947
+ }
948
+ return $from.parent.textBetween(0, $from.parentOffset, "\n", "\uFFFC");
949
+ }
950
+ function logRichTextTriggerDebug(event, payload) {
951
+ console.info(`[tutti-rich-text-trigger] ${event}`, payload);
952
+ window.__tuttiRichTextDebugLog?.(event, payload);
953
+ }
954
+ function findEditorTriggerQuery(editor, triggers) {
724
955
  const { selection } = editor.state;
725
956
  if (!selection.empty) {
726
957
  return null;
@@ -735,7 +966,11 @@ function findEditorAtQuery(editor) {
735
966
  "\n",
736
967
  "\uFFFC"
737
968
  );
738
- const query = findRichTextAtQuery(textBeforeCursor, textBeforeCursor.length);
969
+ const query = findRichTextTriggerQuery(
970
+ textBeforeCursor,
971
+ textBeforeCursor.length,
972
+ triggers
973
+ );
739
974
  if (!query) {
740
975
  return null;
741
976
  }
@@ -743,6 +978,7 @@ function findEditorAtQuery(editor) {
743
978
  return {
744
979
  from: selection.from - distanceFromQueryStart,
745
980
  keyword: query.keyword,
981
+ trigger: query.trigger,
746
982
  to: selection.from
747
983
  };
748
984
  }
@@ -770,8 +1006,95 @@ function renderInsertResultAsEditorContent(providerId, result) {
770
1006
  return null;
771
1007
  }
772
1008
  }
1009
+ function collectHydratableMentionNodes(editor) {
1010
+ const mentions = [];
1011
+ editor.state.doc.descendants((node, pos) => {
1012
+ if (node.type.name !== mentionReferenceNodeName) {
1013
+ return;
1014
+ }
1015
+ const attrs = node.attrs;
1016
+ if (attrs.trigger !== "@" || typeof attrs.providerId !== "string" || !attrs.providerId.trim() || typeof attrs.entityId !== "string" || !attrs.entityId.trim() || typeof attrs.label !== "string" || !attrs.label.trim()) {
1017
+ return;
1018
+ }
1019
+ mentions.push({
1020
+ pos,
1021
+ attrs: {
1022
+ trigger: "@",
1023
+ providerId: attrs.providerId.trim(),
1024
+ entityId: attrs.entityId.trim(),
1025
+ label: attrs.label.trim().replace(/^@+/, "").trim(),
1026
+ scope: normalizeMentionStringRecord(attrs.scope),
1027
+ presentation: normalizeMentionPresentation(attrs.presentation)
1028
+ }
1029
+ });
1030
+ });
1031
+ return mentions;
1032
+ }
1033
+ function applyResolvedMentionAttrs(editor, pos, currentAttrs, resolved) {
1034
+ const node = editor.state.doc.nodeAt(pos);
1035
+ if (!node || node.type.name !== mentionReferenceNodeName) {
1036
+ return;
1037
+ }
1038
+ const attrs = node.attrs;
1039
+ if (attrs.providerId !== currentAttrs.providerId || attrs.entityId !== currentAttrs.entityId || JSON.stringify(normalizeMentionStringRecord(attrs.scope) ?? {}) !== JSON.stringify(currentAttrs.scope ?? {})) {
1040
+ return;
1041
+ }
1042
+ const nextLabel = resolved.label?.trim().replace(/^@+/, "").trim();
1043
+ const nextPresentation = normalizeMentionPresentation(resolved.presentation);
1044
+ const nextAttrs = {
1045
+ trigger: "@",
1046
+ providerId: currentAttrs.providerId,
1047
+ entityId: currentAttrs.entityId,
1048
+ label: nextLabel || currentAttrs.label,
1049
+ scope: currentAttrs.scope,
1050
+ presentation: nextPresentation ?? currentAttrs.presentation
1051
+ };
1052
+ if (attrs.label === nextAttrs.label && JSON.stringify(normalizeMentionPresentation(attrs.presentation) ?? {}) === JSON.stringify(nextAttrs.presentation ?? {})) {
1053
+ return;
1054
+ }
1055
+ const transaction = editor.state.tr.setNodeMarkup(pos, void 0, nextAttrs);
1056
+ transaction.setMeta("addToHistory", false);
1057
+ transaction.setMeta("preventUpdate", true);
1058
+ editor.view.dispatch(transaction);
1059
+ }
1060
+ function normalizeMentionStringRecord(value) {
1061
+ if (!value || typeof value !== "object") {
1062
+ return void 0;
1063
+ }
1064
+ const entries = Object.entries(value).map(
1065
+ ([key, entryValue]) => [
1066
+ key.trim(),
1067
+ typeof entryValue === "string" ? entryValue.trim() : ""
1068
+ ]
1069
+ ).filter(([key, entryValue]) => key.length > 0 && entryValue.length > 0).sort(([left], [right]) => left.localeCompare(right));
1070
+ return entries.length > 0 ? Object.freeze(Object.fromEntries(entries)) : void 0;
1071
+ }
1072
+ function normalizeMentionPresentation(value) {
1073
+ if (!value || typeof value !== "object") {
1074
+ return void 0;
1075
+ }
1076
+ const source = value;
1077
+ const next = {};
1078
+ for (const key of [
1079
+ "iconUrl",
1080
+ "thumbnailUrl",
1081
+ "subtitle",
1082
+ "description",
1083
+ "status"
1084
+ ]) {
1085
+ const fieldValue = source[key];
1086
+ if (typeof fieldValue !== "string") {
1087
+ continue;
1088
+ }
1089
+ const trimmed = fieldValue.trim();
1090
+ if (trimmed) {
1091
+ next[key] = trimmed;
1092
+ }
1093
+ }
1094
+ return Object.keys(next).length > 0 ? Object.freeze(next) : void 0;
1095
+ }
773
1096
 
774
- // src/editor/RichTextAtTextarea.tsx
1097
+ // src/editor/RichTextTriggerTextarea.tsx
775
1098
  import {
776
1099
  useEffect as useEffect3,
777
1100
  useLayoutEffect as useLayoutEffect2,
@@ -910,7 +1233,7 @@ function getTextareaPresentationStyle(textarea) {
910
1233
  // src/editor/richTextTextareaDecorations.tsx
911
1234
  import { MentionPill as MentionPill2 } from "@tutti-os/ui-system/components";
912
1235
  import { cn as cn2 } from "@tutti-os/ui-system/utils";
913
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1236
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
914
1237
  var richTextWorkspaceReferencePillClassName2 = "max-w-[18rem]";
915
1238
  function RichTextTextareaDecoratedContent({
916
1239
  onClickSegment,
@@ -921,7 +1244,7 @@ function RichTextTextareaDecoratedContent({
921
1244
  segments,
922
1245
  textareaStyle
923
1246
  }) {
924
- return /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-0 z-10 overflow-hidden", children: /* @__PURE__ */ jsx4(
1247
+ return /* @__PURE__ */ jsx5("div", { className: "pointer-events-none absolute inset-0 z-10 overflow-hidden", children: /* @__PURE__ */ jsx5(
925
1248
  "div",
926
1249
  {
927
1250
  className: "min-h-full min-w-full",
@@ -931,11 +1254,11 @@ function RichTextTextareaDecoratedContent({
931
1254
  },
932
1255
  children: segments.map((segment, index) => {
933
1256
  if (segment.type === "text") {
934
- return /* @__PURE__ */ jsx4("span", { children: segment.text }, index);
1257
+ return /* @__PURE__ */ jsx5("span", { children: segment.text }, index);
935
1258
  }
936
- return /* @__PURE__ */ jsxs4("span", { className: "relative", children: [
937
- /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "text-transparent", children: segment.text }),
938
- /* @__PURE__ */ jsx4(
1259
+ return /* @__PURE__ */ jsxs5("span", { className: "relative", children: [
1260
+ /* @__PURE__ */ jsx5("span", { "aria-hidden": "true", className: "text-transparent", children: segment.text }),
1261
+ /* @__PURE__ */ jsx5(
939
1262
  MentionPill2,
940
1263
  {
941
1264
  className: cn2(
@@ -953,7 +1276,7 @@ function RichTextTextareaDecoratedContent({
953
1276
  onRemoveSegment(segment);
954
1277
  }
955
1278
  },
956
- summary: /* @__PURE__ */ jsx4(
1279
+ summary: /* @__PURE__ */ jsx5(
957
1280
  "span",
958
1281
  {
959
1282
  className: cn2(
@@ -1077,12 +1400,12 @@ function getTextareaCaretViewportPoint(textarea, selectionStart) {
1077
1400
  }
1078
1401
  }
1079
1402
 
1080
- // src/editor/RichTextAtTextarea.tsx
1081
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1082
- function RichTextAtTextarea({
1403
+ // src/editor/RichTextTriggerTextarea.tsx
1404
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1405
+ function RichTextTriggerTextarea({
1083
1406
  value,
1084
1407
  onChange,
1085
- providers = [],
1408
+ triggerProviders = [],
1086
1409
  placeholder,
1087
1410
  disabled = false,
1088
1411
  className,
@@ -1096,14 +1419,16 @@ function RichTextAtTextarea({
1096
1419
  overlay
1097
1420
  }) {
1098
1421
  const menuOffset = 6;
1099
- const text = resolveRichTextAtText(
1422
+ const text = resolveRichTextTriggerText(
1100
1423
  textOverrides,
1101
1424
  removeDecorationAriaLabel,
1102
1425
  i18n
1103
1426
  );
1104
1427
  const textareaRef = useRef2(null);
1105
1428
  const [selectionStart, setSelectionStart] = useState3(0);
1106
- const [matches, setMatches] = useState3([]);
1429
+ const [matches, setMatches] = useState3(
1430
+ []
1431
+ );
1107
1432
  const [activeIndex, setActiveIndex] = useState3(0);
1108
1433
  const [isLoading, setIsLoading] = useState3(false);
1109
1434
  const [isFocused, setIsFocused] = useState3(false);
@@ -1114,16 +1439,23 @@ function RichTextAtTextarea({
1114
1439
  const [textareaPresentationStyle, setTextareaPresentationStyle] = useState3(null);
1115
1440
  const pendingSelectionRef = useRef2(null);
1116
1441
  const registry = useMemo2(
1117
- () => createRichTextAtRegistry(providers),
1118
- [providers]
1442
+ () => createRichTextTriggerRegistry(triggerProviders),
1443
+ [triggerProviders]
1444
+ );
1445
+ const activeTriggerConfigs = useMemo2(
1446
+ () => registry.listTriggerConfigs(),
1447
+ [registry]
1119
1448
  );
1120
1449
  const decorationSegments = useMemo2(
1121
1450
  () => buildRichTextTextareaDecorationSegments(value),
1122
1451
  [value]
1123
1452
  );
1124
1453
  const hasDecorations = hasRichTextTextareaDecorations(decorationSegments);
1125
- const query = isFocused ? findRichTextAtQuery(value, selectionStart) : null;
1126
- const shouldQuery = query !== null && query.keyword.length >= minQueryLength && providers.length > 0;
1454
+ const query = useMemo2(
1455
+ () => isFocused ? findRichTextTriggerQuery(value, selectionStart, activeTriggerConfigs) : null,
1456
+ [activeTriggerConfigs, isFocused, selectionStart, value]
1457
+ );
1458
+ const shouldQuery = query !== null && query.keyword.length >= minQueryLength && activeTriggerConfigs.length > 0;
1127
1459
  const isMenuOpen = isFocused && shouldQuery && (isLoading || matches.length > 0);
1128
1460
  useEffect3(() => {
1129
1461
  const nextSelection = pendingSelectionRef.current;
@@ -1142,10 +1474,11 @@ function RichTextAtTextarea({
1142
1474
  }
1143
1475
  const abortController = new AbortController();
1144
1476
  setIsLoading(true);
1145
- void queryRichTextAtMatches(registry, {
1477
+ void queryRichTextTriggerMatches(registry, {
1146
1478
  abortSignal: abortController.signal,
1147
1479
  keyword: query.keyword,
1148
1480
  maxResults,
1481
+ trigger: query.trigger,
1149
1482
  context: {
1150
1483
  documentText: value,
1151
1484
  blockText: value
@@ -1273,14 +1606,15 @@ function RichTextAtTextarea({
1273
1606
  });
1274
1607
  };
1275
1608
  const applyMatch = (match) => {
1276
- const currentQuery = findRichTextAtQuery(
1609
+ const currentQuery = findRichTextTriggerQuery(
1277
1610
  value,
1278
- textareaRef.current?.selectionStart ?? selectionStart
1611
+ textareaRef.current?.selectionStart ?? selectionStart,
1612
+ activeTriggerConfigs
1279
1613
  );
1280
1614
  if (!currentQuery) {
1281
1615
  return;
1282
1616
  }
1283
- const insertedValue = renderRichTextAtInsertResult(
1617
+ const insertedValue = renderRichTextTriggerInsertResult(
1284
1618
  match.providerId,
1285
1619
  match.insertResult
1286
1620
  );
@@ -1323,8 +1657,8 @@ function RichTextAtTextarea({
1323
1657
  closeMenu();
1324
1658
  }
1325
1659
  };
1326
- return /* @__PURE__ */ jsxs5("div", { className: cn3("relative min-w-0 w-full", className), children: [
1327
- hasDecorations && textareaPresentationStyle ? /* @__PURE__ */ jsx5(
1660
+ return /* @__PURE__ */ jsxs6("div", { className: cn3("relative min-w-0 w-full", className), children: [
1661
+ hasDecorations && textareaPresentationStyle ? /* @__PURE__ */ jsx6(
1328
1662
  RichTextTextareaDecoratedContent,
1329
1663
  {
1330
1664
  onClickSegment: handleClickDecoration,
@@ -1336,7 +1670,7 @@ function RichTextAtTextarea({
1336
1670
  textareaStyle: textareaPresentationStyle
1337
1671
  }
1338
1672
  ) : null,
1339
- /* @__PURE__ */ jsx5(
1673
+ /* @__PURE__ */ jsx6(
1340
1674
  "textarea",
1341
1675
  {
1342
1676
  ref: textareaRef,
@@ -1407,7 +1741,7 @@ function RichTextAtTextarea({
1407
1741
  }
1408
1742
  ),
1409
1743
  overlay,
1410
- isMenuOpen && menuPoint ? /* @__PURE__ */ jsx5(
1744
+ isMenuOpen && menuPoint ? /* @__PURE__ */ jsx6(
1411
1745
  ViewportMenuSurface2,
1412
1746
  {
1413
1747
  open: true,
@@ -1423,33 +1757,24 @@ function RichTextAtTextarea({
1423
1757
  height: 256
1424
1758
  }
1425
1759
  },
1426
- children: matches.length > 0 ? matches.map((match, index) => /* @__PURE__ */ jsxs5(
1427
- "button",
1760
+ children: matches.length > 0 ? matches.map((match, index) => /* @__PURE__ */ jsx6(
1761
+ RichTextTriggerMenuItem,
1428
1762
  {
1429
- "aria-selected": index === activeIndex,
1430
- className: cn3(
1431
- "flex w-full cursor-pointer flex-col items-start gap-0.5 rounded-md px-2.5 py-2 text-left outline-none transition-colors",
1432
- index === activeIndex ? "bg-transparency-block text-[var(--text-primary)]" : "text-[var(--text-primary)] hover:bg-transparency-block"
1433
- ),
1434
- type: "button",
1435
- onMouseDown: (event) => {
1436
- event.preventDefault();
1437
- applyMatch(match);
1438
- },
1439
- children: [
1440
- /* @__PURE__ */ jsx5("div", { className: "text-[13px] leading-5 font-medium", children: match.label }),
1441
- match.subtitle ? /* @__PURE__ */ jsx5("div", { className: "text-[11px] leading-4 text-[var(--text-secondary)]", children: match.subtitle }) : null
1442
- ]
1763
+ label: match.label,
1764
+ selected: index === activeIndex,
1765
+ subtitle: match.subtitle,
1766
+ thumbnailUrl: match.thumbnailUrl,
1767
+ onSelect: () => applyMatch(match)
1443
1768
  },
1444
1769
  `${match.providerId}:${match.key}`
1445
- )) : /* @__PURE__ */ jsx5("div", { className: "px-3 py-2 text-[11px] leading-4 text-[var(--text-secondary)]", children: isLoading ? text.loadingLabel : text.noMatchesLabel })
1770
+ )) : /* @__PURE__ */ jsx6("div", { className: "px-3 py-2 text-[11px] leading-4 text-[var(--text-secondary)]", children: isLoading ? text.loadingLabel : text.noMatchesLabel })
1446
1771
  }
1447
1772
  ) : null
1448
1773
  ] });
1449
1774
  }
1450
1775
 
1451
1776
  // src/editor/RichTextMentionReadonly.tsx
1452
- import { jsx as jsx6 } from "react/jsx-runtime";
1777
+ import { jsx as jsx7 } from "react/jsx-runtime";
1453
1778
  var baseStyle = {
1454
1779
  alignItems: "center",
1455
1780
  border: "1px solid transparent",
@@ -1526,7 +1851,7 @@ function RichTextMentionReadonly({
1526
1851
  `tutti-rich-text-mention--${view.state}`,
1527
1852
  className
1528
1853
  ),
1529
- "data-plugin": mention.plugin,
1854
+ "data-provider-id": mention.providerId,
1530
1855
  "data-state": view.state,
1531
1856
  style: {
1532
1857
  ...baseStyle,
@@ -1534,21 +1859,8 @@ function RichTextMentionReadonly({
1534
1859
  },
1535
1860
  title: elementTitle
1536
1861
  };
1537
- if (view.interactive && view.href) {
1538
- return /* @__PURE__ */ jsx6(
1539
- "a",
1540
- {
1541
- ...sharedProps,
1542
- href: view.href,
1543
- onClick: (event) => {
1544
- handleClick(event);
1545
- },
1546
- children: label
1547
- }
1548
- );
1549
- }
1550
1862
  if (view.interactive && onClick) {
1551
- return /* @__PURE__ */ jsx6(
1863
+ return /* @__PURE__ */ jsx7(
1552
1864
  "button",
1553
1865
  {
1554
1866
  ...sharedProps,
@@ -1565,7 +1877,7 @@ function RichTextMentionReadonly({
1565
1877
  }
1566
1878
  );
1567
1879
  }
1568
- return /* @__PURE__ */ jsx6("span", { ...sharedProps, children: label });
1880
+ return /* @__PURE__ */ jsx7("span", { ...sharedProps, children: label });
1569
1881
  }
1570
1882
 
1571
1883
  // src/editor/RichTextReadonlyContent.tsx
@@ -1605,13 +1917,14 @@ function buildRichTextReadonlyInlineSegments(content) {
1605
1917
  }
1606
1918
 
1607
1919
  // src/editor/RichTextReadonlyContent.tsx
1608
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1920
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1609
1921
  var externalHrefPattern = /^(?:[a-z]+:)?\/\//i;
1610
1922
  var richTextWorkspaceReferencePillClassName3 = "max-w-[18rem]";
1611
1923
  function RichTextReadonlyContent({
1612
1924
  value,
1613
1925
  className,
1614
1926
  paragraphClassName,
1927
+ onMentionAction,
1615
1928
  onOpenWorkspaceReference
1616
1929
  }) {
1617
1930
  const normalizedValue = normalizeRichTextContent(value).trim();
@@ -1619,29 +1932,34 @@ function RichTextReadonlyContent({
1619
1932
  return null;
1620
1933
  }
1621
1934
  const paragraphs = normalizedValue.split(/\n{2,}/).map((paragraph) => paragraph.trim()).filter(Boolean);
1622
- return /* @__PURE__ */ jsx7("div", { className: cn4("space-y-4", className), children: paragraphs.map((paragraph, paragraphIndex) => /* @__PURE__ */ jsx7(
1935
+ return /* @__PURE__ */ jsx8("div", { className: cn4("space-y-4", className), children: paragraphs.map((paragraph, paragraphIndex) => /* @__PURE__ */ jsx8(
1623
1936
  "p",
1624
1937
  {
1625
1938
  className: cn4("whitespace-pre-wrap", paragraphClassName),
1626
- children: renderReadonlyInlineMarkdown(paragraph, onOpenWorkspaceReference)
1939
+ children: renderReadonlyInlineMarkdown(
1940
+ paragraph,
1941
+ onOpenWorkspaceReference,
1942
+ onMentionAction
1943
+ )
1627
1944
  },
1628
1945
  `${paragraphIndex}:${paragraph}`
1629
1946
  )) });
1630
1947
  }
1631
- function renderReadonlyInlineMarkdown(content, onOpenWorkspaceReference) {
1948
+ function renderReadonlyInlineMarkdown(content, onOpenWorkspaceReference, onMentionAction) {
1632
1949
  const parts = [];
1633
1950
  const segments = buildRichTextReadonlyInlineSegments(content);
1634
1951
  segments.forEach((segment, index) => {
1635
1952
  if (segment.type === "text") {
1636
- parts.push(/* @__PURE__ */ jsx7("span", { children: segment.text }, `text:${index}`));
1953
+ parts.push(/* @__PURE__ */ jsx8("span", { children: segment.text }, `text:${index}`));
1637
1954
  return;
1638
1955
  }
1639
1956
  parts.push(
1640
- /* @__PURE__ */ jsx7(
1957
+ /* @__PURE__ */ jsx8(
1641
1958
  RichTextReadonlyInlineLink,
1642
1959
  {
1643
1960
  href: segment.href,
1644
1961
  label: segment.label,
1962
+ onMentionAction,
1645
1963
  onOpenWorkspaceReference
1646
1964
  },
1647
1965
  `link:${index}:${segment.href}`
@@ -1653,18 +1971,27 @@ function renderReadonlyInlineMarkdown(content, onOpenWorkspaceReference) {
1653
1971
  function RichTextReadonlyInlineLink({
1654
1972
  href,
1655
1973
  label,
1974
+ onMentionAction,
1656
1975
  onOpenWorkspaceReference
1657
1976
  }) {
1658
1977
  const trimmedHref = href.trim();
1659
1978
  const mention = parseRichTextMentionHref(trimmedHref, label);
1660
1979
  if (mention) {
1661
- return /* @__PURE__ */ jsx7(RichTextMentionReadonly, { mention });
1980
+ return /* @__PURE__ */ jsx8(
1981
+ RichTextMentionReadonly,
1982
+ {
1983
+ mention,
1984
+ onClick: onMentionAction ? ({ mention: mention2 }) => {
1985
+ void onMentionAction(mention2);
1986
+ } : void 0
1987
+ }
1988
+ );
1662
1989
  }
1663
1990
  if (!trimmedHref) {
1664
- return /* @__PURE__ */ jsx7("span", { children: label });
1991
+ return /* @__PURE__ */ jsx8("span", { children: label });
1665
1992
  }
1666
1993
  if (externalHrefPattern.test(trimmedHref) && !isRichTextMentionHref(trimmedHref)) {
1667
- return /* @__PURE__ */ jsx7(
1994
+ return /* @__PURE__ */ jsx8(
1668
1995
  "a",
1669
1996
  {
1670
1997
  className: "font-medium text-[var(--text-primary)] underline decoration-[var(--border-1)] underline-offset-4 hover:text-[var(--text-primary-hover)]",
@@ -1677,7 +2004,7 @@ function RichTextReadonlyInlineLink({
1677
2004
  }
1678
2005
  const kind = trimmedHref.endsWith("/") ? "folder" : "file";
1679
2006
  const path = normalizeRichTextLinkHref(trimmedHref, kind);
1680
- return /* @__PURE__ */ jsx7(
2007
+ return /* @__PURE__ */ jsx8(
1681
2008
  WorkspaceReferenceReadonly,
1682
2009
  {
1683
2010
  reference: {
@@ -1697,7 +2024,7 @@ function WorkspaceReferenceReadonly({
1697
2024
  reference.label,
1698
2025
  reference.path
1699
2026
  );
1700
- const content = /* @__PURE__ */ jsx7(
2027
+ const content = /* @__PURE__ */ jsx8(
1701
2028
  MentionPill3,
1702
2029
  {
1703
2030
  className: richTextWorkspaceReferencePillClassName3,
@@ -1707,8 +2034,8 @@ function WorkspaceReferenceReadonly({
1707
2034
  removable: false
1708
2035
  }
1709
2036
  );
1710
- return /* @__PURE__ */ jsxs6(Tooltip2, { children: [
1711
- /* @__PURE__ */ jsx7(TooltipTrigger2, { asChild: true, children: onOpenWorkspaceReference ? /* @__PURE__ */ jsx7(
2037
+ return /* @__PURE__ */ jsxs7(Tooltip2, { children: [
2038
+ /* @__PURE__ */ jsx8(TooltipTrigger2, { asChild: true, children: onOpenWorkspaceReference ? /* @__PURE__ */ jsx8(
1712
2039
  "button",
1713
2040
  {
1714
2041
  className: "inline-flex max-w-full appearance-none items-baseline bg-transparent p-0 text-inherit",
@@ -1718,8 +2045,8 @@ function WorkspaceReferenceReadonly({
1718
2045
  },
1719
2046
  children: content
1720
2047
  }
1721
- ) : /* @__PURE__ */ jsx7("span", { className: "inline-flex max-w-full align-baseline", children: content }) }),
1722
- /* @__PURE__ */ jsx7(
2048
+ ) : /* @__PURE__ */ jsx8("span", { className: "inline-flex max-w-full align-baseline", children: content }) }),
2049
+ /* @__PURE__ */ jsx8(
1723
2050
  TooltipContent2,
1724
2051
  {
1725
2052
  className: "max-w-md whitespace-normal break-all",
@@ -1730,9 +2057,12 @@ function WorkspaceReferenceReadonly({
1730
2057
  ] });
1731
2058
  }
1732
2059
  export {
1733
- RichTextAtEditor,
1734
- RichTextAtTextarea,
1735
2060
  RichTextMentionReadonly,
1736
- RichTextReadonlyContent
2061
+ RichTextReadonlyContent,
2062
+ RichTextTriggerEditor,
2063
+ RichTextTriggerTextarea,
2064
+ findRichTextTriggerQuery,
2065
+ isRichTextTriggerPrefixBoundary,
2066
+ queryRichTextTriggerMatches
1737
2067
  };
1738
2068
  //# sourceMappingURL=index.js.map