npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@relevaince/mentions

v0.6.0

Published

Structured mention engine for Relevaince OMNI chat input

Downloads

885

Readme

@relevaince/mentions

A structured mention engine for React. Built on Tiptap/ProseMirror internally, exposed as a single component externally.

This is not a simple @mention dropdown. It is a resource-addressing language inside text — typed entity tokens that look like text but carry structured data. Think Slack, Notion, or Linear mentions.

Features

  • Multiple trigger characters@, #, :, or any custom trigger
  • Nested suggestions — drill into workspaces, then pick a file inside
  • Cross-group searchsearchAll finds items across every level with a single query
  • Nested search — search input inside the dropdown to filter children in real time
  • Async providers — fetch suggestions from any API
  • Debounced fetching — per-provider debounceMs prevents API floods
  • Structured output — returns { markdown, tokens, plainText } on every change
  • Markdown serialization@[label](id) token syntax for storage and LLM context
  • Markdown parsingextractFromMarkdown() returns tokens + plain text from a markdown string
  • Headless styling — zero bundled CSS, style via data-* attributes with Tailwind or plain CSS
  • Accessible — full ARIA combobox pattern with keyboard navigation
  • SSR compatible — safe for Next.js with "use client" directive
  • Grouped suggestions — section items under headers (e.g. "Active", "Pending")
  • Recently used items — show recent mentions when the query is empty
  • Empty state — customizable "No results" message
  • Mention lifecycleonMentionAdd / onMentionRemove callbacks
  • Mention interactiononMentionClick / onMentionHover handlers on chips
  • Mention validation — mark stale/invalid mentions with validateMention
  • Controlled value — reactive value prop for external content updates
  • Auto-resizeminHeight / maxHeight for growing editor
  • Submit key customization — choose Enter, Cmd+Enter, or none
  • Edge-aware popover — flips above when near viewport bottom
  • Portal support — render the dropdown into a custom container
  • Tab to complete — Tab selects the active suggestion
  • Trigger gatingallowTrigger to conditionally suppress the dropdown
  • Streaming support — stream AI-generated text into the editor with automatic mention parsing
  • Multi-instance — unique ARIA IDs per component instance

Install

npm install @relevaince/mentions

Peer dependencies:

npm install react react-dom

Quick start

import { MentionsInput, type MentionProvider } from "@relevaince/mentions";

const workspaceProvider: MentionProvider = {
  trigger: "@",
  name: "Workspaces",
  debounceMs: 200,
  async getRootItems(query) {
    const res = await fetch(`/api/workspaces?q=${query}`);
    return res.json();
  },
  async getChildren(parent, query) {
    const res = await fetch(`/api/workspaces/${parent.id}/files?q=${query}`);
    return res.json();
  },
  async getRecentItems() {
    const res = await fetch("/api/workspaces/recent");
    return res.json();
  },
};

function Chat() {
  return (
    <MentionsInput
      providers={[workspaceProvider]}
      onChange={(output) => {
        console.log(output.markdown);  // "Summarize @[Marketing](ws_123)"
        console.log(output.tokens);    // [{ id: "ws_123", type: "workspace", label: "Marketing" }]
        console.log(output.plainText); // "Summarize Marketing"
      }}
      onSubmit={(output) => sendMessage(output)}
      onMentionAdd={(token) => console.log("Added:", token)}
      onMentionRemove={(token) => console.log("Removed:", token)}
      placeholder="Ask anything..."
      minHeight={40}
      maxHeight={200}
    />
  );
}

Core concepts

MentionToken

The fundamental data model. Every mention in the editor is a typed token:

type MentionToken = {
  id: string;       // unique entity identifier
  type: string;     // "workspace" | "contract" | "file" | "web" | custom
  label: string;    // display text
  data?: unknown;   // optional payload, passed through untouched
};

MentionProvider

Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down, cross-group search, debouncing, and recent items:

type MentionProvider = {
  trigger: string;
  name: string;
  getRootItems: (query: string) => Promise<MentionItem[]>;
  getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
  searchAll?: (query: string) => Promise<MentionItem[]>;
  debounceMs?: number;
  getRecentItems?: () => Promise<MentionItem[]>;
};

| Property | Type | Required | Description | |----------|------|----------|-------------| | trigger | string | Yes | Character(s) that activate this provider | | name | string | Yes | Human-readable name for ARIA labels | | getRootItems | (query) => Promise<MentionItem[]> | Yes | Top-level suggestions | | getChildren | (parent, query) => Promise<MentionItem[]> | No | Child suggestions for drill-down | | searchAll | (query) => Promise<MentionItem[]> | No | Flat search across all levels | | debounceMs | number | No | Delay before fetching (prevents API floods) | | getRecentItems | () => Promise<MentionItem[]> | No | Recently used items, shown on empty query |

