@relevaince/mentions 0.2.1 → 0.3.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
@@ -8,9 +8,12 @@ This is **not** a simple `@mention` dropdown. It is a resource-addressing langua
8
8
 
9
9
  - **Multiple trigger characters** — `@`, `#`, `:`, or any custom trigger
10
10
  - **Nested suggestions** — drill into workspaces, then pick a file inside
11
+ - **Cross-group search** — `searchAll` finds items across every level with a single query
12
+ - **Nested search** — search input inside the dropdown to filter children in real time
11
13
  - **Async providers** — fetch suggestions from any API
12
14
  - **Structured output** — returns `{ markdown, tokens, plainText }` on every change
13
15
  - **Markdown serialization** — `@[label](id)` token syntax for storage and LLM context
16
+ - **Markdown parsing** — `extractFromMarkdown()` returns tokens + plain text from a markdown string
14
17
  - **Headless styling** — zero bundled CSS, style via `data-*` attributes with Tailwind or plain CSS
15
18
  - **Accessible** — full ARIA combobox pattern with keyboard navigation
16
19
  - **SSR compatible** — safe for Next.js with `"use client"` directive
@@ -78,7 +81,7 @@ type MentionToken = {
78
81
 
79
82
  ### MentionProvider
80
83
 
81
- Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down:
84
+ Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down and cross-group search:
82
85
 
83
86
  ```ts
84
87
  type MentionProvider = {
@@ -86,9 +89,12 @@ type MentionProvider = {
86
89
  name: string; // human label
87
90
  getRootItems: (query: string) => Promise<MentionItem[]>;
88
91
  getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
92
+ searchAll?: (query: string) => Promise<MentionItem[]>; // flat search across all levels
89
93
  };
90
94
  ```
91
95
 
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).
97
+
92
98
  ### MentionItem
93
99
 
94
100
  Items returned by providers:
@@ -102,9 +108,12 @@ type MentionItem = {
102
108
  description?: string; // secondary text
103
109
  hasChildren?: boolean; // when true, selecting drills into a child level
104
110
  data?: unknown;
111
+ rootLabel?: string; // parent label for flat search results (used in serialization)
105
112
  };
106
113
  ```
107
114
 
115
+ Set `rootLabel` on items returned by `searchAll` that originate from a nested level. This ensures correct `@rootLabel[label](id)` serialization.
116
+
108
117
  ### MentionsOutput
109
118
 
110
119
  Structured output returned on every change and submit:
@@ -124,7 +133,7 @@ type MentionsOutput = {
124
133
  | `value` | `string` | — | Initial markdown content with `@[label](id)` tokens |
125
134
  | `providers` | `MentionProvider[]` | **required** | Suggestion providers, one per trigger |
126
135
  | `onChange` | `(output: MentionsOutput) => void` | — | Called on every content change |
127
- | `onSubmit` | `(output: MentionsOutput) => void` | — | Called on Enter / Cmd+Enter |
136
+ | `onSubmit` | `(output: MentionsOutput) => void` | — | Called on Enter or Cmd+Enter |
128
137
  | `placeholder` | `string` | `"Type a message..."` | Placeholder text |
129
138
  | `autoFocus` | `boolean` | `false` | Focus editor on mount |
130
139
  | `disabled` | `boolean` | `false` | Disable editing |
@@ -132,7 +141,6 @@ type MentionsOutput = {
132
141
  | `maxLength` | `number` | — | Max plain text character count |
133
142
  | `renderItem` | `(item, depth) => ReactNode` | — | Custom suggestion item renderer |
134
143
  | `clearOnSubmit` | `boolean` | `true` | Auto-clear the editor after `onSubmit` fires |
135
- | `renderItem` | `(item, depth) => ReactNode` | — | Custom suggestion item renderer |
136
144
  | `renderChip` | `(token) => ReactNode` | — | Custom inline mention chip renderer |
137
145
 
138
146
  ## Imperative ref API
@@ -194,14 +202,21 @@ The killer feature. When a `MentionItem` has `hasChildren: true`, selecting it d
194
202
  :web → choose provider → type query
195
203
  ```
196
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
+
197
207
  Keyboard navigation:
198
208
 
