@relevaince/mentions 0.1.0 → 0.2.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
@@ -131,8 +131,59 @@ type MentionsOutput = {
131
131
  | `className` | `string` | — | CSS class on the wrapper |
132
132
  | `maxLength` | `number` | — | Max plain text character count |
133
133
  | `renderItem` | `(item, depth) => ReactNode` | — | Custom suggestion item renderer |
134
+ | `clearOnSubmit` | `boolean` | `true` | Auto-clear the editor after `onSubmit` fires |
135
+ | `renderItem` | `(item, depth) => ReactNode` | — | Custom suggestion item renderer |
134
136
  | `renderChip` | `(token) => ReactNode` | — | Custom inline mention chip renderer |
135
137
 
138
+ ## Imperative ref API
139
+
140
+ `MentionsInput` supports `forwardRef` for programmatic control. This is essential for flows like clearing after submit, injecting prompts from a library, or "Enhance Prompt" features.
141
+
142
+ ```tsx
143
+ import { useRef } from "react";
144
+ import { MentionsInput, type MentionsInputHandle } from "@relevaince/mentions";
145
+
146
+ function Chat() {
147
+ const ref = useRef<MentionsInputHandle>(null);
148
+
149
+ return (
150
+ <>
151
+ <MentionsInput ref={ref} providers={providers} />
152
+
153
+ <button onClick={() => ref.current?.clear()}>
154
+ Clear
155
+ </button>
156
+
157
+ <button onClick={() => {
158
+ ref.current?.setContent("Summarize @[NDA](contract:c_1) risks");
159
+ ref.current?.focus();
160
+ }}>
161
+ Use Prompt
162
+ </button>
163
+ </>
164
+ );
165
+ }
166
+ ```
167
+
168
+ ### Handle methods
169
+
170
+ | Method | Signature | Description |
171
+ |--------|-----------|-------------|
172
+ | `clear` | `() => void` | Clears all editor content |
173
+ | `setContent` | `(markdown: string) => void` | Replaces content with a markdown string (mention tokens are parsed) |
174
+ | `focus` | `() => void` | Focuses the editor and places the cursor at the end |
175
+
176
+ ### Auto-clear on submit
177
+
178
+ By default, the editor clears itself after `onSubmit` fires. Disable with `clearOnSubmit={false}` if you want to manage clearing yourself:
179
+
180
+ ```tsx
181
+ <MentionsInput
182
+ onSubmit={(output) => sendMessage(output)}
183
+ clearOnSubmit={false} // don't auto-clear
184
+ />
185
+ ```
186
+
136
187
  ## Nested mentions
137
188
 
138
189
  The killer feature. When a `MentionItem` has `hasChildren: true`, selecting it drills into the next level using `provider.getChildren()`:
package/dist/index.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ReactNode } from 'react';
1
+ import React, { ReactNode } from 'react';
3
2
  import { JSONContent } from '@tiptap/core';
4
3
 
5
4
  /**
@@ -93,6 +92,8 @@ type MentionsInputProps = {
93
92
  className?: string;
94
93
  /** Called when the user presses Enter (or Cmd+Enter). */
95
94
  onSubmit?: (output: MentionsOutput) => void;
95
+ /** Automatically clear the editor after `onSubmit` fires. Defaults to `true`. */
96
+ clearOnSubmit?: boolean;
96
97
  /** Maximum character count (plain text length). */
97
98
  maxLength?: number;
98
99
  /** Custom renderer for suggestion list items. */
@@ -100,6 +101,26 @@ type MentionsInputProps = {
100
101
  /** Custom renderer for inline mention chips. */
101
102
  renderChip?: (token: MentionToken) => ReactNode;
102
103
  };
104
+ /**
105
+ * Imperative handle exposed via `ref` on `<MentionsInput>`.
106
+ *
107
+ * ```tsx
108
+ * const ref = useRef<MentionsInputHandle>(null);
109
+ * <MentionsInput ref={ref} ... />
110
+ *
111
+ * ref.current.clear();
112
+ * ref.current.setContent("Hello @[Marketing](ws_123)");
113
+ * ref.current.focus();
114
+ * ```
115
+ */
116
+ type MentionsInputHandle = {
117
+ /** Clear all editor content. */
118
+ clear: () => void;
119
+ /** Replace editor content with a markdown string. */
120
+ setContent: (markdown: string) => void;
121
+ /** Focus the editor. */
122
+ focus: () => void;
123
+ };
103
124
 
