@relevaince/mentions 0.3.3 → 0.5.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/README.md CHANGED
@@ -11,12 +11,27 @@ This is **not** a simple `@mention` dropdown. It is a resource-addressing langua
11
11
  - **Cross-group search** — `searchAll` finds items across every level with a single query
12
12
  - **Nested search** — search input inside the dropdown to filter children in real time
13
13
  - **Async providers** — fetch suggestions from any API
14
+ - **Debounced fetching** — per-provider `debounceMs` prevents API floods
14
15
  - **Structured output** — returns `{ markdown, tokens, plainText }` on every change
15
16
  - **Markdown serialization** — `@[label](id)` token syntax for storage and LLM context
16
17
  - **Markdown parsing** — `extractFromMarkdown()` returns tokens + plain text from a markdown string
17
18
  - **Headless styling** — zero bundled CSS, style via `data-*` attributes with Tailwind or plain CSS
18
19
  - **Accessible** — full ARIA combobox pattern with keyboard navigation
19
20
  - **SSR compatible** — safe for Next.js with `"use client"` directive
21
+ - **Grouped suggestions** — section items under headers (e.g. "Active", "Pending")
22
+ - **Recently used items** — show recent mentions when the query is empty
23
+ - **Empty state** — customizable "No results" message
24
+ - **Mention lifecycle** — `onMentionAdd` / `onMentionRemove` callbacks
25
+ - **Mention interaction** — `onMentionClick` / `onMentionHover` handlers on chips
26
+ - **Mention validation** — mark stale/invalid mentions with `validateMention`
27
+ - **Controlled value** — reactive `value` prop for external content updates
28
+ - **Auto-resize** — `minHeight` / `maxHeight` for growing editor
29
+ - **Submit key customization** — choose Enter, Cmd+Enter, or none
30
+ - **Edge-aware popover** — flips above when near viewport bottom
31
+ - **Portal support** — render the dropdown into a custom container
32
+ - **Tab to complete** — Tab selects the active suggestion
33
+ - **Trigger gating** — `allowTrigger` to conditionally suppress the dropdown
34
+ - **Multi-instance** — unique ARIA IDs per component instance
20
35
 
21
36
  ## Install
22
37
 
@@ -38,6 +53,7 @@ import { MentionsInput, type MentionProvider } from "@relevaince/mentions";
38
53
  const workspaceProvider: MentionProvider = {
39
54
  trigger: "@",
40
55
  name: "Workspaces",
56
+ debounceMs: 200,
41
57
  async getRootItems(query) {
42
58
  const res = await fetch(`/api/workspaces?q=${query}`);
43
59
  return res.json();
@@ -46,6 +62,10 @@ const workspaceProvider: MentionProvider = {
46
62
  const res = await fetch(`/api/workspaces/${parent.id}/files?q=${query}`);
47
63
  return res.json();
48
64
  },
65
+ async getRecentItems() {
66
+ const res = await fetch("/api/workspaces/recent");
67
+ return res.json();
68
+ },
49
69
  };
50
70
 
51
71
  function Chat() {
@@ -58,7 +78,11 @@ function Chat() {
58
78
  console.log(output.plainText); // "Summarize Marketing"
59
79
  }}
60
80
  onSubmit={(output) => sendMessage(output)}
81
+ onMentionAdd={(token) => console.log("Added:", token)}
82
+ onMentionRemove={(token) => console.log("Removed:", token)}
61
83
  placeholder="Ask anything..."
84
+ minHeight={40}
85
+ maxHeight={200}
62
86
  />
63
87
  );
64
88
  }