199
- | Key | Action |
200
- |-----|--------|
201
- | `↑` `↓` | Navigate suggestions |
202
- | `Enter` `→` | Select / drill into children |
203
- | `←` `Backspace` | Go back one level |
204
- | `Escape` | Close suggestions |
209
+ | Key | Context | Action |
210
+ |-----|---------|--------|
211
+ | `↑` `↓` | Suggestions open | Navigate suggestions |
212
+ | `Enter` | Suggestions open | Select / drill into children |
213
+ | `→` | Suggestions open | Drill into children (if item has children) |
214
+ | `←` | Nested level | Go back one level |
215
+ | `Backspace` | Nested level, empty search | Go back one level |
216
+ | `Escape` | Suggestions open | Close suggestions |
217
+ | `Enter` | Editor (no suggestions) | Submit message |
218
+ | `Shift+Enter` | Editor | New line |
219
+ | `Cmd/Ctrl+Enter` | Editor | Submit message |
205
220
 
206
221
  ## Markdown format
207
222
 
@@ -214,13 +229,24 @@ Mentions serialize to a compact token syntax:
214
229
  Use the standalone helpers for server-side processing:
215
230
 
216
231
  ```ts
217
- import { serializeToMarkdown, parseFromMarkdown } from "@relevaince/mentions";
232
+ import {
233
+ serializeToMarkdown,
234
+ parseFromMarkdown,
235
+ extractFromMarkdown,
236
+ } from "@relevaince/mentions";
218
237
 
219
238
  // Parse markdown into a Tiptap-compatible JSON document
220
239
  const doc = parseFromMarkdown("Check @[NDA](contract:c_44) for risks");
221
240
 
222
241
  // Serialize a Tiptap JSON document back to markdown
223
242
  const md = serializeToMarkdown(doc);
243
+
244
+ // Extract tokens and plain text directly from a markdown string
245
+ const { tokens, plainText } = extractFromMarkdown(
246
+ "Summarize @Marketing[Q4 Strategy.pdf](file:file_1) for the team"
247
+ );
248
+ // tokens → [{ id: "file_1", type: "file", label: "Q4 Strategy.pdf" }]
249
+ // plainText → "Summarize Q4 Strategy.pdf for the team"
224
250
  ```
225
251
 
226
252
  ## Styling
@@ -238,6 +264,8 @@ Zero bundled CSS. Every element exposes `data-*` attributes for styling:
238
264
  [data-suggestion-item] { /* each item */ }
239
265
  [data-suggestion-item-active] { /* highlighted item */ }
240
266
  [data-suggestion-breadcrumb] { /* breadcrumb bar (nested) */ }
267
+ [data-suggestion-search] { /* search input wrapper (nested) */ }
268
+ [data-suggestion-search-input] { /* search input field */ }
241
269
  [data-suggestion-loading] { /* loading indicator */ }
242
270
  ```
243
271
 
@@ -269,9 +297,9 @@ Example with Tailwind:
269
297
  ├── Tiptap Editor (ProseMirror)
270
298
  │ ├── MentionNode Extension — inline atom nodes with id/label/type
271
299
  │ ├── Suggestion Plugin — multi-trigger detection + popover callbacks
272
- │ ├── Submit Extension — Enter / Cmd+Enter handling
273
- │ └── Markdown Serializer — doc ↔ @[label](id) format
274
- └── SuggestionList (React) — headless popover with ARIA
300
+ │ ├── Enter/Submit Extension — Enter submits, Shift+Enter new line
301
+ │ └── Markdown Parser/Serializer — doc ↔ @[label](id) + extractFromMarkdown
302
+ └── SuggestionList (React) — headless popover with ARIA + nested search
275
303
  ```
276
304
 
277
305
  Consumers interact with a single React component. Tiptap and ProseMirror are internal implementation details.
package/dist/index.d.mts CHANGED
@@ -19,6 +19,12 @@ type MentionItem = {
19
19
  hasChildren?: boolean;
20
20
  /** Optional payload carried through to `MentionToken.data`. */
21
21
  data?: unknown;
22
+ /**
23
+ * Root parent label for items returned by `searchAll`.
24
+ * Used for correct `@rootLabel[label](id)` serialization when the
25
+ * item originates from a nested level.
26
+ */
27
+ rootLabel?: string;
22
28
  };
23
29
  /**
24
30
  * Hierarchical suggestion source registered per trigger character.
@@ -36,6 +42,12 @@ type MentionProvider = {
36
42
  getRootItems: (query: string) => Promise<MentionItem[]>;
37
43
  /** Fetch child suggestions when the user drills into `parent`. */
