testomatio-editor-blocks 0.4.77 → 0.4.78

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.
@@ -0,0 +1,38 @@
1
+ import type { ReactNode } from "react";
2
+ import { type MentionSource } from "./mentionAutocomplete";
3
+ export type UseMentionAutocompleteOptions = {
4
+ /** The textarea the mentions are typed into (e.g. an OverType instance's textarea). */
5
+ textarea: HTMLTextAreaElement | null;
6
+ /** Reads the current full text. Defaults to `textarea.value`. */
7
+ getText?: () => string;
8
+ /**
9
+ * Writes the full text back and positions the caret. Defaults to mutating
10
+ * `textarea.value` directly; pass a custom writer when a wrapper (OverType,
11
+ * a controlled component) owns the value.
12
+ */
13
+ setText?: (text: string, caret: number) => void;
14
+ /** Sources to use. Falls back to the global registry (`setMentionSources`). */
15
+ sources?: MentionSource[];
16
+ /** Master switch; when false the popup never opens. Default true. */
17
+ enabled?: boolean;
18
+ /**
19
+ * When true (default) the hook attaches its own capture-phase keydown listener
20
+ * to the textarea. Set false to drive navigation yourself via the returned
21
+ * `onKeyDown` (useful when another handler must take priority ordering).
22
+ */
23
+ attachKeyDown?: boolean;
24
+ /** Debounce (ms) before an async `search` runs. Default 150. */
25
+ searchDebounceMs?: number;
26
+ };
27
+ export type UseMentionAutocompleteResult = {
28
+ /** Render this somewhere in the tree; it portals the popup to `document.body`. */
29
+ overlay: ReactNode;
30
+ /**
31
+ * Feed keydown events here when `attachKeyDown` is false. Returns true when the
32
+ * event was consumed (it also calls preventDefault / stopImmediatePropagation).
33
+ */
34
+ onKeyDown: (event: KeyboardEvent) => boolean;
35
+ isOpen: boolean;
36
+ close: () => void;
37
+ };
38
+ export declare function useMentionAutocomplete(options: UseMentionAutocompleteOptions): UseMentionAutocompleteResult;
@@ -0,0 +1,312 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* eslint-disable react-refresh/only-export-components -- the hook colocates
3
+ with its private popup component; they are one cohesive module. */
4
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
5
+ import { createPortal } from "react-dom";
6
+ import { applyMention, buildMentionInsertText, filterMentionItems, getMentionSources, normalizeMentionItems, parseActiveMention, } from "./mentionAutocomplete";
7
+ const NAV_KEYS = new Set(["ArrowDown", "ArrowUp", "Enter", "Tab", "Escape"]);
8
+ export function useMentionAutocomplete(options) {
9
+ const { textarea, sources: sourcesProp, enabled = true, attachKeyDown = true, searchDebounceMs = 150, } = options;
10
+ const getText = useCallback(() => { var _a; return (options.getText ? options.getText() : (_a = textarea === null || textarea === void 0 ? void 0 : textarea.value) !== null && _a !== void 0 ? _a : ""); }, [options, textarea]);
11
+ const setText = useCallback((text, caret) => {
12
+ if (options.setText) {
13
+ options.setText(text, caret);
14
+ return;
15
+ }
16
+ if (!textarea)
17
+ return;
18
+ textarea.value = text;
19
+ textarea.setSelectionRange(caret, caret);
20
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
21
+ }, [options, textarea]);
22
+ const [active, setActive] = useState(null);
23
+ const [items, setItems] = useState([]);
24
+ const [activeIndex, setActiveIndex] = useState(0);
25
+ const [loading, setLoading] = useState(false);
26
+ const [caretRect, setCaretRect] = useState(null);
27
+ // Latest committed active mention, for cheap change-detection in refresh().
28
+ const activeRef = useRef(null);
29
+ // Cache resolved static item lists per source so provider functions run once.
30
+ const staticCacheRef = useRef(new WeakMap());
31
+ // Cache async search results per `${prefix}::${query}`.
32
+ const searchCacheRef = useRef(new Map());
33
+ const sources = sourcesProp !== null && sourcesProp !== void 0 ? sourcesProp : getMentionSources();
34
+ const isOpen = enabled && active !== null;
35
+ const close = useCallback(() => {
36
+ activeRef.current = null;
37
+ setActive(null);
38
+ setItems([]);
39
+ setActiveIndex(0);
40
+ setLoading(false);
41
+ }, []);
42
+ /** Re-read the caret and (re)detect the active mention, ignoring no-op changes. */
43
+ const refresh = useCallback(() => {
44
+ var _a;
45
+ let next = null;
46
+ if (enabled && textarea && textarea.selectionStart === textarea.selectionEnd) {
47
+ next = parseActiveMention(getText(), (_a = textarea.selectionStart) !== null && _a !== void 0 ? _a : 0, sources);
48
+ }
49
+ const prev = activeRef.current;
50
+ const unchanged = (!next && !prev) ||
51
+ (!!next &&
52
+ !!prev &&
53
+ next.source === prev.source &&
54
+ next.token === prev.token &&
55
+ next.start === prev.start &&
56
+ next.end === prev.end);
57
+ if (unchanged)
58
+ return;
59
+ activeRef.current = next;
60
+ setActive(next);
61
+ if (next && textarea) {
62
+ setCaretRect(caretCoordinates(textarea, next.start));
63
+ }
64
+ }, [enabled, textarea, getText, sources]);
65
+ // Track input + caret movement.
66
+ useEffect(() => {
67
+ if (!textarea || !enabled)
68
+ return;
69
+ const onInput = () => refresh();
70
+ const onSelectionChange = () => {
71
+ if (document.activeElement === textarea)
72
+ refresh();
73
+ };
74
+ const onBlur = () => close();
75
+ textarea.addEventListener("input", onInput);
76
+ textarea.addEventListener("blur", onBlur);
77
+ document.addEventListener("selectionchange", onSelectionChange);
78
+ return () => {
79
+ textarea.removeEventListener("input", onInput);
80
+ textarea.removeEventListener("blur", onBlur);
81
+ document.removeEventListener("selectionchange", onSelectionChange);
82
+ };
83
+ }, [textarea, enabled, refresh, close]);
84
+ // Resolve items for the active mention (static filter or async search).
85
+ useEffect(() => {
86
+ var _a, _b;
87
+ if (!active)
88
+ return;
89
+ const { source, query } = active;
90
+ setActiveIndex(0);
91
+ const limit = (_a = source.limit) !== null && _a !== void 0 ? _a : 8;
92
+ // Async, server-backed source.
93
+ if (typeof source.search === "function") {
94
+ const minChars = (_b = source.minChars) !== null && _b !== void 0 ? _b : 0;
95
+ if (query.length < minChars) {
96
+ setItems([]);
97
+ setLoading(false);
98
+ return;
99
+ }
100
+ const key = `${source.prefix}::${query}`;
101
+ const cached = searchCacheRef.current.get(key);
102
+ if (cached) {
103
+ setItems(cached);
104
+ setLoading(false);
105
+ return;
106
+ }
107
+ let cancelled = false;
108
+ setLoading(true);
109
+ const timer = setTimeout(() => {
110
+ Promise.resolve(source.search(query))
111
+ .then((result) => normalizeMentionItems(result).slice(0, limit))
112
+ .then((resolved) => {
113
+ searchCacheRef.current.set(key, resolved);
114
+ if (!cancelled) {
115
+ setItems(resolved);
116
+ setLoading(false);
117
+ }
118
+ })
119
+ .catch((error) => {
120
+ console.error("Mention search failed", error);
121
+ if (!cancelled) {
122
+ setItems([]);
123
+ setLoading(false);
124
+ }
125
+ });
126
+ }, searchDebounceMs);
127
+ return () => {
128
+ cancelled = true;
129
+ clearTimeout(timer);
130
+ };
131
+ }
132
+ // Static, in-memory source.
133
+ const resolveStatic = () => {
134
+ const cached = staticCacheRef.current.get(source);
135
+ if (cached)
136
+ return Promise.resolve(cached);
137
+ const input = source.items;
138
+ const raw = typeof input === "function" ? input() : input !== null && input !== void 0 ? input : [];
139
+ return Promise.resolve(raw).then((list) => {
140
+ const normalized = normalizeMentionItems(list);
141
+ staticCacheRef.current.set(source, normalized);
142
+ return normalized;
143
+ });
144
+ };
145
+ let cancelled = false;
146
+ resolveStatic().then((list) => {
147
+ if (!cancelled)
148
+ setItems(filterMentionItems(list, query, limit));
149
+ });
150
+ return () => {
151
+ cancelled = true;
152
+ };
153
+ }, [active, searchDebounceMs]);
154
+ const apply = useCallback((item) => {
155
+ if (!active)
156
+ return;
157
+ const insertText = buildMentionInsertText(active.source, item);
158
+ const result = applyMention(getText(), active, insertText);
159
+ close();
160
+ setText(result.text, result.caret);
161
+ // Return focus to the textarea after the value write settles.
162
+ requestAnimationFrame(() => {
163
+ textarea === null || textarea === void 0 ? void 0 : textarea.focus();
164
+ textarea === null || textarea === void 0 ? void 0 : textarea.setSelectionRange(result.caret, result.caret);
165
+ });
166
+ }, [active, getText, setText, close, textarea]);
167
+ const onKeyDown = useCallback((event) => {
168
+ var _a;
169
+ if (!isOpen || !NAV_KEYS.has(event.key))
170
+ return false;
171
+ if (event.key === "Escape") {
172
+ event.preventDefault();
173
+ event.stopImmediatePropagation();
174
+ close();
175
+ return true;
176
+ }
177
+ if (items.length === 0) {
178
+ // Swallow nav keys while open with no results only for Escape (handled).
179
+ // Let other keys through so typing/navigation still works.
180
+ return false;
181
+ }
182
+ if (event.key === "ArrowDown") {
183
+ event.preventDefault();
184
+ event.stopImmediatePropagation();
185
+ setActiveIndex((prev) => (prev + 1 >= items.length ? 0 : prev + 1));
186
+ return true;
187
+ }
188
+ if (event.key === "ArrowUp") {
189
+ event.preventDefault();
190
+ event.stopImmediatePropagation();
191
+ setActiveIndex((prev) => (prev - 1 < 0 ? items.length - 1 : prev - 1));
192
+ return true;
193
+ }
194
+ if (event.key === "Enter" || event.key === "Tab") {
195
+ event.preventDefault();
196
+ event.stopImmediatePropagation();
197
+ apply((_a = items[activeIndex]) !== null && _a !== void 0 ? _a : items[0]);
198
+ return true;
199
+ }
200
+ return false;
201
+ }, [isOpen, items, activeIndex, apply, close]);
202
+ // Keep a fresh handler for the (optional) self-attached listener.
203
+ const handlerRef = useRef(onKeyDown);
204
+ handlerRef.current = onKeyDown;
205
+ useEffect(() => {
206
+ if (!textarea || !attachKeyDown || !enabled)
207
+ return;
208
+ const listener = (event) => {
209
+ handlerRef.current(event);
210
+ };
211
+ const opts = { capture: true };
212
+ textarea.addEventListener("keydown", listener, opts);
213
+ return () => textarea.removeEventListener("keydown", listener, opts);
214
+ }, [textarea, attachKeyDown, enabled]);
215
+ const overlay = isOpen ? (_jsx(MentionPopup, { active: active, items: items, activeIndex: activeIndex, loading: loading, caretRect: caretRect, onHover: setActiveIndex, onSelect: apply })) : null;
216
+ return { overlay, onKeyDown, isOpen, close };
217
+ }
218
+ function MentionPopup({ active, items, activeIndex, loading, caretRect, onHover, onSelect, }) {
219
+ var _a;
220
+ const listRef = useRef(null);
221
+ // Keep the highlighted item scrolled into view.
222
+ useLayoutEffect(() => {
223
+ var _a;
224
+ const el = (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('[data-active="true"]');
225
+ el === null || el === void 0 ? void 0 : el.scrollIntoView({ block: "nearest" });
226
+ }, [activeIndex, items]);
227
+ if (typeof document === "undefined")
228
+ return null;
229
+ const style = caretRect
230
+ ? { position: "fixed", left: caretRect.left, top: caretRect.bottom + 4 }
231
+ : { position: "fixed", left: 16, top: 16 };
232
+ const source = active.source;
233
+ const minChars = (_a = source.minChars) !== null && _a !== void 0 ? _a : 0;
234
+ const belowMin = typeof source.search === "function" && active.query.length < minChars;
235
+ return createPortal(_jsxs("div", { className: "bn-mention-popup", role: "listbox", style: style, ref: listRef, children: [source.label && (_jsxs("div", { className: "bn-mention-popup__header", children: [_jsxs("span", { className: "bn-mention-popup__prefix", children: ["@", source.prefix] }), _jsx("span", { className: "bn-mention-popup__label", children: source.label })] })), belowMin ? (_jsx("div", { className: "bn-mention-popup__status", children: "Keep typing to search\u2026" })) : loading ? (_jsx("div", { className: "bn-mention-popup__status", children: "Searching\u2026" })) : items.length === 0 ? (_jsx("div", { className: "bn-mention-popup__status", children: "No matches" })) : (items.map((item, index) => (_jsxs("button", { type: "button", role: "option", "data-active": index === activeIndex, "aria-selected": index === activeIndex, className: index === activeIndex
236
+ ? "bn-mention-item bn-mention-item--active"
237
+ : "bn-mention-item",
238
+ // onMouseDown (not onClick) + preventDefault keeps the textarea focused.
239
+ onMouseDown: (event) => {
240
+ event.preventDefault();
241
+ onSelect(item);
242
+ }, onMouseEnter: () => onHover(index), tabIndex: -1, children: [_jsx("span", { className: "bn-mention-item__label", children: item.label }), item.detail && _jsx("span", { className: "bn-mention-item__detail", children: item.detail })] }, item.id))))] }), document.body);
243
+ }
244
+ /* ------------------------------------------------------------------ *
245
+ * Caret coordinates via the mirror-div technique. Works for any plain
246
+ * <textarea>; returns viewport (fixed) coordinates of the given index.
247
+ * ------------------------------------------------------------------ */
248
+ const MIRROR_PROPS = [
249
+ "boxSizing",
250
+ "width",
251
+ "borderTopWidth",
252
+ "borderRightWidth",
253
+ "borderBottomWidth",
254
+ "borderLeftWidth",
255
+ "paddingTop",
256
+ "paddingRight",
257
+ "paddingBottom",
258
+ "paddingLeft",
259
+ "fontStyle",
260
+ "fontVariant",
261
+ "fontWeight",
262
+ "fontStretch",
263
+ "fontSize",
264
+ "fontSizeAdjust",
265
+ "lineHeight",
266
+ "fontFamily",
267
+ "textAlign",
268
+ "textTransform",
269
+ "textIndent",
270
+ "textDecoration",
271
+ "letterSpacing",
272
+ "wordSpacing",
273
+ "tabSize",
274
+ "whiteSpace",
275
+ "wordWrap",
276
+ "wordBreak",
277
+ ];
278
+ function caretCoordinates(textarea, position) {
279
+ const rect = textarea.getBoundingClientRect();
280
+ const fallback = {
281
+ left: rect.left,
282
+ top: rect.top,
283
+ bottom: rect.bottom,
284
+ height: rect.height,
285
+ };
286
+ if (typeof document === "undefined")
287
+ return fallback;
288
+ const style = window.getComputedStyle(textarea);
289
+ const mirror = document.createElement("div");
290
+ const cs = mirror.style;
291
+ cs.position = "absolute";
292
+ cs.visibility = "hidden";
293
+ cs.whiteSpace = "pre-wrap";
294
+ cs.overflow = "hidden";
295
+ const sourceStyle = style;
296
+ const mirrorStyle = cs;
297
+ for (const prop of MIRROR_PROPS) {
298
+ mirrorStyle[prop] = sourceStyle[prop];
299
+ }
300
+ cs.width = `${textarea.clientWidth}px`;
301
+ const value = textarea.value;
302
+ mirror.textContent = value.slice(0, position);
303
+ const marker = document.createElement("span");
304
+ marker.textContent = value.slice(position) || ".";
305
+ mirror.appendChild(marker);
306
+ document.body.appendChild(mirror);
307
+ const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2;
308
+ const left = rect.left + marker.offsetLeft - textarea.scrollLeft;
309
+ const top = rect.top + marker.offsetTop - textarea.scrollTop;
310
+ document.body.removeChild(mirror);
311
+ return { left, top, bottom: top + lineHeight, height: lineHeight };
312
+ }
@@ -0,0 +1,17 @@
1
+ import { type MentionSource } from "./mentionAutocomplete";
2
+ export type MentionMenuProps = {
3
+ /** Sources to use. Falls back to the global registry (`setMentionSources`). */
4
+ sources?: MentionSource[];
5
+ /** Character that opens the menu. Default `@`. */
6
+ triggerCharacter?: string;
7
+ };
8
+ /**
9
+ * Universal `@`-mention menu for the main BlockNote editor. Render it as a child
10
+ * of `<BlockNoteView>`. On selection it inserts the source's token (e.g.
11
+ * `@T123456`, `@S98765`, `@username`) as inline text at the cursor.
12
+ *
13
+ * <BlockNoteView editor={editor}>
14
+ * <MentionMenu />
15
+ * </BlockNoteView>
16
+ */
17
+ export declare function MentionMenu({ sources: sourcesProp, triggerCharacter }: MentionMenuProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { SuggestionMenuController, useBlockNoteEditor } from "@blocknote/react";
3
+ import { buildMentionInsertText, getMentionSources, resolveMentionQuery, } from "./mentionAutocomplete";
4
+ /**
5
+ * Universal `@`-mention menu for the main BlockNote editor. Render it as a child
6
+ * of `<BlockNoteView>`. On selection it inserts the source's token (e.g.
7
+ * `@T123456`, `@S98765`, `@username`) as inline text at the cursor.
8
+ *
9
+ * <BlockNoteView editor={editor}>
10
+ * <MentionMenu />
11
+ * </BlockNoteView>
12
+ */
13
+ export function MentionMenu({ sources: sourcesProp, triggerCharacter = "@" }) {
14
+ const editor = useBlockNoteEditor();
15
+ const getItems = async (query) => {
16
+ const sources = sourcesProp !== null && sourcesProp !== void 0 ? sourcesProp : getMentionSources();
17
+ const resolved = await resolveMentionQuery(query, sources);
18
+ if (!resolved)
19
+ return [];
20
+ const { source, items } = resolved;
21
+ return items.map((item) => {
22
+ var _a;
23
+ return ({
24
+ title: item.label,
25
+ subtext: (_a = item.detail) !== null && _a !== void 0 ? _a : undefined,
26
+ onItemClick: () => {
27
+ // The controller has already removed the trigger + query text; we insert
28
+ // the token plus a trailing space so the caret is ready for more typing.
29
+ editor.insertInlineContent([buildMentionInsertText(source, item), " "]);
30
+ },
31
+ });
32
+ });
33
+ };
34
+ return _jsx(SuggestionMenuController, { triggerCharacter: triggerCharacter, getItems: getItems });
35
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Universal, prefix-based `@`-mention autocomplete.
3
+ *
4
+ * A single mechanism that turns typed tokens like `@`, `@T`, `@S` into inserted
5
+ * references such as `@T123456`, `@S98765`, or `@username`. Which list is shown
6
+ * (and what gets inserted) is decided by a configurable set of {@link MentionSource}s,
7
+ * each keyed by the characters that follow the `@`:
8
+ *
9
+ * { prefix: "T", search: (q) => api.findTests(q) } // @T -> fetch tests -> "@T123456"
10
+ * { prefix: "S", search: (q) => api.findSuites(q) } // @S -> fetch suites -> "@S98765"
11
+ * { prefix: "", items: users, insert: (u) => `@${u.label}` } // @ -> filter users -> "@john"
12
+ *
13
+ * This file is intentionally DOM-free: it holds only the pure logic (matching,
14
+ * source resolution, filtering, token building) plus the global source registry.
15
+ * The React wiring lives in `MentionAutocomplete.tsx`.
16
+ */
17
+ export type MentionItem = {
18
+ /** Stable identifier. For test/suite sources this is what gets inserted (`@T{id}`). */
19
+ id: string;
20
+ /** Primary text shown in the popup and used for client-side filtering. */
21
+ label: string;
22
+ /** Optional secondary text (e.g. `#123`, an email, a description). */
23
+ detail?: string | null;
24
+ /**
25
+ * Optional explicit text to insert for this specific item. When omitted the
26
+ * source's `insert` builder (or the default `@{prefix}{id}`) is used.
27
+ */
28
+ insertText?: string;
29
+ /** Sources may carry arbitrary extra data through to their renderers. */
30
+ [key: string]: unknown;
31
+ };
32
+ /** Static item list, or a (possibly async) provider of one. */
33
+ export type MentionItemsInput = MentionItem[] | (() => MentionItem[] | Promise<MentionItem[]>);
34
+ /** Result of an async `search`. May be raw items or a JSON:API document. */
35
+ export type MentionSearchResult = MentionItem[] | MentionJsonApiDocument | MentionJsonApiResource[] | null | undefined;
36
+ export type MentionSource = {
37
+ /**
38
+ * Characters that must appear immediately after `@` to select this source.
39
+ * Use `""` for the default/fallback source (e.g. usernames on a bare `@`).
40
+ * Matching is case-sensitive and longest-prefix wins, so `@T` routes to the
41
+ * `"T"` source rather than the `""` source.
42
+ */
43
+ prefix: string;
44
+ /** Human label for the popup header / grouping (e.g. "Tests", "Users"). */
45
+ label?: string;
46
+ /**
47
+ * Static list (or provider). Filtered client-side by the typed query. Use
48
+ * this for data already in memory, like the list of project members.
49
+ */
50
+ items?: MentionItemsInput;
51
+ /**
52
+ * Async fetcher invoked with the typed query (the text after the prefix).
53
+ * Use this for server-backed lookups like tests (`@T`) and suites (`@S`).
54
+ */
55
+ search?: (query: string) => Promise<MentionSearchResult> | MentionSearchResult;
56
+ /**
57
+ * Builds the text inserted when an item is chosen. Defaults to
58
+ * `@{prefix}{id}` (so a test with id `123456` becomes `@T123456`).
59
+ */
60
+ insert?: (item: MentionItem, source: MentionSource) => string;
61
+ /** Minimum query length before `search` runs. Default 0 (fires on empty query). */
62
+ minChars?: number;
63
+ /** Max items shown. Default 8. */
64
+ limit?: number;
65
+ };
66
+ /** A currently-active mention detected at the caret. */
67
+ export type ActiveMention = {
68
+ source: MentionSource;
69
+ /** Matched prefix (e.g. "T", "S", or ""). */
70
+ prefix: string;
71
+ /** Text typed after the prefix (what `search`/filter receives). */
72
+ query: string;
73
+ /** Index of the `@` in the source text. */
74
+ start: number;
75
+ /** Caret index (exclusive end of the token being edited). */
76
+ end: number;
77
+ /** Full token text between `@` (exclusive) and the caret, i.e. `prefix + query`. */
78
+ token: string;
79
+ };
80
+ export type MentionJsonApiAttributes = {
81
+ title?: string | null;
82
+ name?: string | null;
83
+ label?: string | null;
84
+ username?: string | null;
85
+ email?: string | null;
86
+ description?: string | null;
87
+ detail?: string | null;
88
+ [key: string]: unknown;
89
+ };
90
+ export type MentionJsonApiResource = {
91
+ id?: string | number | null;
92
+ type?: string | null;
93
+ attributes?: MentionJsonApiAttributes | null;
94
+ };
95
+ export type MentionJsonApiDocument = {
96
+ data?: MentionJsonApiResource[] | null;
97
+ };
98
+ /** Register the mention sources used when a consumer doesn't pass `sources` explicitly. */
99
+ export declare function setMentionSources(sources: MentionSource[] | null): void;
100
+ /** Read the globally-registered mention sources. */
101
+ export declare function getMentionSources(): MentionSource[];
102
+ /**
103
+ * Pick the source whose `prefix` matches the start of `token`. Longest prefix
104
+ * wins; the empty-prefix source is the ultimate fallback. Returns `null` when
105
+ * nothing matches (e.g. `@T` typed but only a `""` source is registered and it
106
+ * is absent).
107
+ */
108
+ export declare function resolveMentionSource(token: string, sources: MentionSource[]): MentionSource | null;
109
+ /**
110
+ * Detect the mention being edited at `caret` within `text`.
111
+ *
112
+ * A mention is the run of non-whitespace characters after an `@` that sits at
113
+ * the start of the text or immediately after whitespace (so emails like
114
+ * `a@b.com` never trigger). Returns `null` when the caret is not inside such a
115
+ * token or no source matches.
116
+ */
117
+ export declare function parseActiveMention(text: string, caret: number, sources: MentionSource[]): ActiveMention | null;
118
+ /** The text a chosen item inserts, honoring item/source overrides. */
119
+ export declare function buildMentionInsertText(source: MentionSource, item: MentionItem): string;
120
+ export type MentionApplication = {
121
+ /** The full text after replacing the active token with the inserted mention. */
122
+ text: string;
123
+ /** Caret position after insertion (after the token and its trailing space). */
124
+ caret: number;
125
+ };
126
+ /**
127
+ * Replace the active mention token in `text` with `insertText`, adding a single
128
+ * trailing space so the caret lands ready for the next word and the freshly
129
+ * inserted `@…` token does not immediately re-trigger the popup.
130
+ */
131
+ export declare function applyMention(text: string, active: Pick<ActiveMention, "start" | "end">, insertText: string): MentionApplication;
132
+ /**
133
+ * Rank static items against a query: label starts-with beats label contains,
134
+ * which beats id matches. Case-insensitive. An empty query returns the head of
135
+ * the list unchanged.
136
+ */
137
+ export declare function filterMentionItems(items: MentionItem[], query: string, limit?: number): MentionItem[];
138
+ /**
139
+ * Route a typed query (everything after the trigger char, e.g. `T123` or `bob`)
140
+ * to its source and resolve the items to show. Handles prefix stripping,
141
+ * `minChars`, async `search`, static `items` (incl. provider functions),
142
+ * normalization, and the `limit`. Returns `null` when no source matches.
143
+ *
144
+ * Shared by both mention frontends (the textarea hook and the BlockNote menu).
145
+ */
146
+ export declare function resolveMentionQuery(query: string, sources: MentionSource[]): Promise<{
147
+ source: MentionSource;
148
+ items: MentionItem[];
149
+ } | null>;
150
+ export declare function parseMentionsFromJsonApi(document: MentionJsonApiDocument | MentionJsonApiResource[] | null | undefined): MentionItem[];
151
+ /** Coerce any supported `search`/`items` result into a clean `MentionItem[]`. */
152
+ export declare function normalizeMentionItems(result: MentionSearchResult): MentionItem[];