MentionItem

Items returned by providers:

type MentionItem = {
  id: string;
  type: string;
  label: string;
  icon?: ReactNode;
  description?: string;
  hasChildren?: boolean;
  data?: unknown;
  rootLabel?: string;
  group?: string;          // group items under section headers
};

Set group on items to render them under section headers in the dropdown (e.g. "Active", "Pending", "Recent").

MentionsOutput

Structured output returned on every change and submit:

type MentionsOutput = {
  markdown: string;
  tokens: MentionToken[];
  plainText: string;
};

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | string | — | Controlled markdown content (reactive — updates editor on change) | | providers | MentionProvider[] | required | Suggestion providers, one per trigger | | onChange | (output: MentionsOutput) => void | — | Called on every content change | | onSubmit | (output: MentionsOutput) => void | — | Called on submit shortcut | | placeholder | string | "Type a message..." | Placeholder text | | autoFocus | boolean | false | Focus editor on mount | | disabled | boolean | false | Disable editing | | className | string | — | CSS class on the wrapper | | maxLength | number | — | Max plain text character count | | clearOnSubmit | boolean | true | Auto-clear after onSubmit | | submitKey | "enter" \| "mod+enter" \| "none" | "enter" | Which key combo triggers submit | | minHeight | number | — | Minimum editor height in px | | maxHeight | number | — | Maximum editor height in px (enables scroll) | | onFocus | () => void | — | Called when editor gains focus | | onBlur | () => void | — | Called when editor loses focus | | onMentionAdd | (token: MentionToken) => void | — | Called when a mention is inserted | | onMentionRemove | (token: MentionToken) => void | — | Called when a mention is deleted | | onMentionClick | (token: MentionToken, event: MouseEvent) => void | — | Called when a mention chip is clicked | | onMentionHover | (token: MentionToken) => ReactNode | — | Return content for a hover tooltip | | renderItem | (item, depth) => ReactNode | — | Custom suggestion item renderer | | renderChip | (token) => ReactNode | — | Custom inline mention chip renderer | | renderEmpty | (query: string) => ReactNode | — | Custom empty state (no results) | | renderLoading | () => ReactNode | — | Custom loading indicator | | renderGroupHeader | (group: string) => ReactNode | — | Custom section header renderer | | allowTrigger | (trigger, { textBefore }) => boolean | — | Conditionally suppress the dropdown | | validateMention | (token) => boolean \| Promise<boolean> | — | Validate mentions; invalid ones get data-mention-invalid | | portalContainer | HTMLElement | — | Render dropdown into a custom DOM node | | streaming | boolean | false | Signals the editor is receiving streamed content (suppresses triggers, blocks user input, throttles onChange) | | onStreamingComplete | (output: MentionsOutput) => void | — | Fires once when streaming transitions from true to false with the final output |

Imperative ref API

MentionsInput supports forwardRef for programmatic control:

import { useRef } from "react";
import { MentionsInput, type MentionsInputHandle } from "@relevaince/mentions";

function Chat() {
  const ref = useRef<MentionsInputHandle>(null);

  return (
    <>
      <MentionsInput ref={ref} providers={providers} />

      <button onClick={() => ref.current?.clear()}>Clear</button>
      <button onClick={() => {
        ref.current?.setContent("Summarize @[NDA](contract:c_1) risks");
        ref.current?.focus();
      }}>Use Prompt</button>
      <button onClick={() => {
        const output = ref.current?.getOutput();
        console.log(output?.tokens);
      }}>Read Output</button>
    </>
  );
}

Handle methods

| Method | Signature | Description | |--------|-----------|-------------| | clear | () => void | Clears all editor content | | setContent | (markdown: string) => void | Replaces content with a markdown string (mention tokens are parsed) | | appendText | (text: string) => void | Appends plain text at the end (no mention parsing — use for plain-text streaming) | | focus | () => void | Focuses the editor and places the cursor at the end | | getOutput | () => MentionsOutput \| null | Reads the current structured output without waiting for onChange |

Streaming

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.

const ref = useRef<MentionsInputHandle>(null);
const [isStreaming, setIsStreaming] = useState(false);

async function enhancePrompt() {
  setIsStreaming(true);
  let accumulated = "";

  const stream = await fetchEnhancedPrompt(currentPrompt);
  for await (const chunk of stream) {
    accumulated += chunk;
    ref.current?.setContent(accumulated);
  }

  setIsStreaming(false);
}

<MentionsInput
  ref={ref}
  streaming={isStreaming}
  providers={providers}
  onChange={handleChange}
  onStreamingComplete={(output) => {
    console.log("Final tokens:", output.tokens);
  }}