@@ -81,19 +105,29 @@ type MentionToken = {
81
105
 
82
106
  ### MentionProvider
83
107
 
84
- Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down and cross-group search:
108
+ Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down, cross-group search, debouncing, and recent items:
85
109
 
86
110
  ```ts
87
111
  type MentionProvider = {
88
- trigger: string; // "@", "#", ":"
89
- name: string; // human label
112
+ trigger: string;
113
+ name: string;
90
114
  getRootItems: (query: string) => Promise<MentionItem[]>;
91
115
  getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
92
- searchAll?: (query: string) => Promise<MentionItem[]>; // flat search across all levels
116
+ searchAll?: (query: string) => Promise<MentionItem[]>;
117
+ debounceMs?: number;
118
+ getRecentItems?: () => Promise<MentionItem[]>;
93
119
  };
94
120
  ```
95
121
 
96
- When `searchAll` is provided, typing a non-empty query at the root level will call `searchAll` instead of `getRootItems`, returning a flat list of results from every level (root items and their children).
122
+ | Property | Type | Required | Description |
123
+ |----------|------|----------|-------------|
124
+ | `trigger` | `string` | Yes | Character(s) that activate this provider |
125
+ | `name` | `string` | Yes | Human-readable name for ARIA labels |
126
+ | `getRootItems` | `(query) => Promise<MentionItem[]>` | Yes | Top-level suggestions |
127
+ | `getChildren` | `(parent, query) => Promise<MentionItem[]>` | No | Child suggestions for drill-down |
128
+ | `searchAll` | `(query) => Promise<MentionItem[]>` | No | Flat search across all levels |
129
+ | `debounceMs` | `number` | No | Delay before fetching (prevents API floods) |
130
+ | `getRecentItems` | `() => Promise<MentionItem[]>` | No | Recently used items, shown on empty query |
97
131
 
98
132
  ### MentionItem
99
133
 
@@ -104,15 +138,16 @@ type MentionItem = {
104
138
  id: string;
105
139
  type: string;
106
140
  label: string;
107
- icon?: ReactNode; // rendered before the label
108
- description?: string; // secondary text
109
- hasChildren?: boolean; // when true, selecting drills into a child level
141
+ icon?: ReactNode;
142
+ description?: string;
143
+ hasChildren?: boolean;
110
144
  data?: unknown;
111
- rootLabel?: string; // parent label for flat search results (used in serialization)
145
+ rootLabel?: string;
146
+ group?: string; // group items under section headers
112
147
  };
113
148
  ```
114
149
 
115
- Set `rootLabel` on items returned by `searchAll` that originate from a nested level. This ensures correct `@rootLabel[label](id)` serialization.
150
+ Set `group` on items to render them under section headers in the dropdown (e.g. "Active", "Pending", "Recent").
116
151
 
117
152
  ### MentionsOutput
118
153
 
@@ -120,9 +155,9 @@ Structured output returned on every change and submit:
120
155
 
121
156
  ```ts
122
157
  type MentionsOutput = {
123
- markdown: string; // "@[Marketing](ws_123) summarize files"
124
- tokens: MentionToken[]; // [{ id: "ws_123", type: "workspace", label: "Marketing" }]
125
- plainText: string; // "Marketing summarize files"
158
+ markdown: string;
159
+ tokens: MentionToken[];
160
+ plainText: string;
126
161
  };
127
162
  ```
128
163
 
@@ -130,22 +165,37 @@ type MentionsOutput = {
130
165
 
131
166
  | Prop | Type | Default | Description |
132
167
  |------|------|---------|-------------|
133
- | `value` | `string` | — | Initial markdown content with `@[label](id)` tokens |
168
+ | `value` | `string` | — | Controlled markdown content (reactive — updates editor on change) |
134
169
  | `providers` | `MentionProvider[]` | **required** | Suggestion providers, one per trigger |
135
170
  | `onChange` | `(output: MentionsOutput) => void` | — | Called on every content change |
136
- | `onSubmit` | `(output: MentionsOutput) => void` | — | Called on Enter or Cmd+Enter |
171
+ | `onSubmit` | `(output: MentionsOutput) => void` | — | Called on submit shortcut |
137
172
  | `placeholder` | `string` | `"Type a message..."` | Placeholder text |
138
173
  | `autoFocus` | `boolean` | `false` | Focus editor on mount |
139
174
  | `disabled` | `boolean` | `false` | Disable editing |
140
175
  | `className` | `string` | — | CSS class on the wrapper |
141
176
  | `maxLength` | `number` | — | Max plain text character count |
177
+ | `clearOnSubmit` | `boolean` | `true` | Auto-clear after `onSubmit` |
178
+ | `submitKey` | `"enter" \| "mod+enter" \| "none"` | `"enter"` | Which key combo triggers submit |
179
+ | `minHeight` | `number` | — | Minimum editor height in px |
180
+ | `maxHeight` | `number` | — | Maximum editor height in px (enables scroll) |
181
+ | `onFocus` | `() => void` | — | Called when editor gains focus |
182
+ | `onBlur` | `() => void` | — | Called when editor loses focus |
183
+ | `onMentionAdd` | `(token: MentionToken) => void` | — | Called when a mention is inserted |
184
+ | `onMentionRemove` | `(token: MentionToken) => void` | — | Called when a mention is deleted |
185
+ | `onMentionClick` | `(token: MentionToken, event: MouseEvent) => void` | — | Called when a mention chip is clicked |
186
+ | `onMentionHover` | `(token: MentionToken) => ReactNode` | — | Return content for a hover tooltip |
142
187
  | `renderItem` | `(item, depth) => ReactNode` | — | Custom suggestion item renderer |
143
- | `clearOnSubmit` | `boolean` | `true` | Auto-clear the editor after `onSubmit` fires |
144
188
  | `renderChip` | `(token) => ReactNode` | — | Custom inline mention chip renderer |
189
+ | `renderEmpty` | `(query: string) => ReactNode` | — | Custom empty state (no results) |
190
+ | `renderLoading` | `() => ReactNode` | — | Custom loading indicator |
191
+ | `renderGroupHeader` | `(group: string) => ReactNode` | — | Custom section header renderer |
192
+ | `allowTrigger` | `(trigger, { textBefore }) => boolean` | — | Conditionally suppress the dropdown |
193
+ | `validateMention` | `(token) => boolean \| Promise<boolean>` | — | Validate mentions; invalid ones get `data-mention-invalid` |
194
+ | `portalContainer` | `HTMLElement` | — | Render dropdown into a custom DOM node |
145
195
 
146
196
  ## Imperative ref API
147
197
 
148
- `MentionsInput` supports `forwardRef` for programmatic control. This is essential for flows like clearing after submit, injecting prompts from a library, or "Enhance Prompt" features.
198
+ `MentionsInput` supports `forwardRef` for programmatic control:
149
199
 
150
200
  ```tsx
151
201
  import { useRef } from "react";
@@ -158,16 +208,15 @@ function Chat() {
158
208
  <>
159
209
  <MentionsInput ref={ref} providers={providers} />
160
210
 
161
- <button onClick={() => ref.current?.clear()}>
162
- Clear
163
- </button>
164
-
211
+ <button onClick={() => ref.current?.clear()}>Clear</button>
165
212
  <button onClick={() => {
166
213
  ref.current?.setContent("Summarize @[NDA](contract:c_1) risks");
167
214
  ref.current?.focus();
168
- }}>
169
- Use Prompt
170
- </button>
215
+ }}>Use Prompt</button>
216
+ <button onClick={() => {
217
+ const output = ref.current?.getOutput();
218
+ console.log(output?.tokens);
219
+ }}>Read Output</button>
171
220
  </>