104
125
  /**
105
126
  * `<MentionsInput>` — the single public component.
@@ -107,8 +128,16 @@ type MentionsInputProps = {
107
128
  * A structured text editor with typed entity tokens.
108
129
  * Consumers register `providers` for each trigger character,
109
130
  * and receive structured output via `onChange` and `onSubmit`.
131
+ *
132
+ * Supports an imperative ref handle for programmatic control:
133
+ * ```tsx
134
+ * const ref = useRef<MentionsInputHandle>(null);
135
+ * ref.current.clear();
136
+ * ref.current.setContent("@[Marketing](ws_123) summarize");
137
+ * ref.current.focus();
138
+ * ```
110
139
  */
111
- declare function MentionsInput({ value, providers, onChange, placeholder, autoFocus, disabled, className, onSubmit, maxLength, renderItem, renderChip, }: MentionsInputProps): react_jsx_runtime.JSX.Element;
140
+ declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps & React.RefAttributes<MentionsInputHandle>>;
112
141
 
113
142
  /**
114
143
  * Serialize a Tiptap JSON document to a markdown string.
@@ -124,4 +153,4 @@ declare function serializeToMarkdown(doc: JSONContent): string;
124
153
  */
125
154
  declare function parseFromMarkdown(markdown: string): JSONContent;
126
155
 
127
- export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };
156
+ export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ReactNode } from 'react';
1
+ import React, { ReactNode } from 'react';
3
2
  import { JSONContent } from '@tiptap/core';
4
3
 
5
4
  /**
@@ -93,6 +92,8 @@ type MentionsInputProps = {
93
92
  className?: string;
94
93
  /** Called when the user presses Enter (or Cmd+Enter). */
95
94
  onSubmit?: (output: MentionsOutput) => void;
95
+ /** Automatically clear the editor after `onSubmit` fires. Defaults to `true`. */
96
+ clearOnSubmit?: boolean;
96
97
  /** Maximum character count (plain text length). */
97
98
  maxLength?: number;
98
99
  /** Custom renderer for suggestion list items. */
@@ -100,6 +101,26 @@ type MentionsInputProps = {
100
101
  /** Custom renderer for inline mention chips. */
101
102
  renderChip?: (token: MentionToken) => ReactNode;
102
103
  };
104
+ /**
105
+ * Imperative handle exposed via `ref` on `<MentionsInput>`.
106
+ *
107
+ * ```tsx
108
+ * const ref = useRef<MentionsInputHandle>(null);
109
+ * <MentionsInput ref={ref} ... />
110
+ *
111
+ * ref.current.clear();
112
+ * ref.current.setContent("Hello @[Marketing](ws_123)");
113
+ * ref.current.focus();
114
+ * ```
115
+ */
116
+ type MentionsInputHandle = {
117
+ /** Clear all editor content. */
118
+ clear: () => void;
119
+ /** Replace editor content with a markdown string. */
120
+ setContent: (markdown: string) => void;
121
+ /** Focus the editor. */
122
+ focus: () => void;
123
+ };
103
124
 
104
125
  /**
105
126
  * `<MentionsInput>` — the single public component.
@@ -107,8 +128,16 @@ type MentionsInputProps = {
107
128
  * A structured text editor with typed entity tokens.
108
129
  * Consumers register `providers` for each trigger character,
109
130
  * and receive structured output via `onChange` and `onSubmit`.
131
+ *
132
+ * Supports an imperative ref handle for programmatic control:
133
+ * ```tsx
134
+ * const ref = useRef<MentionsInputHandle>(null);
135
+ * ref.current.clear();
136
+ * ref.current.setContent("@[Marketing](ws_123) summarize");
137
+ * ref.current.focus();
138
+ * ```
110
139
  */
111
- declare function MentionsInput({ value, providers, onChange, placeholder, autoFocus, disabled, className, onSubmit, maxLength, renderItem, renderChip, }: MentionsInputProps): react_jsx_runtime.JSX.Element;
140
+ declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps & React.RefAttributes<MentionsInputHandle>>;
112
141
 
113
142
  /**
114
143
  * Serialize a Tiptap JSON document to a markdown string.
@@ -124,4 +153,4 @@ declare function serializeToMarkdown(doc: JSONContent): string;
124
153
  */
125
154
  declare function parseFromMarkdown(markdown: string): JSONContent;
126
155
 
127
- export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };
156
+ export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };
package/dist/index.js CHANGED
@@ -369,19 +369,25 @@ function parseLine(line) {
369
369
  }
370
370
 
371
371
  // src/hooks/useMentionsEditor.ts
