@relevaince/mentions 0.1.0 → 0.2.1

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
@@ -44,6 +44,7 @@ var import_react6 = require("@tiptap/react");
44
44
  var import_react = require("react");
45
45
  var import_react2 = require("@tiptap/react");
46
46
  var import_starter_kit = __toESM(require("@tiptap/starter-kit"));
47
+ var import_extension_placeholder = __toESM(require("@tiptap/extension-placeholder"));
47
48
  var import_core3 = require("@tiptap/core");
48
49
 
49
50
  // src/core/mentionExtension.ts
@@ -369,19 +370,25 @@ function parseLine(line) {
369
370
  }
370
371
 
371
372
  // src/hooks/useMentionsEditor.ts
372
- function createSubmitExtension(onSubmitRef) {
373
+ function buildOutput(editor) {
374
+ const json = editor.getJSON();
375
+ return {
376
+ markdown: serializeToMarkdown(json),
377
+ tokens: extractTokens(json),
378
+ plainText: extractPlainText(json)
379
+ };
380
+ }
381
+ function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
373
382
  return import_core3.Extension.create({
374
383
  name: "submitShortcut",
375
384
  addKeyboardShortcuts() {
376
385
  return {
377
386
  "Mod-Enter": () => {
378
387
  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
- });
388
+ onSubmitRef.current(buildOutput(this.editor));
389
+ if (clearOnSubmitRef.current) {
390
+ this.editor.commands.clearContent(true);
391
+ }
385
392
  }
386
393
  return true;
387
394
  }
@@ -389,7 +396,7 @@ function createSubmitExtension(onSubmitRef) {
389
396
  }
390
397
  });
391
398
  }
