@relevaince/mentions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,858 @@
1
+ // src/components/MentionsInput.tsx
2
+ import { useCallback as useCallback3 } from "react";
3
+ import { EditorContent } from "@tiptap/react";
4
+
5
+ // src/hooks/useMentionsEditor.ts
6
+ import { useCallback, useEffect, useMemo, useRef } from "react";
7
+ import { useEditor } from "@tiptap/react";
8
+ import StarterKit from "@tiptap/starter-kit";
9
+ import { Extension as Extension2 } from "@tiptap/core";
10
+
11
+ // src/core/mentionExtension.ts
12
+ import { mergeAttributes, Node } from "@tiptap/core";
13
+ var DEFAULT_PREFIXES = {
14
+ workspace: "@",
15
+ contract: "@",
16
+ file: "#",
17
+ web: ":"
18
+ };
19
+ var MentionNode = Node.create({
20
+ name: "mention",
21
+ group: "inline",
22
+ inline: true,
23
+ atom: true,
24
+ selectable: true,
25
+ draggable: false,
26
+ addAttributes() {
27
+ return {
28
+ id: {
29
+ default: null,
30
+ parseHTML: (element) => element.getAttribute("data-id"),
31
+ renderHTML: (attributes) => ({ "data-id": attributes.id })
32
+ },
33
+ label: {
34
+ default: null,
35
+ parseHTML: (element) => element.getAttribute("data-label"),
36
+ renderHTML: (attributes) => ({ "data-label": attributes.label })
37
+ },
38
+ entityType: {
39
+ default: null,
40
+ parseHTML: (element) => element.getAttribute("data-type"),
41
+ renderHTML: (attributes) => ({ "data-type": attributes.entityType })
42
+ }
43
+ };
44
+ },
45
+ parseHTML() {
46
+ return [{ tag: "span[data-mention]" }];
47
+ },
48
+ renderHTML({ node, HTMLAttributes }) {
49
+ const entityType = node.attrs.entityType;
50
+ const label = node.attrs.label;
51
+ const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
52
+ return [
53
+ "span",
54
+ mergeAttributes(HTMLAttributes, {
55
+ "data-mention": "",
56
+ class: "mention-chip"
57
+ }),
58
+ `${prefix}${label}`
59
+ ];
60
+ },
61
+ renderText({ node }) {
62
+ const entityType = node.attrs.entityType;
63
+ const label = node.attrs.label;
64
+ const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
65
+ return `${prefix}${label}`;
66
+ },
67
+ addKeyboardShortcuts() {
68
+ return {
69
+ Backspace: () => this.editor.commands.command(({ tr, state }) => {
70
+ let isMention = false;
71
+ const { selection } = state;
72
+ const { empty, anchor } = selection;
73
+ if (!empty) return false;
74
+ state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
75
+ if (node.type.name === this.name) {
76
+ isMention = true;
77
+ tr.insertText("", pos, pos + node.nodeSize);
78
+ }
79
+ });
80
+ return isMention;
81
+ })
82
+ };
83
+ }
84
+ });
85
+
86
+ // src/core/suggestionPlugin.ts
87
+ import { Extension } from "@tiptap/core";
88
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
89
+ function detectTrigger(text, cursorPos, docStartPos, triggers) {
90
+ const relCursor = cursorPos - docStartPos;
91
+ const before = text.slice(0, relCursor);
92
+ for (let i = before.length - 1; i >= 0; i--) {
93
+ const ch = before[i];
94
+ if (ch === "\n") return null;
95
+ for (const trigger of triggers) {
96
+ if (before.substring(i, i + trigger.length) === trigger) {
97
+ if (i === 0 || /\s/.test(before[i - 1])) {
98
+ const query = before.slice(i + trigger.length);
99
+ return { trigger, query, from: docStartPos + i, to: cursorPos };
100
+ }
101
+ }
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ var suggestionPluginKey = new PluginKey("mentionSuggestion");
107
+ function createSuggestionExtension(triggers, callbacksRef) {
108
+ return Extension.create({
109
+ name: "mentionSuggestion",
110
+ addProseMirrorPlugins() {
111
+ const editor = this.editor;
112
+ let active = false;
113
+ let lastQuery = null;
114
+ let lastTrigger = null;
115
+ const getClientRect = (view, from) => {
116
+ return () => {
117
+ try {
118
+ const coords = view.coordsAtPos(from);
119
+ return new DOMRect(
120
+ coords.left,
121
+ coords.top,
122
+ 0,
123
+ coords.bottom - coords.top
124
+ );
125
+ } catch {
126
+ return null;
127
+ }
128
+ };
129
+ };
130
+ const makeCommand = (range) => {
131
+ return (attrs) => {
132
+ editor.chain().focus().insertContentAt(range, [
133
+ {
134
+ type: "mention",
135
+ attrs: {
136
+ id: attrs.id,
137
+ label: attrs.label,
138
+ entityType: attrs.entityType
139
+ }
140
+ },
141
+ { type: "text", text: " " }
142
+ ]).run();
143
+ };
144
+ };
145
+ const plugin = new Plugin({
146
+ key: suggestionPluginKey,
147
+ props: {
148
+ handleKeyDown(_view, event) {
149
+ if (!active) return false;
150
+ return callbacksRef.current.onKeyDown({ event });
151
+ }
152
+ },
153
+ view() {
154
+ return {
155
+ update(view, _prevState) {
156
+ const { state } = view;
157
+ const { selection } = state;
158
+ if (!selection.empty) {
159
+ if (active) {
160
+ active = false;
161
+ lastQuery = null;
162
+ lastTrigger = null;
163
+ callbacksRef.current.onExit();
164
+ }
165
+ return;
166
+ }
167
+ const $pos = selection.$from;
168
+ const textBlock = $pos.parent;
169
+ if (!textBlock.isTextblock) {
170
+ if (active) {
171
+ active = false;
172
+ lastQuery = null;
173
+ lastTrigger = null;
174
+ callbacksRef.current.onExit();
175
+ }
176
+ return;
177
+ }
178
+ const blockStart = $pos.start();
179
+ const blockText = textBlock.textContent;
180
+ const cursorPos = $pos.pos;
181
+ const match = detectTrigger(blockText, cursorPos, blockStart, triggers);
182
+ if (match) {
183
+ const range = { from: match.from, to: match.to };
184
+ const props = {
185
+ query: match.query,
186
+ trigger: match.trigger,
187
+ clientRect: getClientRect(view, match.from),
188
+ range,
189
+ command: makeCommand(range)
190
+ };
191
+ if (!active) {
192
+ active = true;
193
+ lastQuery = match.query;
194
+ lastTrigger = match.trigger;
195
+ callbacksRef.current.onStart(props);
196
+ } else if (match.query !== lastQuery || match.trigger !== lastTrigger) {
197
+ lastQuery = match.query;
198
+ lastTrigger = match.trigger;
199
+ callbacksRef.current.onUpdate(props);
200
+ }
201
+ } else {
202
+ if (active) {
203
+ active = false;
204
+ lastQuery = null;
205
+ lastTrigger = null;
206
+ callbacksRef.current.onExit();
207
+ }
208
+ }
209
+ },
210
+ destroy() {
211
+ if (active) {
212
+ callbacksRef.current.onExit();
213
+ }
214
+ }
215
+ };
216
+ }
217
+ });
218
+ return [plugin];
219
+ }
220
+ });
221
+ }
222
+
223
+ // src/core/markdownSerializer.ts
224
+ function serializeToMarkdown(doc) {
225
+ if (!doc.content) return "";
226
+ const parts = [];
227
+ for (const block of doc.content) {
228
+ if (block.type === "paragraph") {
229
+ parts.push(serializeParagraph(block));
230
+ } else if (block.type === "text") {
231
+ parts.push(block.text ?? "");
232
+ }
233
+ }
234
+ return parts.join("\n");
235
+ }
236
+ function serializeParagraph(node) {
237
+ if (!node.content) return "";
238
+ return node.content.map((child) => {
239
+ if (child.type === "mention") {
240
+ const { id, label } = child.attrs ?? {};
241
+ return `@[${label}](${id})`;
242
+ }
243
+ return child.text ?? "";
244
+ }).join("");
245
+ }
246
+ function extractTokens(doc) {
247
+ const tokens = [];
248
+ function walk(node) {
249
+ if (node.type === "mention" && node.attrs) {
250
+ tokens.push({
251
+ id: node.attrs.id,
252
+ type: node.attrs.entityType,
253
+ label: node.attrs.label
254
+ });
255
+ }
256
+ if (node.content) {
257
+ for (const child of node.content) {
258
+ walk(child);
259
+ }
260
+ }
261
+ }
262
+ walk(doc);
263
+ return tokens;
264
+ }
265
+ function extractPlainText(doc) {
266
+ if (!doc.content) return "";
267
+ const parts = [];
268
+ for (const block of doc.content) {
269
+ if (block.type === "paragraph") {
270
+ parts.push(extractParagraphText(block));
271
+ }
272
+ }
273
+ return parts.join("\n");
274
+ }
275
+ function extractParagraphText(node) {
276
+ if (!node.content) return "";
277
+ return node.content.map((child) => {
278
+ if (child.type === "mention") {
279
+ return child.attrs?.label ?? "";
280
+ }
281
+ return child.text ?? "";
282
+ }).join("");
283
+ }
284
+
285
+ // src/core/markdownParser.ts
286
+ var MENTION_RE = /@\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
287
+ function parseFromMarkdown(markdown) {
288
+ const lines = markdown.split("\n");
289
+ const content = lines.map((line) => ({
290
+ type: "paragraph",
291
+ content: parseLine(line)
292
+ }));
293
+ return { type: "doc", content };
294
+ }
295
+ function parseLine(line) {
296
+ const nodes = [];
297
+ let lastIndex = 0;
298
+ MENTION_RE.lastIndex = 0;
299
+ let match;
300
+ while ((match = MENTION_RE.exec(line)) !== null) {
301
+ const fullMatch = match[0];
302
+ const label = match[1];
303
+ const entityType = match[2] ?? "unknown";
304
+ const id = match[3];
305
+ if (match.index > lastIndex) {
306
+ nodes.push({
307
+ type: "text",
308
+ text: line.slice(lastIndex, match.index)
309
+ });
310
+ }
311
+ nodes.push({
312
+ type: "mention",
313
+ attrs: {
314
+ id,
315
+ label,
316
+ entityType
317
+ }
318
+ });
319
+ lastIndex = match.index + fullMatch.length;
320
+ }
321
+ if (lastIndex < line.length) {
322
+ nodes.push({
323
+ type: "text",
324
+ text: line.slice(lastIndex)
325
+ });
326
+ }
327
+ if (nodes.length === 0) {
328
+ nodes.push({ type: "text", text: "" });
329
+ }
330
+ return nodes;
331
+ }
332
+
333
+ // src/hooks/useMentionsEditor.ts
334
+ function createSubmitExtension(onSubmitRef) {
335
+ return Extension2.create({
336
+ name: "submitShortcut",
337
+ addKeyboardShortcuts() {
338
+ return {
339
+ "Mod-Enter": () => {
340
+ if (onSubmitRef.current) {
341
+ const json = this.editor.getJSON();
342
+ onSubmitRef.current({
343
+ markdown: serializeToMarkdown(json),
344
+ tokens: extractTokens(json),
345
+ plainText: extractPlainText(json)
346
+ });
347
+ }
348
+ return true;
349
+ }
350
+ };
351
+ }
352
+ });
353
+ }
354
+ function createEnterExtension(onSubmitRef) {
355
+ return Extension2.create({
356
+ name: "enterSubmit",
357
+ priority: 50,
358
+ addKeyboardShortcuts() {
359
+ return {
360
+ Enter: () => {
361
+ if (onSubmitRef.current) {
362
+ const json = this.editor.getJSON();
363
+ onSubmitRef.current({
364
+ markdown: serializeToMarkdown(json),
365
+ tokens: extractTokens(json),
366
+ plainText: extractPlainText(json)
367
+ });
368
+ }
369
+ return true;
370
+ }
371
+ };
372
+ }
373
+ });
374
+ }
375
+ function useMentionsEditor({
376
+ providers,
377
+ value,
378
+ onChange,
379
+ onSubmit,
380
+ placeholder,
381
+ autoFocus = false,
382
+ editable = true,
383
+ callbacksRef
384
+ }) {
385
+ const onChangeRef = useRef(onChange);
386
+ onChangeRef.current = onChange;
387
+ const onSubmitRef = useRef(onSubmit);
388
+ onSubmitRef.current = onSubmit;
389
+ const initialContent = useMemo(() => {
390
+ if (!value) return void 0;
391
+ return parseFromMarkdown(value);
392
+ }, []);
393
+ const triggersKey = providers.map((p) => p.trigger).join(",");
394
+ const triggers = useMemo(
395
+ () => providers.map((p) => p.trigger),
396
+ // eslint-disable-next-line react-hooks/exhaustive-deps
397
+ [triggersKey]
398
+ );
399
+ const suggestionExtension = useMemo(
400
+ () => createSuggestionExtension(triggers, callbacksRef),
401
+ // eslint-disable-next-line react-hooks/exhaustive-deps
402
+ [triggersKey]
403
+ );
404
+ const submitExt = useMemo(() => createSubmitExtension(onSubmitRef), []);
405
+ const enterExt = useMemo(() => createEnterExtension(onSubmitRef), []);
406
+ const editor = useEditor({
407
+ extensions: [
408
+ StarterKit.configure({
409
+ heading: false,
410
+ blockquote: false,
411
+ codeBlock: false,
412
+ bulletList: false,
413
+ orderedList: false,
414
+ listItem: false,
415
+ horizontalRule: false
416
+ }),
417
+ MentionNode,
418
+ suggestionExtension,
419
+ submitExt,
420
+ enterExt
421
+ ],
422
+ content: initialContent,
423
+ autofocus: autoFocus ? "end" : false,
424
+ editable,
425
+ editorProps: {
426
+ attributes: {
427
+ "data-placeholder": placeholder ?? "",
428
+ class: "mentions-editor"
429
+ }
430
+ },
431
+ onUpdate: ({ editor: editor2 }) => {
432
+ const json = editor2.getJSON();
433
+ onChangeRef.current?.({
434
+ markdown: serializeToMarkdown(json),
435
+ tokens: extractTokens(json),
436
+ plainText: extractPlainText(json)
437
+ });
438
+ }
439
+ });
440
+ useEffect(() => {
441
+ if (editor && editor.isEditable !== editable) {
442
+ editor.setEditable(editable);
443
+ }
444
+ }, [editor, editable]);
445
+ const getOutput = useCallback(() => {
446
+ if (!editor) return null;
447
+ const json = editor.getJSON();
448
+ return {
449
+ markdown: serializeToMarkdown(json),
450
+ tokens: extractTokens(json),
451
+ plainText: extractPlainText(json)
452
+ };
453
+ }, [editor]);
454
+ return { editor, getOutput };
455
+ }
456
+
457
+ // src/hooks/useSuggestion.ts
458
+ import { useCallback as useCallback2, useRef as useRef2, useState } from "react";
459
+ var IDLE_STATE = {
460
+ state: "idle",
461
+ items: [],
462
+ breadcrumbs: [],
463
+ activeIndex: 0,
464
+ loading: false,
465
+ clientRect: null,
466
+ trigger: null,
467
+ query: ""
468
+ };
469
+ function useSuggestion(providers) {
470
+ const [uiState, setUIState] = useState(IDLE_STATE);
471
+ const stateRef = useRef2(uiState);
472
+ stateRef.current = uiState;
473
+ const providersRef = useRef2(providers);
474
+ providersRef.current = providers;
475
+ const commandRef = useRef2(
476
+ null
477
+ );
478
+ const providerRef = useRef2(null);
479
+ const fetchItems = useCallback2(
480
+ async (provider, query, parent) => {
481
+ setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
482
+ try {
483
+ const items = parent && provider.getChildren ? await provider.getChildren(parent, query) : await provider.getRootItems(query);
484
+ setUIState((prev) => ({
485
+ ...prev,
486
+ items,
487
+ loading: false,
488
+ state: "showing",
489
+ activeIndex: 0
490
+ }));
491
+ } catch {
492
+ setUIState((prev) => ({
493
+ ...prev,
494
+ items: [],
495
+ loading: false,
496
+ state: "showing"
497
+ }));
498
+ }
499
+ },
500
+ []
501
+ );
502
+ const onStart = useCallback2(
503
+ (props) => {
504
+ const provider = providersRef.current.find(
505
+ (p) => p.trigger === props.trigger
506
+ );
507
+ if (!provider) return;
508
+ providerRef.current = provider;
509
+ commandRef.current = props.command;
510
+ setUIState({
511
+ state: "loading",
512
+ items: [],
513
+ breadcrumbs: [],
514
+ activeIndex: 0,
515
+ loading: true,
516
+ clientRect: props.clientRect,
517
+ trigger: props.trigger,
518
+ query: props.query
519
+ });
520
+ fetchItems(provider, props.query);
521
+ },
522
+ [fetchItems]
523
+ );
524
+ const onUpdate = useCallback2(
525
+ (props) => {
526
+ const provider = providerRef.current;
527
+ if (!provider) return;
528
+ commandRef.current = props.command;
529
+ setUIState((prev) => ({
530
+ ...prev,
531
+ clientRect: props.clientRect,
532
+ query: props.query
533
+ }));
534
+ if (stateRef.current.breadcrumbs.length === 0) {
535
+ fetchItems(provider, props.query);
536
+ }
537
+ },
538
+ [fetchItems]
539
+ );
540
+ const onExit = useCallback2(() => {
541
+ providerRef.current = null;
542
+ commandRef.current = null;
543
+ setUIState(IDLE_STATE);
544
+ }, []);
545
+ const navigateUp = useCallback2(() => {
546
+ setUIState((prev) => ({
547
+ ...prev,
548
+ activeIndex: Math.max(0, prev.activeIndex - 1)
549
+ }));
550
+ }, []);
551
+ const navigateDown = useCallback2(() => {
552
+ setUIState((prev) => ({
553
+ ...prev,
554
+ activeIndex: Math.min(prev.items.length - 1, prev.activeIndex + 1)
555
+ }));
556
+ }, []);
557
+ const select = useCallback2(
558
+ (item) => {
559
+ const current = stateRef.current;
560
+ const selected = item ?? current.items[current.activeIndex];
561
+ if (!selected) return;
562
+ const provider = providerRef.current;
563
+ if (selected.hasChildren && provider?.getChildren) {
564
+ setUIState((prev) => ({
565
+ ...prev,
566
+ state: "drilling",
567
+ breadcrumbs: [...prev.breadcrumbs, selected],
568
+ items: [],
569
+ activeIndex: 0,
570
+ query: ""
571
+ }));
572
+ fetchItems(provider, "", selected);
573
+ return;
574
+ }
575
+ if (commandRef.current) {
576
+ commandRef.current({
577
+ id: selected.id,
578
+ label: selected.label,
579
+ entityType: selected.type
580
+ });
581
+ }
582
+ },
583
+ [fetchItems]
584
+ );
585
+ const goBack = useCallback2(() => {
586
+ const provider = providerRef.current;
587
+ if (!provider) return;
588
+ setUIState((prev) => {
589
+ const newBreadcrumbs = prev.breadcrumbs.slice(0, -1);
590
+ const parent = newBreadcrumbs[newBreadcrumbs.length - 1];
591
+ if (parent) {
592
+ fetchItems(provider, "", parent);
593
+ } else {
594
+ fetchItems(provider, prev.query);
595
+ }
596
+ return {
597
+ ...prev,
598
+ breadcrumbs: newBreadcrumbs,
599
+ items: [],
600
+ activeIndex: 0,
601
+ state: "loading"
602
+ };
603
+ });
604
+ }, [fetchItems]);
605
+ const close = useCallback2(() => {
606
+ setUIState(IDLE_STATE);
607
+ }, []);
608
+ const onKeyDown = useCallback2(
609
+ ({ event }) => {
610
+ const current = stateRef.current;
611
+ if (current.state === "idle") return false;
612
+ switch (event.key) {
613
+ case "ArrowUp":
614
+ event.preventDefault();
615
+ navigateUp();
616
+ return true;
617
+ case "ArrowDown":
618
+ event.preventDefault();
619
+ navigateDown();
620
+ return true;
621
+ case "Enter": {
622
+ event.preventDefault();
623
+ const selectedItem = current.items[current.activeIndex];
624
+ if (selectedItem) {
625
+ select(selectedItem);
626
+ }
627
+ return true;
628
+ }
629
+ case "ArrowRight": {
630
+ const activeItem = current.items[current.activeIndex];
631
+ if (activeItem?.hasChildren) {
632
+ event.preventDefault();
633
+ select(activeItem);
634
+ return true;
635
+ }
636
+ return false;
637
+ }
638
+ case "ArrowLeft":
639
+ if (current.breadcrumbs.length > 0) {
640
+ event.preventDefault();
641
+ goBack();
642
+ return true;
643
+ }
644
+ return false;
645
+ case "Escape":
646
+ event.preventDefault();
647
+ close();
648
+ return true;
649
+ default:
650
+ return false;
651
+ }
652
+ },
653
+ [navigateUp, navigateDown, select, goBack, close]
654
+ );
655
+ const callbacksRef = useRef2({
656
+ onStart,
657
+ onUpdate,
658
+ onExit,
659
+ onKeyDown
660
+ });
661
+ callbacksRef.current = { onStart, onUpdate, onExit, onKeyDown };
662
+ const actions = {
663
+ navigateUp,
664
+ navigateDown,
665
+ select,
666
+ goBack,
667
+ close
668
+ };
669
+ return { uiState, actions, callbacksRef };
670
+ }
671
+
672
+ // src/components/SuggestionList.tsx
673
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
674
+
675
+ // src/utils/ariaHelpers.ts
676
+ function comboboxAttrs(expanded, listboxId) {
677
+ return {
678
+ role: "combobox",
679
+ "aria-haspopup": "listbox",
680
+ "aria-expanded": expanded,
681
+ "aria-owns": expanded ? listboxId : void 0
682
+ };
683
+ }
684
+ function listboxAttrs(id, label) {
685
+ return {
686
+ id,
687
+ role: "listbox",
688
+ "aria-label": label
689
+ };
690
+ }
691
+ function optionAttrs(id, selected, index) {
692
+ return {
693
+ id,
694
+ role: "option",
695
+ "aria-selected": selected,
696
+ "aria-posinset": index + 1
697
+ };
698
+ }
699
+
700
+ // src/components/SuggestionList.tsx
701
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
702
+ var LISTBOX_ID = "mentions-suggestion-listbox";
703
+ function SuggestionList({
704
+ items,
705
+ activeIndex,
706
+ breadcrumbs,
707
+ loading,
708
+ trigger,
709
+ clientRect,
710
+ onSelect,
711
+ onHover,
712
+ onGoBack,
713
+ renderItem
714
+ }) {
715
+ const listRef = useRef3(null);
716
+ const depth = breadcrumbs.length;
717
+ useEffect2(() => {
718
+ if (!listRef.current) return;
719
+ const active = listRef.current.querySelector('[aria-selected="true"]');
720
+ active?.scrollIntoView({ block: "nearest" });
721
+ }, [activeIndex]);
722
+ const style = usePopoverPosition(clientRect);
723
+ if (items.length === 0 && !loading) return null;
724
+ return /* @__PURE__ */ jsxs(
725
+ "div",
726
+ {
727
+ "data-suggestions": "",
728
+ "data-trigger": trigger,
729
+ style,
730
+ ref: listRef,
731
+ children: [
732
+ breadcrumbs.length > 0 && /* @__PURE__ */ jsxs("div", { "data-suggestion-breadcrumb": "", children: [
733
+ /* @__PURE__ */ jsx(
734
+ "button",
735
+ {
736
+ type: "button",
737
+ "data-suggestion-back": "",
738
+ onClick: onGoBack,
739
+ "aria-label": "Go back",
740
+ children: "\u2190"
741
+ }
742
+ ),
743
+ breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs("span", { "data-suggestion-breadcrumb-item": "", children: [
744
+ i > 0 && /* @__PURE__ */ jsx("span", { "data-suggestion-breadcrumb-sep": "", children: "/" }),
745
+ crumb.label
746
+ ] }, crumb.id))
747
+ ] }),
748
+ loading && /* @__PURE__ */ jsx("div", { "data-suggestion-loading": "", children: "Loading..." }),
749
+ !loading && /* @__PURE__ */ jsx("div", { ...listboxAttrs(LISTBOX_ID, `${trigger ?? ""} suggestions`), children: items.map((item, index) => {
750
+ const isActive = index === activeIndex;
751
+ const itemId = `mention-option-${item.id}`;
752
+ return /* @__PURE__ */ jsx(
753
+ "div",
754
+ {
755
+ ...optionAttrs(itemId, isActive, index),
756
+ "data-suggestion-item": "",
757
+ "data-suggestion-item-active": isActive ? "" : void 0,
758
+ "data-has-children": item.hasChildren ? "" : void 0,
759
+ onMouseEnter: () => onHover(index),
760
+ onClick: () => onSelect(item),
761
+ children: renderItem ? renderItem(item, depth) : /* @__PURE__ */ jsx(DefaultSuggestionItem, { item })
762
+ },
763
+ item.id
764
+ );
765
+ }) })
766
+ ]
767
+ }
768
+ );
769
+ }
770
+ function DefaultSuggestionItem({ item }) {
771
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
772
+ item.icon && /* @__PURE__ */ jsx("span", { "data-suggestion-item-icon": "", children: item.icon }),
773
+ /* @__PURE__ */ jsx("span", { "data-suggestion-item-label": "", children: item.label }),
774
+ item.description && /* @__PURE__ */ jsx("span", { "data-suggestion-item-description": "", children: item.description }),
775
+ item.hasChildren && /* @__PURE__ */ jsx("span", { "data-suggestion-item-chevron": "", "aria-hidden": "true", children: "\u203A" })
776
+ ] });
777
+ }
778
+ function usePopoverPosition(clientRect) {
779
+ if (!clientRect) {
780
+ return { display: "none" };
781
+ }
782
+ const rect = clientRect();
783
+ if (!rect) {
784
+ return { display: "none" };
785
+ }
786
+ return {
787
+ position: "fixed",
788
+ left: `${rect.left}px`,
789
+ top: `${rect.bottom + 4}px`,
790
+ zIndex: 50
791
+ };
792
+ }
793
+
794
+ // src/components/MentionsInput.tsx
795
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
796
+ var LISTBOX_ID2 = "mentions-suggestion-listbox";
797
+ function MentionsInput({
798
+ value,
799
+ providers,
800
+ onChange,
801
+ placeholder = "Type a message...",
802
+ autoFocus = false,
803
+ disabled = false,
804
+ className,
805
+ onSubmit,
806
+ maxLength,
807
+ renderItem,
808
+ renderChip
809
+ }) {
810
+ const { uiState, actions, callbacksRef } = useSuggestion(providers);
811
+ const { editor } = useMentionsEditor({
812
+ providers,
813
+ value,
814
+ onChange,
815
+ onSubmit,
816
+ placeholder,
817
+ autoFocus,
818
+ editable: !disabled,
819
+ callbacksRef
820
+ });
821
+ const isExpanded = uiState.state !== "idle";
822
+ const handleHover = useCallback3((index) => {
823
+ }, []);
824
+ return /* @__PURE__ */ jsxs2(
825
+ "div",
826
+ {
827
+ className,
828
+ "data-mentions-input": "",
829
+ "data-disabled": disabled ? "" : void 0,
830
+ ...comboboxAttrs(isExpanded, LISTBOX_ID2),
831
+ "aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
832
+ children: [
833
+ /* @__PURE__ */ jsx2(EditorContent, { editor }),
834
+ isExpanded && /* @__PURE__ */ jsx2(
835
+ SuggestionList,
836
+ {
837
+ items: uiState.items,
838
+ activeIndex: uiState.activeIndex,
839
+ breadcrumbs: uiState.breadcrumbs,
840
+ loading: uiState.loading,
841
+ trigger: uiState.trigger,
842
+ clientRect: uiState.clientRect,
843
+ onSelect: (item) => actions.select(item),
844
+ onHover: handleHover,
845
+ onGoBack: actions.goBack,
846
+ renderItem
847
+ }
848
+ )
849
+ ]
850
+ }
851
+ );
852
+ }
853
+ export {
854
+ MentionsInput,
855
+ parseFromMarkdown,
856
+ serializeToMarkdown
857
+ };
858
+ //# sourceMappingURL=index.mjs.map