172
221
  );
173
222
  }
@@ -180,42 +229,21 @@ function Chat() {
180
229
  | `clear` | `() => void` | Clears all editor content |
181
230
  | `setContent` | `(markdown: string) => void` | Replaces content with a markdown string (mention tokens are parsed) |
182
231
  | `focus` | `() => void` | Focuses the editor and places the cursor at the end |
232
+ | `getOutput` | `() => MentionsOutput \| null` | Reads the current structured output without waiting for onChange |
183
233
 
184
- ### Auto-clear on submit
185
-
186
- By default, the editor clears itself after `onSubmit` fires. Disable with `clearOnSubmit={false}` if you want to manage clearing yourself:
187
-
188
- ```tsx
189
- <MentionsInput
190
- onSubmit={(output) => sendMessage(output)}
191
- clearOnSubmit={false} // don't auto-clear
192
- />
193
- ```
194
-
195
- ## Nested mentions
196
-
197
- The killer feature. When a `MentionItem` has `hasChildren: true`, selecting it drills into the next level using `provider.getChildren()`:
198
-
199
- ```
200
- @workspace → choose workspace → choose file inside workspace
201
- @contract → choose contract → choose clause
202
- :web → choose provider → type query
203
- ```
204
-
205
- When inside a nested level, a search input appears in the dropdown. Type to filter children in real time. Press `Backspace` on an empty search input to go back one level.
206
-
207
- Keyboard navigation:
234
+ ## Keyboard shortcuts
208
235
 
209
236
  | Key | Context | Action |
210
237
  |-----|---------|--------|
211
238
  | `↑` `↓` | Suggestions open | Navigate suggestions |
212
239
  | `Enter` | Suggestions open | Select / drill into children |
240
+ | `Tab` | Suggestions open | Select the active suggestion |
213
241
  | `→` | Suggestions open | Drill into children (if item has children) |
214
242
  | `←` | Nested level | Go back one level |
215
243
  | `Backspace` | Nested level, empty search | Go back one level |
216
244
  | `Escape` | Suggestions open | Close suggestions |
217
- | `Enter` | Editor (no suggestions) | Submit message |
218
- | `Shift+Enter` | Editor | New line |
245
+ | `Enter` | Editor (default `submitKey`) | Submit message |
246
+ | `Shift+Enter` | Editor (default `submitKey`) | New line |
219
247
  | `Cmd/Ctrl+Enter` | Editor | Submit message |
220
248
 
221
249
  ## Markdown format
@@ -224,6 +252,7 @@ Mentions serialize to a compact token syntax:
224
252
 
225
253
  ```
226
254
  @[Marketing Workspace](ws_123) summarize the latest files
255
+ @Marketing[Q4 Strategy.pdf](file:file_1) review this document
227
256
  ```
228
257
 
229
258
  Use the standalone helpers for server-side processing:
@@ -235,38 +264,38 @@ import {
235
264
  extractFromMarkdown,
236
265
  } from "@relevaince/mentions";
237
266
 
238
- // Parse markdown into a Tiptap-compatible JSON document
239
267
  const doc = parseFromMarkdown("Check @[NDA](contract:c_44) for risks");
240
-
241
- // Serialize a Tiptap JSON document back to markdown
242
268
  const md = serializeToMarkdown(doc);
243
-
244
- // Extract tokens and plain text directly from a markdown string
245
269
  const { tokens, plainText } = extractFromMarkdown(
246
270
  "Summarize @Marketing[Q4 Strategy.pdf](file:file_1) for the team"
247
271
  );
248
- // tokens → [{ id: "file_1", type: "file", label: "Q4 Strategy.pdf" }]
249
- // plainText → "Summarize Q4 Strategy.pdf for the team"
250
272
  ```
251
273
 
252
274
  ## Styling
253
275
 
254
- Zero bundled CSS. Every element exposes `data-*` attributes for styling:
276
+ Zero bundled CSS. Every element exposes `data-*` attributes:
255
277
 
256
278
  ```css
257
- /* Mention chips inside the editor */
258
- [data-mention] { /* base chip */ }
259
- [data-mention][data-type="workspace"] { /* workspace chip */ }
260
- [data-mention][data-type="contract"] { /* contract chip */ }
279
+ /* Mention chips */
280
+ [data-mention] { /* base chip */ }
281
+ [data-mention][data-type="workspace"] { /* workspace chip */ }
282
+ [data-mention][data-type="contract"] { /* contract chip */ }
283
+ [data-mention-clickable] { /* clickable chip (when onMentionClick set) */ }
284
+ [data-mention-invalid] { /* invalid/stale mention */ }
285
+ [data-mention-tooltip] { /* hover tooltip container */ }
261
286
 
262
287
  /* Suggestion popover */
263
- [data-suggestions] { /* popover wrapper */ }
264
- [data-suggestion-item] { /* each item */ }
265
- [data-suggestion-item-active] { /* highlighted item */ }
266
- [data-suggestion-breadcrumb] { /* breadcrumb bar (nested) */ }
267
- [data-suggestion-search] { /* search input wrapper (nested) */ }
268
- [data-suggestion-search-input] { /* search input field */ }
269
- [data-suggestion-loading] { /* loading indicator */ }
288
+ [data-suggestions] { /* popover wrapper */ }
289
+ [data-suggestions-position="above"] { /* when popover flips above */ }
290
+ [data-suggestions-position="below"] { /* when popover is below */ }
291
+ [data-suggestion-item] { /* each item */ }
292
+ [data-suggestion-item-active] { /* highlighted item */ }
293
+ [data-suggestion-group-header] { /* group section header */ }
294
+ [data-suggestion-empty] { /* "no results" state */ }
295
+ [data-suggestion-loading] { /* loading indicator */ }
296
+ [data-suggestion-breadcrumb] { /* breadcrumb bar (nested) */ }
297
+ [data-suggestion-search] { /* search input wrapper */ }
298
+ [data-suggestion-search-input] { /* search input field */ }
270
299
  ```
271
300
 
272
301
  Example with Tailwind:
@@ -288,6 +317,18 @@ Example with Tailwind:
288
317
  [data-suggestion-item-active] {
289
318
  @apply bg-neutral-100;
290
319
  }
320
+
321
+ [data-suggestion-group-header] {
322
+ @apply px-3 py-1 text-[10px] font-semibold text-neutral-400 uppercase tracking-wider;
323
+ }
324
+
325
+ [data-suggestion-empty] {
326
+ @apply px-3 py-2 text-xs text-neutral-400 italic;
327
+ }
328
+
329
+ [data-mention-invalid] {
330
+ @apply opacity-50 line-through;
331
+ }
291
332
  ```
292
333
 
293
334
  ## Architecture
@@ -295,30 +336,31 @@ Example with Tailwind:
295
336
  ```
296
337
  <MentionsInput>
297
338
  ├── Tiptap Editor (ProseMirror)
298
- │ ├── MentionNode Extension — inline atom nodes with id/label/type
299
- │ ├── Suggestion Plugin — multi-trigger detection + popover callbacks
300
- │ ├── Enter/Submit Extension — Enter submits, Shift+Enter new line
339
+ │ ├── MentionNode Extension — inline atom nodes with id/label/type + click/hover
340
+ │ ├── Suggestion Plugin — multi-trigger detection + allowTrigger gating
341
+ │ ├── Enter/Submit Extension — configurable submit key (Enter/Mod+Enter/none)
342
+ │ ├── Mention Remove Detector — transaction-based onMentionRemove
301
343
  │ └── Markdown Parser/Serializer — doc ↔ @[label](id) + extractFromMarkdown
302
- └── SuggestionList (React) — headless popover with ARIA + nested search
344
+ └── SuggestionList (React) — headless popover with ARIA, groups, edge-aware positioning
303
345
  ```
304
346
 
305
- Consumers interact with a single React component. Tiptap and ProseMirror are internal implementation details.
347
+ ## Testing
348
+
349
+ ```bash
350
+ npm test # run all tests
351
+ npm run test:watch # watch mode
352
+ ```
306
353
 
307
354
  ## Development
308
355
 
309
356
  ```bash
310
- # Install dependencies
311
- npm install
312
-
313
- # Build the library
314
- npm run build
357
+ npm install # install dependencies
358
+ npm run build # build the library
359
+ npm test # run tests
315
360
 
316
- # Run the demo app
317
- cd demo && npm install && npx vite
361
+ cd demo && npm install && npx vite # run the demo app
318
362
  ```
319
363
 
320
- The demo app registers mock providers for workspaces, contracts, and web search. It displays live structured output below the editor.
321
-
322
364
  ## License
323
365
 
324
366
  MIT
package/dist/index.d.mts CHANGED
@@ -25,6 +25,8 @@ type MentionItem = {
25
25
  * item originates from a nested level.
26
26
  */
27
27
  rootLabel?: string;
28
+ /** Optional group name for sectioned suggestion lists. Items with the same group are grouped together. */
29
+ group?: string;
28
30
  };
29
31
  /**
30
32
  * Hierarchical suggestion source registered per trigger character.
@@ -48,6 +50,10 @@ type MentionProvider = {
48
50
  * results from `searchAll` are shown instead of `getRootItems`.
49
51
  */
50
52
  searchAll?: (query: string) => Promise<MentionItem[]>;
53
+ /** Debounce delay in ms before fetching suggestions. `0` or `undefined` means no debounce. */
54
+ debounceMs?: number;
55
+ /** Fetch recently used items, shown when the query is empty. Items are auto-grouped under "Recent". */
56
+ getRecentItems?: () => Promise<MentionItem[]>;
51
57
  };