38
44
  getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
45
+ /**
46
+ * Flat search across **all** items, including nested children.
47
+ * When provided and the user types a non-empty query at root level,
48
+ * results from `searchAll` are shown instead of `getRootItems`.
49
+ */
50
+ searchAll?: (query: string) => Promise<MentionItem[]>;
39
51
  };
40
52
 
41
53
  /**
@@ -142,15 +154,34 @@ declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps
142
154
  /**
143
155
  * Serialize a Tiptap JSON document to a markdown string.
144
156
  *
145
- * Mention nodes are encoded as `@[label](id)` tokens.
157
+ * Mention nodes are encoded as `@[label](id)` or `@rootLabel[label](id)` when the mention has a root (e.g. file under workspace).
146
158
  * All other text passes through verbatim.
147
159
  */
148
160
  declare function serializeToMarkdown(doc: JSONContent): string;
149
161
 
150
162
  /**
151
- * Parse a markdown string (with `@[label](id)` or `@[label](type:id)` tokens)
163
+ * Parse a markdown string (with `@[label](id)`, `@[label](type:id)`, or `@rootLabel[label](id)` tokens)
152
164
  * into a Tiptap-compatible JSON document.
153
165
  */
154
166
  declare function parseFromMarkdown(markdown: string): JSONContent;
167
+ /**
168
+ * Parse a markdown string and extract structured data from it.
169
+ *
170
+ * This is a convenience wrapper that combines `parseFromMarkdown`,
171
+ * `extractTokens`, and `extractPlainText` into a single call.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const { tokens, plainText } = extractFromMarkdown(
176
+ * "Check @Marketing[Q4 Strategy.pdf](file_1) for details"
177
+ * );
178
+ * // tokens → [{ id: "file_1", type: "unknown", label: "Q4 Strategy.pdf" }]
179
+ * // plainText → "Check Q4 Strategy.pdf for details"
180
+ * ```
181
+ */
182
+ declare function extractFromMarkdown(markdown: string): {
183
+ tokens: MentionToken[];
184
+ plainText: string;
185
+ };
155
186
 
156
- export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };
187
+ export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, extractFromMarkdown, parseFromMarkdown, serializeToMarkdown };
package/dist/index.d.ts CHANGED
@@ -19,6 +19,12 @@ type MentionItem = {
19
19
  hasChildren?: boolean;
20
20
  /** Optional payload carried through to `MentionToken.data`. */
21
21
  data?: unknown;
22
+ /**
23
+ * Root parent label for items returned by `searchAll`.
24
+ * Used for correct `@rootLabel[label](id)` serialization when the
25
+ * item originates from a nested level.
26
+ */
27
+ rootLabel?: string;
22
28
  };
23
29
  /**
24
30
  * Hierarchical suggestion source registered per trigger character.
@@ -36,6 +42,12 @@ type MentionProvider = {
36
42
  getRootItems: (query: string) => Promise<MentionItem[]>;
37
43
  /** Fetch child suggestions when the user drills into `parent`. */
38
44
  getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
45
+ /**
46
+ * Flat search across **all** items, including nested children.
47
+ * When provided and the user types a non-empty query at root level,
48
+ * results from `searchAll` are shown instead of `getRootItems`.
49
+ */
50
+ searchAll?: (query: string) => Promise<MentionItem[]>;
39
51
  };
40
52
 
41
53
  /**
@@ -142,15 +154,34 @@ declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps
142
154
  /**
143
155
  * Serialize a Tiptap JSON document to a markdown string.
144
156
  *
145
- * Mention nodes are encoded as `@[label](id)` tokens.
157
+ * Mention nodes are encoded as `@[label](id)` or `@rootLabel[label](id)` when the mention has a root (e.g. file under workspace).
146
158
  * All other text passes through verbatim.
147
159
  */
148
160
  declare function serializeToMarkdown(doc: JSONContent): string;
149
161
 
150
162
  /**
151
- * Parse a markdown string (with `@[label](id)` or `@[label](type:id)` tokens)
163
+ * Parse a markdown string (with `@[label](id)`, `@[label](type:id)`, or `@rootLabel[label](id)` tokens)
152
164
  * into a Tiptap-compatible JSON document.
153
165
  */
154
166
  declare function parseFromMarkdown(markdown: string): JSONContent;