372
- function createSubmitExtension(onSubmitRef) {
372
+ function buildOutput(editor) {
373
+ const json = editor.getJSON();
374
+ return {
375
+ markdown: serializeToMarkdown(json),
376
+ tokens: extractTokens(json),
377
+ plainText: extractPlainText(json)
378
+ };
379
+ }
380
+ function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
373
381
  return import_core3.Extension.create({
374
382
  name: "submitShortcut",
375
383
  addKeyboardShortcuts() {
376
384
  return {
377
385
  "Mod-Enter": () => {
378
386
  if (onSubmitRef.current) {
379
- const json = this.editor.getJSON();
380
- onSubmitRef.current({
381
- markdown: serializeToMarkdown(json),
382
- tokens: extractTokens(json),
383
- plainText: extractPlainText(json)
384
- });
387
+ onSubmitRef.current(buildOutput(this.editor));
388
+ if (clearOnSubmitRef.current) {
389
+ this.editor.commands.clearContent(true);
390
+ }
385
391
  }
386
392
  return true;
387
393
  }
@@ -389,7 +395,7 @@ function createSubmitExtension(onSubmitRef) {
389
395
  }
390
396
  });
391
397
  }
392
- function createEnterExtension(onSubmitRef) {
398
+ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
393
399
  return import_core3.Extension.create({
394
400
  name: "enterSubmit",
395
401
  priority: 50,
@@ -397,12 +403,10 @@ function createEnterExtension(onSubmitRef) {
397
403
  return {
398
404
  Enter: () => {
399
405
  if (onSubmitRef.current) {
400
- const json = this.editor.getJSON();
401
- onSubmitRef.current({
402
- markdown: serializeToMarkdown(json),
403
- tokens: extractTokens(json),
404
- plainText: extractPlainText(json)
405
- });
406
+ onSubmitRef.current(buildOutput(this.editor));
407
+ if (clearOnSubmitRef.current) {
408
+ this.editor.commands.clearContent(true);
409
+ }
406
410
  }
407
411
  return true;
408
412
  }
@@ -415,6 +419,7 @@ function useMentionsEditor({
415
419
  value,
416
420
  onChange,
417
421
  onSubmit,
422
+ clearOnSubmit = true,
418
423
  placeholder,
419
424
  autoFocus = false,
420
425
  editable = true,
@@ -424,6 +429,8 @@ function useMentionsEditor({
424
429
  onChangeRef.current = onChange;
425
430
  const onSubmitRef = (0, import_react.useRef)(onSubmit);
426
431
  onSubmitRef.current = onSubmit;
432
+ const clearOnSubmitRef = (0, import_react.useRef)(clearOnSubmit);
433
+ clearOnSubmitRef.current = clearOnSubmit;
427
434
  const initialContent = (0, import_react.useMemo)(() => {
428
435
  if (!value) return void 0;
429
436
  return parseFromMarkdown(value);
@@ -439,8 +446,14 @@ function useMentionsEditor({
439
446
  // eslint-disable-next-line react-hooks/exhaustive-deps
440
447
  [triggersKey]
441
448
  );
442
- const submitExt = (0, import_react.useMemo)(() => createSubmitExtension(onSubmitRef), []);
443
- const enterExt = (0, import_react.useMemo)(() => createEnterExtension(onSubmitRef), []);
449
+ const submitExt = (0, import_react.useMemo)(
450
+ () => createSubmitExtension(onSubmitRef, clearOnSubmitRef),
451
+ []
452
+ );
453
+ const enterExt = (0, import_react.useMemo)(
454
+ () => createEnterExtension(onSubmitRef, clearOnSubmitRef),
455
+ []
456
+ );
444
457
  const editor = (0, import_react2.useEditor)({
445
458
  extensions: [
446
459
  import_starter_kit.default.configure({
@@ -467,12 +480,7 @@ function useMentionsEditor({
467
480
  }
468
481
  },
469
482
  onUpdate: ({ editor: editor2 }) => {
470
- const json = editor2.getJSON();
471
- onChangeRef.current?.({
472
- markdown: serializeToMarkdown(json),
473
- tokens: extractTokens(json),
474
- plainText: extractPlainText(json)
475
- });
483
+ onChangeRef.current?.(buildOutput(editor2));
476
484
  }
477
485
  });
478
486
  (0, import_react.useEffect)(() => {
@@ -480,16 +488,25 @@ function useMentionsEditor({
480
488
  editor.setEditable(editable);
481
489
  }
482
490
  }, [editor, editable]);
491
+ const clear = (0, import_react.useCallback)(() => {
492
+ editor?.commands.clearContent(true);
493
+ }, [editor]);
494
+ const setContent = (0, import_react.useCallback)(
495
+ (markdown) => {
496
+ if (!editor) return;
497
+ const doc = parseFromMarkdown(markdown);
498
+ editor.commands.setContent(doc);
499
+ },
500
+ [editor]
501
+ );
502
+ const focus = (0, import_react.useCallback)(() => {
503
+ editor?.commands.focus("end");
504
+ }, [editor]);
483
505
  const getOutput = (0, import_react.useCallback)(() => {
484
506
  if (!editor) return null;
485
- const json = editor.getJSON();
486
- return {
487
- markdown: serializeToMarkdown(json),
488
- tokens: extractTokens(json),
489
- plainText: extractPlainText(json)
490
- };
507
+ return buildOutput(editor);
491
508
  }, [editor]);
492
- return { editor, getOutput };
509
+ return { editor, getOutput, clear, setContent, focus };
493
510
  }
494
511
 
495
512
  // src/hooks/useSuggestion.ts
@@ -832,62 +849,71 @@ function usePopoverPosition(clientRect) {
832
849
  // src/components/MentionsInput.tsx
833
850
  var import_jsx_runtime2 = require("react/jsx-runtime");
834
851
  var LISTBOX_ID2 = "mentions-suggestion-listbox";
835
- function MentionsInput({
836
- value,
837
- providers,
838
- onChange,
839
- placeholder = "Type a message...",
840
- autoFocus = false,
841
- disabled = false,
842
- className,
843
- onSubmit,
844
- maxLength,
845
- renderItem,
846
- renderChip
847
- }) {
848
- const { uiState, actions, callbacksRef } = useSuggestion(providers);
849
- const { editor } = useMentionsEditor({
850
- providers,
852
+ var MentionsInput = (0, import_react5.forwardRef)(
853
+ function MentionsInput2({
851
854
  value,
855
+ providers,
852
856
  onChange,
857
+ placeholder = "Type a message...",
858
+ autoFocus = false,
859
+ disabled = false,
860
+ className,
853
861
  onSubmit,
854
- placeholder,
855
- autoFocus,
856
- editable: !disabled,
857
- callbacksRef
858
- });
859
- const isExpanded = uiState.state !== "idle";
860
- const handleHover = (0, import_react5.useCallback)((index) => {
861
- }, []);
862
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
863
- "div",
864
- {
865
- className,
866
- "data-mentions-input": "",
867
- "data-disabled": disabled ? "" : void 0,
868
- ...comboboxAttrs(isExpanded, LISTBOX_ID2),
869
- "aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
870
- children: [
871
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react6.EditorContent, { editor }),
872
- isExpanded && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
873
- SuggestionList,
874
- {
875
- items: uiState.items,
876
- activeIndex: uiState.activeIndex,
877
- breadcrumbs: uiState.breadcrumbs,
878
- loading: uiState.loading,
879
- trigger: uiState.trigger,
880
- clientRect: uiState.clientRect,
881
- onSelect: (item) => actions.select(item),
882
- onHover: handleHover,
883
- onGoBack: actions.goBack,
884
- renderItem
885
- }
886
- )
887
- ]
888
- }
889
- );
890
- }
862
+ clearOnSubmit = true,
863
+ maxLength,
864
+ renderItem,
865
+ renderChip
866
+ }, ref) {
867
+ const { uiState, actions, callbacksRef } = useSuggestion(providers);
868
+ const { editor, clear, setContent, focus } = useMentionsEditor({
869
+ providers,
870
+ value,
871
+ onChange,
872
+ onSubmit,
873
+ clearOnSubmit,
874
+ placeholder,
875
+ autoFocus,
876
+ editable: !disabled,
877
+ callbacksRef
878
+ });
879
+ (0, import_react5.useImperativeHandle)(
880
+ ref,
881
+ () => ({ clear, setContent, focus }),
882
+ [clear, setContent, focus]
883
+ );
884
+ const isExpanded = uiState.state !== "idle";
885
+ const handleHover = (0, import_react5.useCallback)((_index) => {
886
+ }, []);
887
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
888
+ "div",
889
+ {
890
+ className,
891
+ "data-mentions-input": "",
892
+ "data-disabled": disabled ? "" : void 0,
893
+ ...comboboxAttrs(isExpanded, LISTBOX_ID2),
894
+ "aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
895
+ children: [
896
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react6.EditorContent, { editor }),
897
+ isExpanded && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
898
+ SuggestionList,
899
+ {
900
+ items: uiState.items,
901
+ activeIndex: uiState.activeIndex,
902
+ breadcrumbs: uiState.breadcrumbs,
903
+ loading: uiState.loading,
904
+ trigger: uiState.trigger,
905
+ clientRect: uiState.clientRect,
906
+ onSelect: (item) => actions.select(item),
907
+ onHover: handleHover,
908
+ onGoBack: actions.goBack,
909
+ renderItem
910
+ }
911
+ )
912
+ ]
913
+ }
914
+ );
915
+ }
916
+ );
891
917
  // Annotate the CommonJS export names for ESM import in node:
892
918
  0 && (module.exports = {
893
919
  MentionsInput,