52
58
 
53
59
  /**
@@ -112,6 +118,38 @@ type MentionsInputProps = {
112
118
  renderItem?: (item: MentionItem, depth: number) => ReactNode;
113
119
  /** Custom renderer for inline mention chips. */
114
120
  renderChip?: (token: MentionToken) => ReactNode;
121
+ /** Custom renderer for empty suggestion state. Receives the current query. */
122
+ renderEmpty?: (query: string) => ReactNode;
123
+ /** Custom renderer for the suggestion loading indicator. */
124
+ renderLoading?: () => ReactNode;
125
+ /** Custom renderer for suggestion group section headers. */
126
+ renderGroupHeader?: (group: string) => ReactNode;
127
+ /** Called when the editor gains focus. */
128
+ onFocus?: () => void;
129
+ /** Called when the editor loses focus. */
130
+ onBlur?: () => void;
131
+ /** Called when a mention token is inserted. */
132
+ onMentionAdd?: (token: MentionToken) => void;
133
+ /** Called when a mention token is removed. */
134
+ onMentionRemove?: (token: MentionToken) => void;
135
+ /** Called when a mention chip is clicked. */
136
+ onMentionClick?: (token: MentionToken, event: MouseEvent) => void;
137
+ /** Return a ReactNode to show as a tooltip when hovering over a mention chip. */
138
+ onMentionHover?: (token: MentionToken) => ReactNode;
139
+ /** Minimum height of the editor in pixels. */
140
+ minHeight?: number;
141
+ /** Maximum height of the editor in pixels. Enables scrolling. */
142
+ maxHeight?: number;
143
+ /** Which key combo triggers `onSubmit`. Defaults to `"enter"`. */
144
+ submitKey?: "enter" | "mod+enter" | "none";
145
+ /** Conditionally suppress the suggestion dropdown. Return `false` to block. */
146
+ allowTrigger?: (trigger: string, context: {
147
+ textBefore: string;
148
+ }) => boolean;
149
+ /** Validate existing mentions. Return `false` to mark as invalid. */
150
+ validateMention?: (token: MentionToken) => boolean | Promise<boolean>;
151
+ /** DOM element to portal the suggestion dropdown into. */
152
+ portalContainer?: HTMLElement;
115
153
  };