167
+ /**
168
+ * Parse a markdown string and extract structured data from it.
169
+ *
170
+ * This is a convenience wrapper that combines `parseFromMarkdown`,
171
+ * `extractTokens`, and `extractPlainText` into a single call.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const { tokens, plainText } = extractFromMarkdown(
176
+ * "Check @Marketing[Q4 Strategy.pdf](file_1) for details"
177
+ * );
178
+ * // tokens → [{ id: "file_1", type: "unknown", label: "Q4 Strategy.pdf" }]
179
+ * // plainText → "Check Q4 Strategy.pdf for details"
180
+ * ```
181
+ */
182
+ declare function extractFromMarkdown(markdown: string): {
183
+ tokens: MentionToken[];
184
+ plainText: string;
185
+ };
155
186
 
156
- export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };
187
+ export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, extractFromMarkdown, parseFromMarkdown, serializeToMarkdown };
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  MentionsInput: () => MentionsInput,
34
+ extractFromMarkdown: () => extractFromMarkdown,
34
35
  parseFromMarkdown: () => parseFromMarkdown,
35
36
  serializeToMarkdown: () => serializeToMarkdown
36
37
  });
@@ -46,6 +47,7 @@ var import_react2 = require("@tiptap/react");
46
47
  var import_starter_kit = __toESM(require("@tiptap/starter-kit"));
47
48
  var import_extension_placeholder = __toESM(require("@tiptap/extension-placeholder"));
48
49
  var import_core3 = require("@tiptap/core");
50
+ var import_state2 = require("@tiptap/pm/state");
49
51
 
50
52
  // src/core/mentionExtension.ts
51
53
  var import_core = require("@tiptap/core");
@@ -78,6 +80,11 @@ var MentionNode = import_core.Node.create({
78
80
  default: null,
79
81
  parseHTML: (element) => element.getAttribute("data-type"),
80
82
  renderHTML: (attributes) => ({ "data-type": attributes.entityType })
83
+ },
84
+ rootLabel: {
85
+ default: null,
86
+ parseHTML: (element) => element.getAttribute("data-root-label"),
87
+ renderHTML: (attributes) => attributes.rootLabel ? { "data-root-label": attributes.rootLabel } : {}
81
88
  }
82
89
  };
83
90
  },