392
- function createEnterExtension(onSubmitRef) {
399
+ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
393
400
  return import_core3.Extension.create({
394
401
  name: "enterSubmit",
395
402
  priority: 50,
@@ -397,12 +404,10 @@ function createEnterExtension(onSubmitRef) {
397
404
  return {
398
405
  Enter: () => {
399
406
  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
- });
407
+ onSubmitRef.current(buildOutput(this.editor));
408
+ if (clearOnSubmitRef.current) {
409
+ this.editor.commands.clearContent(true);
410
+ }
406
411
  }
407
412
  return true;
408
413
  }
@@ -415,6 +420,7 @@ function useMentionsEditor({
415
420
  value,
416
421
  onChange,
417
422
  onSubmit,
423
+ clearOnSubmit = true,
418
424
  placeholder,
419
425
  autoFocus = false,
420
426
  editable = true,
@@ -424,6 +430,8 @@ function useMentionsEditor({
424
430
  onChangeRef.current = onChange;
425
431
  const onSubmitRef = (0, import_react.useRef)(onSubmit);
426
432
  onSubmitRef.current = onSubmit;
433
+ const clearOnSubmitRef = (0, import_react.useRef)(clearOnSubmit);
434
+ clearOnSubmitRef.current = clearOnSubmit;
427
435
  const initialContent = (0, import_react.useMemo)(() => {
428
436
  if (!value) return void 0;
429
437
  return parseFromMarkdown(value);
@@ -439,8 +447,14 @@ function useMentionsEditor({
439
447
  // eslint-disable-next-line react-hooks/exhaustive-deps
440
448
  [triggersKey]
441
449
  );
442
- const submitExt = (0, import_react.useMemo)(() => createSubmitExtension(onSubmitRef), []);
443
- const enterExt = (0, import_react.useMemo)(() => createEnterExtension(onSubmitRef), []);
450
+ const submitExt = (0, import_react.useMemo)(
451
+ () => createSubmitExtension(onSubmitRef, clearOnSubmitRef),
452
+ []
453
+ );
454
+ const enterExt = (0, import_react.useMemo)(
455
+ () => createEnterExtension(onSubmitRef, clearOnSubmitRef),
456
+ []
457
+ );
444
458
  const editor = (0, import_react2.useEditor)({
445
459
  extensions: [
446
460
  import_starter_kit.default.configure({
@@ -452,6 +466,9 @@ function useMentionsEditor({
452
466
  listItem: false,
453
467
  horizontalRule: false
454
468
  }),
469
+ import_extension_placeholder.default.configure({
470
+ placeholder: placeholder ?? "Type a message..."
471
+ }),
455
472
  MentionNode,
456
473
  suggestionExtension,
457
474
  submitExt,
@@ -462,17 +479,11 @@ function useMentionsEditor({
462
479
  editable,
463
480
  editorProps: {
464
481
  attributes: {
465
- "data-placeholder": placeholder ?? "",
466
482
  class: "mentions-editor"
467
483
  }
468
484
  },
469
485
  onUpdate: ({ editor: editor2 }) => {
470
- const json = editor2.getJSON();
471
- onChangeRef.current?.({
472
- markdown: serializeToMarkdown(json),
473
- tokens: extractTokens(json),
474
- plainText: extractPlainText(json)
475
- });
486
+ onChangeRef.current?.(buildOutput(editor2));
476
487
  }
477
488
  });
478
489
  (0, import_react.useEffect)(() => {
@@ -480,16 +491,25 @@ function useMentionsEditor({
480
491
  editor.setEditable(editable);
481
492
  }
482
493
  }, [editor, editable]);
494
+ const clear = (0, import_react.useCallback)(() => {
495
+ editor?.commands.clearContent(true);
496
+ }, [editor]);
497
+ const setContent = (0, import_react.useCallback)(
498
+ (markdown) => {
499
+ if (!editor) return;
500
+ const doc = parseFromMarkdown(markdown);
501
+ editor.commands.setContent(doc);
502
+ },
503
+ [editor]
504
+ );
505
+ const focus = (0, import_react.useCallback)(() => {
506
+ editor?.commands.focus("end");
507
+ }, [editor]);
483
508
  const getOutput = (0, import_react.useCallback)(() => {
484
509
  if (!editor) return null;
485
- const json = editor.getJSON();
486
- return {
487
- markdown: serializeToMarkdown(json),
488
- tokens: extractTokens(json),
489
- plainText: extractPlainText(json)
490
- };
510
+ return buildOutput(editor);
491
511
  }, [editor]);
492
- return { editor, getOutput };
512
+ return { editor, getOutput, clear, setContent, focus };
493
513
  }
494
514
 
495
515
  // src/hooks/useSuggestion.ts
@@ -832,62 +852,71 @@ function usePopoverPosition(clientRect) {
832
852
  // src/components/MentionsInput.tsx
833
853
  var import_jsx_runtime2 = require("react/jsx-runtime");
834
854
  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,
855
+ var MentionsInput = (0, import_react5.forwardRef)(
856
+ function MentionsInput2({
851
857
  value,
858
+ providers,
852
859
  onChange,
860
+ placeholder = "Type a message...",
861
+ autoFocus = false,
862
+ disabled = false,
863
+ className,
853
864
  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
- }
865
+ clearOnSubmit = true,
866
+ maxLength,
867
+ renderItem,
868
+ renderChip
869
+ }, ref) {
870
+ const { uiState, actions, callbacksRef } = useSuggestion(providers);
871
+ const { editor, clear, setContent, focus } = useMentionsEditor({
872
+ providers,
873
+ value,
874
+ onChange,
875
+ onSubmit,
876
+ clearOnSubmit,
877
+ placeholder,
878
+ autoFocus,
879
+ editable: !disabled,
880
+ callbacksRef
881
+ });
882
+ (0, import_react5.useImperativeHandle)(
883
+ ref,
884
+ () => ({ clear, setContent, focus }),
885
+ [clear, setContent, focus]
886
+ );
887
+ const isExpanded = uiState.state !== "idle";
888
+ const handleHover = (0, import_react5.useCallback)((_index) => {
889
+ }, []);
890
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
891
+ "div",
892
+ {
893
+ className,
894
+ "data-mentions-input": "",
895
+ "data-disabled": disabled ? "" : void 0,
896
+ ...comboboxAttrs(isExpanded, LISTBOX_ID2),
897
+ "aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
898
+ children: [
899
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react6.EditorContent, { editor }),
900
+ isExpanded && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
901
+ SuggestionList,
902
+ {
903
+ items: uiState.items,
904
+ activeIndex: uiState.activeIndex,
905
+ breadcrumbs: uiState.breadcrumbs,
906
+ loading: uiState.loading,
907
+ trigger: uiState.trigger,
908
+ clientRect: uiState.clientRect,
909
+ onSelect: (item) => actions.select(item),
910
+ onHover: handleHover,
911
+ onGoBack: actions.goBack,
912
+ renderItem
913
+ }
914
+ )
915
+ ]
916
+ }
917
+ );
918
+ }
919
+ );
891
920
  // Annotate the CommonJS export names for ESM import in node:
892
921
  0 && (module.exports = {
893
922
  MentionsInput,