116
154
  /**
117
155
  * Imperative handle exposed via `ref` on `<MentionsInput>`.
@@ -123,6 +161,7 @@ type MentionsInputProps = {
123
161
  * ref.current.clear();
124
162
  * ref.current.setContent("Hello @[Marketing](ws_123)");
125
163
  * ref.current.focus();
164
+ * ref.current.getOutput(); // { markdown, tokens, plainText }
126
165
  * ```
127
166
  */
128
167
  type MentionsInputHandle = {
@@ -132,6 +171,8 @@ type MentionsInputHandle = {
132
171
  setContent: (markdown: string) => void;
133
172
  /** Focus the editor. */
134
173
  focus: () => void;
174
+ /** Read the current structured output without waiting for onChange. */
175
+ getOutput: () => MentionsOutput | null;
135
176
  };
136
177
 
137
178
  /**
@@ -140,14 +181,6 @@ type MentionsInputHandle = {
140
181
  * A structured text editor with typed entity tokens.
141
182
  * Consumers register `providers` for each trigger character,
142
183
  * and receive structured output via `onChange` and `onSubmit`.
143
- *
144
- * Supports an imperative ref handle for programmatic control:
145
- * ```tsx
146
- * const ref = useRef<MentionsInputHandle>(null);
147
- * ref.current.clear();
148
- * ref.current.setContent("@[Marketing](ws_123) summarize");
149
- * ref.current.focus();
150
- * ```
151
184
  */
152
185
  declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps & React.RefAttributes<MentionsInputHandle>>;
153
186
 
package/dist/index.d.ts CHANGED
@@ -25,6 +25,8 @@ type MentionItem = {
25
25
  * item originates from a nested level.
26
26
  */
27
27
  rootLabel?: string;
28
+ /** Optional group name for sectioned suggestion lists. Items with the same group are grouped together. */
29
+ group?: string;
28
30
  };
29
31
  /**
30
32
  * Hierarchical suggestion source registered per trigger character.
@@ -48,6 +50,10 @@ type MentionProvider = {
48
50
  * results from `searchAll` are shown instead of `getRootItems`.
49
51
  */
50
52
  searchAll?: (query: string) => Promise<MentionItem[]>;
53
+ /** Debounce delay in ms before fetching suggestions. `0` or `undefined` means no debounce. */
54
+ debounceMs?: number;
55
+ /** Fetch recently used items, shown when the query is empty. Items are auto-grouped under "Recent". */
56
+ getRecentItems?: () => Promise<MentionItem[]>;
51
57
  };
