@relevaince/mentions 0.3.3 → 0.6.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 +160 -73
- package/dist/index.d.mts +55 -9
- package/dist/index.d.ts +55 -9
- package/dist/index.js +644 -86
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +650 -92
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -11,12 +11,28 @@ 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
|
+
- **Streaming support** — stream AI-generated text into the editor with automatic mention parsing
|
|
35
|
+
- **Multi-instance** — unique ARIA IDs per component instance
|
|
20
36
|
|
|
21
37
|
## Install
|
|
22
38
|
|
|
@@ -38,6 +54,7 @@ import { MentionsInput, type MentionProvider } from "@relevaince/mentions";
|
|
|
38
54
|
const workspaceProvider: MentionProvider = {
|
|
39
55
|
trigger: "@",
|
|
40
56
|
name: "Workspaces",
|
|
57
|
+
debounceMs: 200,
|
|
41
58
|
async getRootItems(query) {
|
|
42
59
|
const res = await fetch(`/api/workspaces?q=${query}`);
|
|
43
60
|
return res.json();
|
|
@@ -46,6 +63,10 @@ const workspaceProvider: MentionProvider = {
|
|
|
46
63
|
const res = await fetch(`/api/workspaces/${parent.id}/files?q=${query}`);
|
|
47
64
|
return res.json();
|
|
48
65
|
},
|
|
66
|
+
async getRecentItems() {
|
|
67
|
+
const res = await fetch("/api/workspaces/recent");
|
|
68
|
+
return res.json();
|
|
69
|
+
},
|
|
49
70
|
};
|
|
50
71
|
|
|
51
72
|
function Chat() {
|
|
@@ -58,7 +79,11 @@ function Chat() {
|
|
|
58
79
|
console.log(output.plainText); // "Summarize Marketing"
|
|
59
80
|
}}
|
|
60
81
|
onSubmit={(output) => sendMessage(output)}
|
|
82
|
+
onMentionAdd={(token) => console.log("Added:", token)}
|
|
83
|
+
onMentionRemove={(token) => console.log("Removed:", token)}
|
|
61
84
|
placeholder="Ask anything..."
|
|
85
|
+
minHeight={40}
|
|
86
|
+
maxHeight={200}
|
|
62
87
|
/>
|
|
63
88
|
);
|
|
64
89
|
}
|
|
@@ -81,19 +106,29 @@ type MentionToken = {
|
|
|
81
106
|
|
|
82
107
|
### MentionProvider
|
|
83
108
|
|
|
84
|
-
Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down
|
|
109
|
+
Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down, cross-group search, debouncing, and recent items:
|
|
85
110
|
|
|
86
111
|
```ts
|
|
87
112
|
type MentionProvider = {
|
|
88
|
-
trigger: string;
|
|
89
|
-
name: string;
|
|
113
|
+
trigger: string;
|
|
114
|
+
name: string;
|
|
90
115
|
getRootItems: (query: string) => Promise<MentionItem[]>;
|
|
91
116
|
getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
|
|
92
|
-
searchAll?: (query: string) => Promise<MentionItem[]>;
|
|
117
|
+
searchAll?: (query: string) => Promise<MentionItem[]>;
|
|
118
|
+
debounceMs?: number;
|
|
119
|
+
getRecentItems?: () => Promise<MentionItem[]>;
|
|
93
120
|
};
|
|
94
121
|
```
|
|
95
122
|
|
|
96
|
-
|
|
123
|
+
| Property | Type | Required | Description |
|
|
124
|
+
|----------|------|----------|-------------|
|
|
125
|
+
| `trigger` | `string` | Yes | Character(s) that activate this provider |
|
|
126
|
+
| `name` | `string` | Yes | Human-readable name for ARIA labels |
|
|
127
|
+
| `getRootItems` | `(query) => Promise<MentionItem[]>` | Yes | Top-level suggestions |
|
|
128
|
+
| `getChildren` | `(parent, query) => Promise<MentionItem[]>` | No | Child suggestions for drill-down |
|
|
129
|
+
| `searchAll` | `(query) => Promise<MentionItem[]>` | No | Flat search across all levels |
|
|
130
|
+
| `debounceMs` | `number` | No | Delay before fetching (prevents API floods) |
|
|
131
|
+
| `getRecentItems` | `() => Promise<MentionItem[]>` | No | Recently used items, shown on empty query |
|
|
97
132
|
|
|
98
133
|
### MentionItem
|
|
99
134
|
|
|
@@ -104,15 +139,16 @@ type MentionItem = {
|
|
|
104
139
|
id: string;
|
|
105
140
|
type: string;
|
|
106
141
|
label: string;
|
|
107
|
-
icon?: ReactNode;
|
|
108
|
-
description?: string;
|
|
109
|
-
hasChildren?: boolean;
|
|
142
|
+
icon?: ReactNode;
|
|
143
|
+
description?: string;
|
|
144
|
+
hasChildren?: boolean;
|
|
110
145
|
data?: unknown;
|
|
111
|
-
rootLabel?: string;
|
|
146
|
+
rootLabel?: string;
|
|
147
|
+
group?: string; // group items under section headers
|
|
112
148
|
};
|
|
113
149
|
```
|
|
114
150
|
|
|
115
|
-
Set `
|
|
151
|
+
Set `group` on items to render them under section headers in the dropdown (e.g. "Active", "Pending", "Recent").
|
|
116
152
|
|
|
117
153
|
### MentionsOutput
|
|
118
154
|
|
|
@@ -120,9 +156,9 @@ Structured output returned on every change and submit:
|
|
|
120
156
|
|
|
121
157
|
```ts
|
|
122
158
|
type MentionsOutput = {
|
|
123
|
-
markdown: string;
|
|
124
|
-
tokens: MentionToken[];
|
|
125
|
-
plainText: string;
|
|
159
|
+
markdown: string;
|
|
160
|
+
tokens: MentionToken[];
|
|
161
|
+
plainText: string;
|
|
126
162
|
};
|
|
127
163
|
```
|
|
128
164
|
|
|
@@ -130,22 +166,39 @@ type MentionsOutput = {
|
|
|
130
166
|
|
|
131
167
|
| Prop | Type | Default | Description |
|
|
132
168
|
|------|------|---------|-------------|
|
|
133
|
-
| `value` | `string` | — |
|
|
169
|
+
| `value` | `string` | — | Controlled markdown content (reactive — updates editor on change) |
|
|
134
170
|
| `providers` | `MentionProvider[]` | **required** | Suggestion providers, one per trigger |
|
|
135
171
|
| `onChange` | `(output: MentionsOutput) => void` | — | Called on every content change |
|
|
136
|
-
| `onSubmit` | `(output: MentionsOutput) => void` | — | Called on
|
|
172
|
+
| `onSubmit` | `(output: MentionsOutput) => void` | — | Called on submit shortcut |
|
|
137
173
|
| `placeholder` | `string` | `"Type a message..."` | Placeholder text |
|
|
138
174
|
| `autoFocus` | `boolean` | `false` | Focus editor on mount |
|
|
139
175
|
| `disabled` | `boolean` | `false` | Disable editing |
|
|
140
176
|
| `className` | `string` | — | CSS class on the wrapper |
|
|
141
177
|
| `maxLength` | `number` | — | Max plain text character count |
|
|
178
|
+
| `clearOnSubmit` | `boolean` | `true` | Auto-clear after `onSubmit` |
|
|
179
|
+
| `submitKey` | `"enter" \| "mod+enter" \| "none"` | `"enter"` | Which key combo triggers submit |
|
|
180
|
+
| `minHeight` | `number` | — | Minimum editor height in px |
|
|
181
|
+
| `maxHeight` | `number` | — | Maximum editor height in px (enables scroll) |
|
|
182
|
+
| `onFocus` | `() => void` | — | Called when editor gains focus |
|
|
183
|
+
| `onBlur` | `() => void` | — | Called when editor loses focus |
|
|
184
|
+
| `onMentionAdd` | `(token: MentionToken) => void` | — | Called when a mention is inserted |
|
|
185
|
+
| `onMentionRemove` | `(token: MentionToken) => void` | — | Called when a mention is deleted |
|
|
186
|
+
| `onMentionClick` | `(token: MentionToken, event: MouseEvent) => void` | — | Called when a mention chip is clicked |
|
|
187
|
+
| `onMentionHover` | `(token: MentionToken) => ReactNode` | — | Return content for a hover tooltip |
|
|
142
188
|
| `renderItem` | `(item, depth) => ReactNode` | — | Custom suggestion item renderer |
|
|
143
|
-
| `clearOnSubmit` | `boolean` | `true` | Auto-clear the editor after `onSubmit` fires |
|
|
144
189
|
| `renderChip` | `(token) => ReactNode` | — | Custom inline mention chip renderer |
|
|
190
|
+
| `renderEmpty` | `(query: string) => ReactNode` | — | Custom empty state (no results) |
|
|
191
|
+
| `renderLoading` | `() => ReactNode` | — | Custom loading indicator |
|
|
192
|
+
| `renderGroupHeader` | `(group: string) => ReactNode` | — | Custom section header renderer |
|
|
193
|
+
| `allowTrigger` | `(trigger, { textBefore }) => boolean` | — | Conditionally suppress the dropdown |
|
|
194
|
+
| `validateMention` | `(token) => boolean \| Promise<boolean>` | — | Validate mentions; invalid ones get `data-mention-invalid` |
|
|
195
|
+
| `portalContainer` | `HTMLElement` | — | Render dropdown into a custom DOM node |
|
|
196
|
+
| `streaming` | `boolean` | `false` | Signals the editor is receiving streamed content (suppresses triggers, blocks user input, throttles onChange) |
|
|
197
|
+
| `onStreamingComplete` | `(output: MentionsOutput) => void` | — | Fires once when `streaming` transitions from `true` to `false` with the final output |
|
|
145
198
|
|
|
146
199
|
## Imperative ref API
|
|
147
200
|
|
|
148
|
-
`MentionsInput` supports `forwardRef` for programmatic control
|
|
201
|
+
`MentionsInput` supports `forwardRef` for programmatic control:
|
|
149
202
|
|
|
150
203
|
```tsx
|
|
151
204
|
import { useRef } from "react";
|
|
@@ -158,16 +211,15 @@ function Chat() {
|
|
|
158
211
|
<>
|
|
159
212
|
<MentionsInput ref={ref} providers={providers} />
|
|
160
213
|
|
|
161
|
-
<button onClick={() => ref.current?.clear()}>
|
|
162
|
-
Clear
|
|
163
|
-
</button>
|
|
164
|
-
|
|
214
|
+
<button onClick={() => ref.current?.clear()}>Clear</button>
|
|
165
215
|
<button onClick={() => {
|
|
166
216
|
ref.current?.setContent("Summarize @[NDA](contract:c_1) risks");
|
|
167
217
|
ref.current?.focus();
|
|
168
|
-
}}>
|
|
169
|
-
|
|
170
|
-
|
|
218
|
+
}}>Use Prompt</button>
|
|
219
|
+
<button onClick={() => {
|
|
220
|
+
const output = ref.current?.getOutput();
|
|
221
|
+
console.log(output?.tokens);
|
|
222
|
+
}}>Read Output</button>
|
|
171
223
|
</>
|
|
172
224
|
);
|
|
173
225
|
}
|
|
@@ -179,43 +231,64 @@ function Chat() {
|
|
|
179
231
|
|--------|-----------|-------------|
|
|
180
232
|
| `clear` | `() => void` | Clears all editor content |
|
|
181
233
|
| `setContent` | `(markdown: string) => void` | Replaces content with a markdown string (mention tokens are parsed) |
|
|
234
|
+
| `appendText` | `(text: string) => void` | Appends plain text at the end (no mention parsing — use for plain-text streaming) |
|
|
182
235
|
| `focus` | `() => void` | Focuses the editor and places the cursor at the end |
|
|
236
|
+
| `getOutput` | `() => MentionsOutput \| null` | Reads the current structured output without waiting for onChange |
|
|
183
237
|
|
|
184
|
-
|
|
238
|
+
## Streaming
|
|
185
239
|
|
|
186
|
-
|
|
240
|
+
Stream AI-generated text into the editor while maintaining mention state. Set `streaming={true}` to enter streaming mode: the suggestion dropdown is suppressed, user keyboard/paste input is blocked, and `onChange` is throttled (~150 ms). Call `ref.current.setContent(accumulated)` on each chunk — completed mention tokens are parsed into chips automatically.
|
|
187
241
|
|
|
188
242
|
```tsx
|
|
243
|
+
const ref = useRef<MentionsInputHandle>(null);
|
|
244
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
245
|
+
|
|
246
|
+
async function enhancePrompt() {
|
|
247
|
+
setIsStreaming(true);
|
|
248
|
+
let accumulated = "";
|
|
249
|
+
|
|
250
|
+
const stream = await fetchEnhancedPrompt(currentPrompt);
|
|
251
|
+
for await (const chunk of stream) {
|
|
252
|
+
accumulated += chunk;
|
|
253
|
+
ref.current?.setContent(accumulated);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
setIsStreaming(false);
|
|
257
|
+
}
|
|
258
|
+
|
|
189
259
|
<MentionsInput
|
|
190
|
-
|
|
191
|
-
|
|
260
|
+
ref={ref}
|
|
261
|
+
streaming={isStreaming}
|
|
262
|
+
providers={providers}
|
|
263
|
+
onChange={handleChange}
|
|
264
|
+
onStreamingComplete={(output) => {
|
|
265
|
+
console.log("Final tokens:", output.tokens);
|
|
266
|
+
}}
|
|
192
267
|
/>
|
|
193
268
|
```
|
|
194
269
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
The killer feature. When a `MentionItem` has `hasChildren: true`, selecting it drills into the next level using `provider.getChildren()`:
|
|
270
|
+
**How it works:**
|
|
198
271
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
```
|
|
272
|
+
1. Set `streaming={true}` — the editor enters streaming mode
|
|
273
|
+
2. On each chunk, accumulate the full text and call `ref.current.setContent(accumulated)`
|
|
274
|
+
3. Incomplete mention tokens (e.g. `@[NDA`) render as plain text until the full `@[label](id)` syntax is received, then snap into mention chips
|
|
275
|
+
4. Set `streaming={false}` — the editor exits streaming mode, fires a final `onChange` and `onStreamingComplete`
|
|
204
276
|
|
|
205
|
-
|
|
277
|
+
For plain-text-only streaming (no mention syntax in chunks), use `ref.current.appendText(chunk)` instead for better performance.
|
|
206
278
|
|
|
207
|
-
Keyboard
|
|
279
|
+
## Keyboard shortcuts
|
|
208
280
|
|
|
209
281
|
| Key | Context | Action |
|
|
210
282
|
|-----|---------|--------|
|
|
211
283
|
| `↑` `↓` | Suggestions open | Navigate suggestions |
|
|
212
284
|
| `Enter` | Suggestions open | Select / drill into children |
|
|
285
|
+
| `Tab` | Suggestions open | Select the active suggestion |
|
|
213
286
|
| `→` | Suggestions open | Drill into children (if item has children) |
|
|
214
287
|
| `←` | Nested level | Go back one level |
|
|
215
288
|
| `Backspace` | Nested level, empty search | Go back one level |
|
|
216
289
|
| `Escape` | Suggestions open | Close suggestions |
|
|
217
|
-
| `Enter` | Editor (
|
|
218
|
-
| `Shift+Enter` | Editor | New line |
|
|
290
|
+
| `Enter` | Editor (default `submitKey`) | Submit message |
|
|
291
|
+
| `Shift+Enter` | Editor (default `submitKey`) | New line |
|
|
219
292
|
| `Cmd/Ctrl+Enter` | Editor | Submit message |
|
|
220
293
|
|
|
221
294
|
## Markdown format
|
|
@@ -224,6 +297,7 @@ Mentions serialize to a compact token syntax:
|
|
|
224
297
|
|
|
225
298
|
```
|
|
226
299
|
@[Marketing Workspace](ws_123) summarize the latest files
|
|
300
|
+
@Marketing[Q4 Strategy.pdf](file:file_1) review this document
|
|
227
301
|
```
|
|
228
302
|
|
|
229
303
|
Use the standalone helpers for server-side processing:
|
|
@@ -235,38 +309,38 @@ import {
|
|
|
235
309
|
extractFromMarkdown,
|
|
236
310
|
} from "@relevaince/mentions";
|
|
237
311
|
|
|
238
|
-
// Parse markdown into a Tiptap-compatible JSON document
|
|
239
312
|
const doc = parseFromMarkdown("Check @[NDA](contract:c_44) for risks");
|
|
240
|
-
|
|
241
|
-
// Serialize a Tiptap JSON document back to markdown
|
|
242
313
|
const md = serializeToMarkdown(doc);
|
|
243
|
-
|
|
244
|
-
// Extract tokens and plain text directly from a markdown string
|
|
245
314
|
const { tokens, plainText } = extractFromMarkdown(
|
|
246
315
|
"Summarize @Marketing[Q4 Strategy.pdf](file:file_1) for the team"
|
|
247
316
|
);
|
|
248
|
-
// tokens → [{ id: "file_1", type: "file", label: "Q4 Strategy.pdf" }]
|
|
249
|
-
// plainText → "Summarize Q4 Strategy.pdf for the team"
|
|
250
317
|
```
|
|
251
318
|
|
|
252
319
|
## Styling
|
|
253
320
|
|
|
254
|
-
Zero bundled CSS. Every element exposes `data-*` attributes
|
|
321
|
+
Zero bundled CSS. Every element exposes `data-*` attributes:
|
|
255
322
|
|
|
256
323
|
```css
|
|
257
|
-
/* Mention chips
|
|
258
|
-
[data-mention]
|
|
259
|
-
[data-mention][data-type="workspace"]
|
|
260
|
-
[data-mention][data-type="contract"]
|
|
324
|
+
/* Mention chips */
|
|
325
|
+
[data-mention] { /* base chip */ }
|
|
326
|
+
[data-mention][data-type="workspace"] { /* workspace chip */ }
|
|
327
|
+
[data-mention][data-type="contract"] { /* contract chip */ }
|
|
328
|
+
[data-mention-clickable] { /* clickable chip (when onMentionClick set) */ }
|
|
329
|
+
[data-mention-invalid] { /* invalid/stale mention */ }
|
|
330
|
+
[data-mention-tooltip] { /* hover tooltip container */ }
|
|
261
331
|
|
|
262
332
|
/* Suggestion popover */
|
|
263
|
-
[data-suggestions]
|
|
264
|
-
[data-
|
|
265
|
-
[data-
|
|
266
|
-
[data-suggestion-
|
|
267
|
-
[data-suggestion-
|
|
268
|
-
[data-suggestion-
|
|
269
|
-
[data-suggestion-
|
|
333
|
+
[data-suggestions] { /* popover wrapper */ }
|
|
334
|
+
[data-suggestions-position="above"] { /* when popover flips above */ }
|
|
335
|
+
[data-suggestions-position="below"] { /* when popover is below */ }
|
|
336
|
+
[data-suggestion-item] { /* each item */ }
|
|
337
|
+
[data-suggestion-item-active] { /* highlighted item */ }
|
|
338
|
+
[data-suggestion-group-header] { /* group section header */ }
|
|
339
|
+
[data-suggestion-empty] { /* "no results" state */ }
|
|
340
|
+
[data-suggestion-loading] { /* loading indicator */ }
|
|
341
|
+
[data-suggestion-breadcrumb] { /* breadcrumb bar (nested) */ }
|
|
342
|
+
[data-suggestion-search] { /* search input wrapper */ }
|
|
343
|
+
[data-suggestion-search-input] { /* search input field */ }
|
|
270
344
|
```
|
|
271
345
|
|
|
272
346
|
Example with Tailwind:
|
|
@@ -288,6 +362,18 @@ Example with Tailwind:
|
|
|
288
362
|
[data-suggestion-item-active] {
|
|
289
363
|
@apply bg-neutral-100;
|
|
290
364
|
}
|
|
365
|
+
|
|
366
|
+
[data-suggestion-group-header] {
|
|
367
|
+
@apply px-3 py-1 text-[10px] font-semibold text-neutral-400 uppercase tracking-wider;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
[data-suggestion-empty] {
|
|
371
|
+
@apply px-3 py-2 text-xs text-neutral-400 italic;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
[data-mention-invalid] {
|
|
375
|
+
@apply opacity-50 line-through;
|
|
376
|
+
}
|
|
291
377
|
```
|
|
292
378
|
|
|
293
379
|
## Architecture
|
|
@@ -295,30 +381,31 @@ Example with Tailwind:
|
|
|
295
381
|
```
|
|
296
382
|
<MentionsInput>
|
|
297
383
|
├── Tiptap Editor (ProseMirror)
|
|
298
|
-
│ ├── MentionNode Extension — inline atom nodes with id/label/type
|
|
299
|
-
│ ├── Suggestion Plugin — multi-trigger detection +
|
|
300
|
-
│ ├── Enter/Submit Extension —
|
|
384
|
+
│ ├── MentionNode Extension — inline atom nodes with id/label/type + click/hover
|
|
385
|
+
│ ├── Suggestion Plugin — multi-trigger detection + allowTrigger gating
|
|
386
|
+
│ ├── Enter/Submit Extension — configurable submit key (Enter/Mod+Enter/none)
|
|
387
|
+
│ ├── Mention Remove Detector — transaction-based onMentionRemove
|
|
301
388
|
│ └── Markdown Parser/Serializer — doc ↔ @[label](id) + extractFromMarkdown
|
|
302
|
-
└── SuggestionList (React) — headless popover with ARIA
|
|
389
|
+
└── SuggestionList (React) — headless popover with ARIA, groups, edge-aware positioning
|
|
303
390
|
```
|
|
304
391
|
|
|
305
|
-
|
|
392
|
+
## Testing
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
npm test # run all tests
|
|
396
|
+
npm run test:watch # watch mode
|
|
397
|
+
```
|
|
306
398
|
|
|
307
399
|
## Development
|
|
308
400
|
|
|
309
401
|
```bash
|
|
310
|
-
#
|
|
311
|
-
npm
|
|
312
|
-
|
|
313
|
-
# Build the library
|
|
314
|
-
npm run build
|
|
402
|
+
npm install # install dependencies
|
|
403
|
+
npm run build # build the library
|
|
404
|
+
npm test # run tests
|
|
315
405
|
|
|
316
|
-
#
|
|
317
|
-
cd demo && npm install && npx vite
|
|
406
|
+
cd demo && npm install && npx vite # run the demo app
|
|
318
407
|
```
|
|
319
408
|
|
|
320
|
-
The demo app registers mock providers for workspaces, contracts, and web search. It displays live structured output below the editor.
|
|
321
|
-
|
|
322
409
|
## License
|
|
323
410
|
|
|
324
411
|
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,49 @@ 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;
|
|
153
|
+
/**
|
|
154
|
+
* Signals the editor is receiving streamed content (e.g. from an AI).
|
|
155
|
+
* When `true`: suggestion triggers are suppressed, user keyboard/paste
|
|
156
|
+
* input is blocked, and `onChange` is throttled (~150 ms).
|
|
157
|
+
*/
|
|
158
|
+
streaming?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Fires once when `streaming` transitions from `true` to `false`
|
|
161
|
+
* with the final structured output.
|
|
162
|
+
*/
|
|
163
|
+
onStreamingComplete?: (output: MentionsOutput) => void;
|
|
115
164
|
};
|
|
116
165
|
/**
|
|
117
166
|
* Imperative handle exposed via `ref` on `<MentionsInput>`.
|
|
@@ -123,15 +172,20 @@ type MentionsInputProps = {
|
|
|
123
172
|
* ref.current.clear();
|
|
124
173
|
* ref.current.setContent("Hello @[Marketing](ws_123)");
|
|
125
174
|
* ref.current.focus();
|
|
175
|
+
* ref.current.getOutput(); // { markdown, tokens, plainText }
|
|
126
176
|
* ```
|
|
127
177
|
*/
|
|
128
178
|
type MentionsInputHandle = {
|
|
129
179
|
/** Clear all editor content. */
|
|
130
180
|
clear: () => void;
|
|
131
|
-
/** Replace editor content with a markdown string. */
|
|
181
|
+
/** Replace editor content with a markdown string. Mention tokens are parsed. */
|
|
132
182
|
setContent: (markdown: string) => void;
|
|
183
|
+
/** Append plain text at the end of the document (no mention parsing). */
|
|
184
|
+
appendText: (text: string) => void;
|
|
133
185
|
/** Focus the editor. */
|
|
134
186
|
focus: () => void;
|
|
187
|
+
/** Read the current structured output without waiting for onChange. */
|
|
188
|
+
getOutput: () => MentionsOutput | null;
|
|
135
189
|
};
|
|
136
190
|
|
|
137
191
|
/**
|
|
@@ -140,14 +194,6 @@ type MentionsInputHandle = {
|
|
|
140
194
|
* A structured text editor with typed entity tokens.
|
|
141
195
|
* Consumers register `providers` for each trigger character,
|
|
142
196
|
* 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
197
|
*/
|
|
152
198
|
declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps & React.RefAttributes<MentionsInputHandle>>;
|
|
153
199
|
|
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,49 @@ 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;
|
|
153
|
+
/**
|
|
154
|
+
* Signals the editor is receiving streamed content (e.g. from an AI).
|
|
155
|
+
* When `true`: suggestion triggers are suppressed, user keyboard/paste
|
|
156
|
+
* input is blocked, and `onChange` is throttled (~150 ms).
|
|
157
|
+
*/
|
|
158
|
+
streaming?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Fires once when `streaming` transitions from `true` to `false`
|
|
161
|
+
* with the final structured output.
|
|
162
|
+
*/
|
|
163
|
+
onStreamingComplete?: (output: MentionsOutput) => void;
|
|
115
164
|
};
|
|
116
165
|
/**
|
|
117
166
|
* Imperative handle exposed via `ref` on `<MentionsInput>`.
|
|
@@ -123,15 +172,20 @@ type MentionsInputProps = {
|
|
|
123
172
|
* ref.current.clear();
|
|
124
173
|
* ref.current.setContent("Hello @[Marketing](ws_123)");
|
|
125
174
|
* ref.current.focus();
|
|
175
|
+
* ref.current.getOutput(); // { markdown, tokens, plainText }
|
|
126
176
|
* ```
|
|
127
177
|
*/
|
|
128
178
|
type MentionsInputHandle = {
|
|
129
179
|
/** Clear all editor content. */
|
|
130
180
|
clear: () => void;
|
|
131
|
-
/** Replace editor content with a markdown string. */
|
|
181
|
+
/** Replace editor content with a markdown string. Mention tokens are parsed. */
|
|
132
182
|
setContent: (markdown: string) => void;
|
|
183
|
+
/** Append plain text at the end of the document (no mention parsing). */
|
|
184
|
+
appendText: (text: string) => void;
|
|
133
185
|
/** Focus the editor. */
|
|
134
186
|
focus: () => void;
|
|
187
|
+
/** Read the current structured output without waiting for onChange. */
|
|
188
|
+
getOutput: () => MentionsOutput | null;
|
|
135
189
|
};
|
|
136
190
|
|
|
137
191
|
/**
|
|
@@ -140,14 +194,6 @@ type MentionsInputHandle = {
|
|
|
140
194
|
* A structured text editor with typed entity tokens.
|
|
141
195
|
* Consumers register `providers` for each trigger character,
|
|
142
196
|
* 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
197
|
*/
|
|
152
198
|
declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps & React.RefAttributes<MentionsInputHandle>>;
|
|
153
199
|
|