/>

How it works:

  1. Set streaming={true} — the editor enters streaming mode
  2. On each chunk, accumulate the full text and call ref.current.setContent(accumulated)
  3. Incomplete mention tokens (e.g. @[NDA) render as plain text until the full @[label](id) syntax is received, then snap into mention chips
  4. Set streaming={false} — the editor exits streaming mode, fires a final onChange and onStreamingComplete

For plain-text-only streaming (no mention syntax in chunks), use ref.current.appendText(chunk) instead for better performance.

Keyboard shortcuts

| Key | Context | Action | |-----|---------|--------| | | Suggestions open | Navigate suggestions | | Enter | Suggestions open | Select / drill into children | | Tab | Suggestions open | Select the active suggestion | | | Suggestions open | Drill into children (if item has children) | | | Nested level | Go back one level | | Backspace | Nested level, empty search | Go back one level | | Escape | Suggestions open | Close suggestions | | Enter | Editor (default submitKey) | Submit message | | Shift+Enter | Editor (default submitKey) | New line | | Cmd/Ctrl+Enter | Editor | Submit message |

Markdown format

Mentions serialize to a compact token syntax:

@[Marketing Workspace](ws_123) summarize the latest files
@Marketing[Q4 Strategy.pdf](file:file_1) review this document

Use the standalone helpers for server-side processing:

import {
  serializeToMarkdown,
  parseFromMarkdown,
  extractFromMarkdown,
} from "@relevaince/mentions";

const doc = parseFromMarkdown("Check @[NDA](contract:c_44) for risks");
const md = serializeToMarkdown(doc);
const { tokens, plainText } = extractFromMarkdown(
  "Summarize @Marketing[Q4 Strategy.pdf](file:file_1) for the team"
);

Styling

Zero bundled CSS. Every element exposes data-* attributes:

/* Mention chips */
[data-mention]                         { /* base chip */ }
[data-mention][data-type="workspace"]  { /* workspace chip */ }
[data-mention][data-type="contract"]   { /* contract chip */ }
[data-mention-clickable]               { /* clickable chip (when onMentionClick set) */ }
[data-mention-invalid]                 { /* invalid/stale mention */ }
[data-mention-tooltip]                 { /* hover tooltip container */ }

/* Suggestion popover */
[data-suggestions]                     { /* popover wrapper */ }
[data-suggestions-position="above"]    { /* when popover flips above */ }
[data-suggestions-position="below"]    { /* when popover is below */ }
[data-suggestion-item]                 { /* each item */ }
[data-suggestion-item-active]          { /* highlighted item */ }
[data-suggestion-group-header]         { /* group section header */ }
[data-suggestion-empty]                { /* "no results" state */ }
[data-suggestion-loading]              { /* loading indicator */ }
[data-suggestion-breadcrumb]           { /* breadcrumb bar (nested) */ }
[data-suggestion-search]               { /* search input wrapper */ }
[data-suggestion-search-input]         { /* search input field */ }

Example with Tailwind:

[data-mention] {
  @apply bg-blue-500/10 text-blue-600 rounded px-1 py-0.5 font-medium text-sm;
}

[data-suggestions] {
  @apply bg-white border border-neutral-200 rounded-lg shadow-lg
         min-w-[240px] max-h-[280px] overflow-y-auto py-1;
}

[data-suggestion-item] {
  @apply flex items-center gap-2 px-3 py-1.5 cursor-pointer text-sm;
}

[data-suggestion-item-active] {
  @apply bg-neutral-100;
}

[data-suggestion-group-header] {
  @apply px-3 py-1 text-[10px] font-semibold text-neutral-400 uppercase tracking-wider;
}

[data-suggestion-empty] {
  @apply px-3 py-2 text-xs text-neutral-400 italic;
}

[data-mention-invalid] {
  @apply opacity-50 line-through;
}

Architecture

<MentionsInput>
  ├── Tiptap Editor (ProseMirror)
  │     ├── MentionNode Extension    — inline atom nodes with id/label/type + click/hover
  │     ├── Suggestion Plugin        — multi-trigger detection + allowTrigger gating
  │     ├── Enter/Submit Extension   — configurable submit key (Enter/Mod+Enter/none)
  │     ├── Mention Remove Detector  — transaction-based onMentionRemove
  │     └── Markdown Parser/Serializer — doc ↔ @[label](id) + extractFromMarkdown
  └── SuggestionList (React)         — headless popover with ARIA, groups, edge-aware positioning

Testing

npm test           # run all tests
npm run test:watch # watch mode

Development

npm install        # install dependencies
npm run build      # build the library
npm test           # run tests

cd demo && npm install && npx vite  # run the demo app

License

MIT