52
58
 
53
59
  /**
@@ -112,6 +118,38 @@ type MentionsInputProps = {
112
118
  renderItem?: (item: MentionItem, depth: number) => ReactNode;
113
119
  /** Custom renderer for inline mention chips. */
114
120
  renderChip?: (token: MentionToken) => ReactNode;
121
+ /** Custom renderer for empty suggestion state. Receives the current query. */
122
+ renderEmpty?: (query: string) => ReactNode;
123
+ /** Custom renderer for the suggestion loading indicator. */
124
+ renderLoading?: () => ReactNode;
125
+ /** Custom renderer for suggestion group section headers. */
126
+ renderGroupHeader?: (group: string) => ReactNode;
127
+ /** Called when the editor gains focus. */
128
+ onFocus?: () => void;
129
+ /** Called when the editor loses focus. */
130
+ onBlur?: () => void;
131
+ /** Called when a mention token is inserted. */
132
+ onMentionAdd?: (token: MentionToken) => void;
133
+ /** Called when a mention token is removed. */
134
+ onMentionRemove?: (token: MentionToken) => void;
135
+ /** Called when a mention chip is clicked. */
136
+ onMentionClick?: (token: MentionToken, event: MouseEvent) => void;
137
+ /** Return a ReactNode to show as a tooltip when hovering over a mention chip. */
138
+ onMentionHover?: (token: MentionToken) => ReactNode;
139
+ /** Minimum height of the editor in pixels. */
140
+ minHeight?: number;
141
+ /** Maximum height of the editor in pixels. Enables scrolling. */
142
+ maxHeight?: number;
143
+ /** Which key combo triggers `onSubmit`. Defaults to `"enter"`. */
144
+ submitKey?: "enter" | "mod+enter" | "none";
145
+ /** Conditionally suppress the suggestion dropdown. Return `false` to block. */
146
+ allowTrigger?: (trigger: string, context: {
147
+ textBefore: string;
148
+ }) => boolean;
149
+ /** Validate existing mentions. Return `false` to mark as invalid. */
150
+ validateMention?: (token: MentionToken) => boolean | Promise<boolean>;
151
+ /** DOM element to portal the suggestion dropdown into. */
152
+ portalContainer?: HTMLElement;
115
153
  };