@@ -88,13 +95,14 @@ var MentionNode = import_core.Node.create({
88
95
  const entityType = node.attrs.entityType;
89
96
  const label = node.attrs.label;
90
97
  const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
98
+ const display = `${prefix}${label}`;
91
99
  return [
92
100
  "span",
93
101
  (0, import_core.mergeAttributes)(HTMLAttributes, {
94
102
  "data-mention": "",
95
103
  class: "mention-chip"
96
104
  }),
97
- `${prefix}${label}`
105
+ display
98
106
  ];
99
107
  },
100
108
  renderText({ node }) {
@@ -146,6 +154,7 @@ var suggestionPluginKey = new import_state.PluginKey("mentionSuggestion");
146
154
  function createSuggestionExtension(triggers, callbacksRef) {
147
155
  return import_core2.Extension.create({
148
156
  name: "mentionSuggestion",
157
+ priority: 200,
149
158
  addProseMirrorPlugins() {
150
159
  const editor = this.editor;
151
160
  let active = false;
@@ -174,7 +183,8 @@ function createSuggestionExtension(triggers, callbacksRef) {
174
183
  attrs: {
175
184
  id: attrs.id,
176
185
  label: attrs.label,
177
- entityType: attrs.entityType
186
+ entityType: attrs.entityType,
187
+ rootLabel: attrs.rootLabel ?? null
178
188
  }
179
189
  },
180
190
  { type: "text", text: " " }
@@ -276,7 +286,10 @@ function serializeParagraph(node) {
276
286
  if (!node.content) return "";
277
287
  return node.content.map((child) => {
278
288
  if (child.type === "mention") {
279
- const { id, label } = child.attrs ?? {};
289
+ const { id, label, rootLabel } = child.attrs ?? {};
290
+ if (rootLabel != null && rootLabel !== "") {
291
+ return `@${rootLabel}[${label}](${id})`;
292
+ }
280
293
  return `@[${label}](${id})`;
281
294
  }
282
295
  return child.text ?? "";
@@ -322,7 +335,7 @@ function extractParagraphText(node) {
322
335
  }
323
336
 
324
337
  // src/core/markdownParser.ts
325
- var MENTION_RE = /@\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
338
+ var MENTION_RE = /@(?:([^\[]+)\[)?\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
326
339
  function parseFromMarkdown(markdown) {
327
340
  const lines = markdown.split("\n");
328
341
  const content = lines.map((line) => ({
@@ -338,9 +351,10 @@ function parseLine(line) {
338
351
  let match;
339
352
  while ((match = MENTION_RE.exec(line)) !== null) {
340
353
  const fullMatch = match[0];
341
- const label = match[1];
342
- const entityType = match[2] ?? "unknown";
343
- const id = match[3];
354
+ const rootLabel = match[1] ?? null;
355
+ const label = match[2];
356
+ const entityType = match[3] ?? "unknown";
357
+ const id = match[4];
344
358
  if (match.index > lastIndex) {
345
359
  nodes.push({
346
360
  type: "text",
@@ -352,7 +366,8 @@ function parseLine(line) {
352
366
  attrs: {
353
367
  id,
354
368
  label,
355
- entityType
369
+ entityType,
370
+ rootLabel
356
371
  }
357
372
  });
358
373
  lastIndex = match.index + fullMatch.length;
@@ -368,6 +383,13 @@ function parseLine(line) {
368
383
  }
369
384
  return nodes;
370
385
  }
386
+ function extractFromMarkdown(markdown) {
387
+ const doc = parseFromMarkdown(markdown);
388
+ return {
389
+ tokens: extractTokens(doc),
390
+ plainText: extractPlainText(doc)
391
+ };
392
+ }
371
393
 
372
394
  // src/hooks/useMentionsEditor.ts
373
395
  function buildOutput(editor) {
@@ -381,6 +403,7 @@ function buildOutput(editor) {
381
403
  function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
382
404
  return import_core3.Extension.create({
383
405
  name: "submitShortcut",
406
+ priority: 150,
384
407
  addKeyboardShortcuts() {
385
408
  return {
386
409
  "Mod-Enter": () => {
@@ -396,22 +419,35 @@ function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
396
419
  }
397
420
  });
398
421
  }
422
+ var enterSubmitPluginKey = new import_state2.PluginKey("enterSubmit");
399
423
  function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
400
424
  return import_core3.Extension.create({
401
425
  name: "enterSubmit",
402
- priority: 50,
403
- addKeyboardShortcuts() {
404
- return {
405
- Enter: () => {
406
- if (onSubmitRef.current) {
407
- onSubmitRef.current(buildOutput(this.editor));
408
- if (clearOnSubmitRef.current) {
409
- this.editor.commands.clearContent(true);
426
+ priority: 150,
427
+ addProseMirrorPlugins() {
428
+ const editor = this.editor;
429
+ return [
430
+ new import_state2.Plugin({
431
+ key: enterSubmitPluginKey,
432
+ props: {
433
+ handleKeyDown(_view, event) {
434
+ if (event.key !== "Enter") return false;
435
+ if (event.shiftKey) {
436
+ editor.commands.splitBlock();
437
+ return true;
438
+ }
439
+ if (event.metaKey || event.ctrlKey) return false;
440
+ if (onSubmitRef.current) {
441
+ onSubmitRef.current(buildOutput(editor));
442
+ if (clearOnSubmitRef.current) {
443
+ editor.commands.clearContent(true);
444
+ }
445
+ }
446
+ return true;
410
447
  }
411
448
  }
412
- return true;
413
- }
414
- };
449
+ })
450
+ ];
415
451
  }
416
452
  });
417
453
  }
@@ -464,10 +500,12 @@ function useMentionsEditor({
464
500
  bulletList: false,
465
501
  orderedList: false,
466
502
  listItem: false,
467
- horizontalRule: false
503
+ horizontalRule: false,
504
+ hardBreak: false
468
505
  }),
469
506
  import_extension_placeholder.default.configure({
470
- placeholder: placeholder ?? "Type a message..."
507
+ placeholder: placeholder ?? "Type a message...",
508
+ showOnlyCurrent: false
471
509
  }),
472
510
  MentionNode,
473
511
  suggestionExtension,
@@ -535,10 +573,17 @@ function useSuggestion(providers) {
535
573
  );
536
574
  const providerRef = (0, import_react3.useRef)(null);
537
575
  const fetchItems = (0, import_react3.useCallback)(
538
- async (provider, query, parent) => {
576
+ async (provider, query, parent, useSearchAll) => {
539
577
  setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
540
578
  try {
541
- const items = parent && provider.getChildren ? await provider.getChildren(parent, query) : await provider.getRootItems(query);
579
+ let items;
580
+ if (useSearchAll && provider.searchAll) {
581
+ items = await provider.searchAll(query);
582
+ } else if (parent && provider.getChildren) {
583
+ items = await provider.getChildren(parent, query);
584
+ } else {
585
+ items = await provider.getRootItems(query);
586
+ }
542
587
  setUIState((prev) => ({
543
588
  ...prev,
544
589
  items,
@@ -575,7 +620,11 @@ function useSuggestion(providers) {
575
620
  trigger: props.trigger,
576
621
  query: props.query
577
622
  });
578
- fetchItems(provider, props.query);
623
+ if (props.query.trim() && provider.searchAll) {
624
+ fetchItems(provider, props.query, void 0, true);
625
+ } else {
626
+ fetchItems(provider, props.query);
627
+ }
579
628
  },
580
629
  [fetchItems]
581
630
  );
@@ -584,12 +633,25 @@ function useSuggestion(providers) {
584
633
  const provider = providerRef.current;
585
634
  if (!provider) return;
586
635
  commandRef.current = props.command;
587
- setUIState((prev) => ({
588
- ...prev,
589
- clientRect: props.clientRect,
590
- query: props.query
591
- }));
592
- if (stateRef.current.breadcrumbs.length === 0) {
636
+ const current = stateRef.current;
637
+ if (current.breadcrumbs.length > 0) {
638
+ setUIState((prev) => ({
639
+ ...prev,
640
+ breadcrumbs: [],
641
+ clientRect: props.clientRect,
642
+ query: props.query,
643
+ activeIndex: 0
644
+ }));
645
+ } else {
646
+ setUIState((prev) => ({
647
+ ...prev,
648
+ clientRect: props.clientRect,
649
+ query: props.query
650
+ }));
651
+ }
652
+ if (props.query.trim() && provider.searchAll) {
653
+ fetchItems(provider, props.query, void 0, true);
654
+ } else {
593
655
  fetchItems(provider, props.query);
594
656
  }
595
657
  },
@@ -631,10 +693,12 @@ function useSuggestion(providers) {
631
693
  return;
632
694
  }
633
695
  if (commandRef.current) {
696
+ const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
634
697
  commandRef.current({
635
698
  id: selected.id,
636
699
  label: selected.label,
637
- entityType: selected.type
700
+ entityType: selected.type,
701
+ rootLabel
638
702
  });
639
703
  }
640
704
  },
@@ -663,6 +727,18 @@ function useSuggestion(providers) {
663
727
  const close = (0, import_react3.useCallback)(() => {
664
728
  setUIState(IDLE_STATE);
665
729
  }, []);
730
+ const searchNested = (0, import_react3.useCallback)(
731
+ (query) => {
732
+ const provider = providerRef.current;
733
+ if (!provider) return;
734
+ const current = stateRef.current;
735
+ const parent = current.breadcrumbs[current.breadcrumbs.length - 1];
736
+ if (parent) {
737
+ fetchItems(provider, query, parent);
738
+ }
739
+ },
740
+ [fetchItems]
741
+ );
666
742
  const onKeyDown = (0, import_react3.useCallback)(
667
743
  ({ event }) => {
668
744
  const current = stateRef.current;
@@ -722,7 +798,8 @@ function useSuggestion(providers) {
722
798
  navigateDown,
723
799
  select,
724
800
  goBack,
725
- close
801
+ close,
802
+ searchNested
726
803
  };
727
804
  return { uiState, actions, callbacksRef };
728
805
  }
@@ -768,10 +845,29 @@ function SuggestionList({
768
845
  onSelect,
769
846
  onHover,
770
847
  onGoBack,
848
+ onSearchNested,
849
+ onNavigateUp,
850
+ onNavigateDown,
851
+ onClose,
771
852
  renderItem
772
853
  }) {
773
854
  const listRef = (0, import_react4.useRef)(null);
855
+ const searchInputRef = (0, import_react4.useRef)(null);
774
856
  const depth = breadcrumbs.length;
857
+ const [nestedQuery, setNestedQuery] = (0, import_react4.useState)("");
858
+ const breadcrumbKey = breadcrumbs.map((b) => b.id).join("/");
859
+ const prevBreadcrumbKey = (0, import_react4.useRef)(breadcrumbKey);
860
+ (0, import_react4.useEffect)(() => {
861
+ if (prevBreadcrumbKey.current !== breadcrumbKey) {
862
+ setNestedQuery("");
863
+ prevBreadcrumbKey.current = breadcrumbKey;
864
+ }
865
+ }, [breadcrumbKey]);
866
+ (0, import_react4.useEffect)(() => {
867
+ if (breadcrumbs.length > 0 && searchInputRef.current) {
868
+ requestAnimationFrame(() => searchInputRef.current?.focus());
869
+ }
870
+ }, [breadcrumbKey, breadcrumbs.length]);
775
871
  (0, import_react4.useEffect)(() => {
776
872
  if (!listRef.current) return;
777
873
  const active = listRef.current.querySelector('[aria-selected="true"]');
@@ -779,6 +875,35 @@ function SuggestionList({
779
875
  }, [activeIndex]);
780
876
  const style = usePopoverPosition(clientRect);
781
877
  if (items.length === 0 && !loading) return null;
878
+ const handleSearchKeyDown = (e) => {
879
+ switch (e.key) {
880
+ case "ArrowDown":
881
+ e.preventDefault();
882
+ onNavigateDown?.();
883
+ break;
884
+ case "ArrowUp":
885
+ e.preventDefault();
886
+ onNavigateUp?.();
887
+ break;
888
+ case "Enter": {
889
+ e.preventDefault();
890
+ e.stopPropagation();
891
+ const selectedItem = items[activeIndex];
892
+ if (selectedItem) onSelect(selectedItem);
893
+ break;
894
+ }
895
+ case "Escape":
896
+ e.preventDefault();
897
+ onClose?.();
898
+ break;
899
+ case "Backspace":
900
+ if (nestedQuery === "") {
901
+ e.preventDefault();
902
+ onGoBack();
903
+ }
904
+ break;
905
+ }
906
+ };
782
907
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
783
908
  "div",
784
909
  {
@@ -803,6 +928,24 @@ function SuggestionList({
803
928
  crumb.label
804
929
  ] }, crumb.id))
805
930
  ] }),
931
+ breadcrumbs.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-search": "", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
932
+ "input",
933
+ {
934
+ ref: searchInputRef,
935
+ type: "text",
936
+ "data-suggestion-search-input": "",
937
+ placeholder: "Search...",
938
+ value: nestedQuery,
939
+ onChange: (e) => {
940
+ const q = e.target.value;
941
+ setNestedQuery(q);
942
+ onSearchNested?.(q);
943
+ },
944
+ onKeyDown: handleSearchKeyDown,
945
+ autoComplete: "off",
946
+ spellCheck: false
947
+ }
948
+ ) }),
806
949
  loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-loading": "", children: "Loading..." }),
807
950
  !loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...listboxAttrs(LISTBOX_ID, `${trigger ?? ""} suggestions`), children: items.map((item, index) => {
808
951
  const isActive = index === activeIndex;
@@ -885,7 +1028,8 @@ var MentionsInput = (0, import_react5.forwardRef)(
885
1028
  [clear, setContent, focus]
886
1029
  );
887
1030
  const isExpanded = uiState.state !== "idle";
888
- const handleHover = (0, import_react5.useCallback)((_index) => {
1031
+ const handleHover = (0, import_react5.useCallback)((index) => {
1032
+ void index;
889
1033
  }, []);
890
1034
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
891
1035
  "div",
@@ -909,6 +1053,10 @@ var MentionsInput = (0, import_react5.forwardRef)(
909
1053
  onSelect: (item) => actions.select(item),
910
1054
  onHover: handleHover,
911
1055
  onGoBack: actions.goBack,
1056
+ onSearchNested: actions.searchNested,
1057
+ onNavigateUp: actions.navigateUp,
1058
+ onNavigateDown: actions.navigateDown,
1059
+ onClose: actions.close,
912
1060
  renderItem
913
1061
  }
914
1062
  )
@@ -920,6 +1068,7 @@ var MentionsInput = (0, import_react5.forwardRef)(
920
1068
  // Annotate the CommonJS export names for ESM import in node:
921
1069
  0 && (module.exports = {
922
1070
  MentionsInput,
1071
+ extractFromMarkdown,
923
1072
  parseFromMarkdown,
924
1073
  serializeToMarkdown
925
1074
  });