116
154
  /**
117
155
  * Imperative handle exposed via `ref` on `<MentionsInput>`.
@@ -123,6 +161,7 @@ type MentionsInputProps = {
123
161
  * ref.current.clear();
124
162
  * ref.current.setContent("Hello @[Marketing](ws_123)");
125
163
  * ref.current.focus();
164
+ * ref.current.getOutput(); // { markdown, tokens, plainText }
126
165
  * ```
127
166
  */
128
167
  type MentionsInputHandle = {
@@ -132,6 +171,8 @@ type MentionsInputHandle = {
132
171
  setContent: (markdown: string) => void;
133
172
  /** Focus the editor. */
134
173
  focus: () => void;
174
+ /** Read the current structured output without waiting for onChange. */
175
+ getOutput: () => MentionsOutput | null;
135
176
  };
136
177
 
137
178
  /**
@@ -140,14 +181,6 @@ type MentionsInputHandle = {
140
181
  * A structured text editor with typed entity tokens.
141
182
  * Consumers register `providers` for each trigger character,
142
183
  * and receive structured output via `onChange` and `onSubmit`.
143
- *
144
- * Supports an imperative ref handle for programmatic control:
145
- * ```tsx
146
- * const ref = useRef<MentionsInputHandle>(null);
147
- * ref.current.clear();
148
- * ref.current.setContent("@[Marketing](ws_123) summarize");
149
- * ref.current.focus();
150
- * ```
151
184
  */
152
185
  declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps & React.RefAttributes<MentionsInputHandle>>;
153
186