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

mentionly

v1.2.0

Published

Lightweight Vue 3 mention input component for AI chat scenarios

Readme

mentionly

tests npm downloads

轻量级 Vue 3 mention 输入组件,专为 AI 聊天场景设计。零外部依赖(仅 peer depend Vue 3)。

在线演示 | English

特性

  • 多触发字符@#/ 或任意自定义字符
  • Headless + 开箱即用 — 核心逻辑在 useMention composable 中,同时提供带 UI 的 MentionInput 组件
  • 原子 mention 实体contenteditable="false" 的 span,不可分割、不可部分选中
  • 可自定义输出结构 — 支持 dataPart 转换函数或声明式 schema 映射
  • 异步数据源 — 支持防抖和竞态保护
  • 后端分页 — 滚动或键盘导航到底时自动加载下一页,数据源约定为 { offset, limit }
  • 命令模式/斜杠 命令触发回调,不插入实体
  • IME 兼容 — 正确处理中文/日文/韩文输入法
  • 序列化与反序列化getDataParts() 用于提交,setContent() 用于编辑已发消息
  • Teleport 下拉列表 — 下拉列表默认 teleport 到 <body>,避免被外层 overflow: hidden 裁剪
  • 光标跟随弹窗 — 可选 popupMode="cursor" 让弹窗跟随光标位置
  • 零依赖 — 仅需 Vue 3

安装

npm install mentionly
# 或
pnpm add mentionly
# 或
bun add mentionly

快速开始

开箱即用组件

<script setup>
import { MentionInput } from 'mentionly'
import 'mentionly/style.css'

const triggers = [
  {
    char: '@',
    items: [
      { id: '1', label: 'Project Alpha' },
      { id: '2', label: 'Project Beta' },
    ],
    dataPart: (item) => ({
      dataType: 'mentioned_ref',
      projectId: item.id,
      projectName: item.label,
    }),
  },
]
function onSubmit(dataParts) {
  console.log(dataParts)
  // [
  //   { type: 'text', text: '检查 ' },
  //   { type: 'data', dataType: 'mentioned_ref', projectId: '1', projectName: 'Project Alpha' },
  //   { type: 'text', text: ' 的状态' },
  // ]
}
</script>

<template>
  <MentionInput
    :triggers="triggers"
    placeholder="输入消息... 试试 @"
    @submit="onSubmit"
  />
</template>

Headless 模式

<script setup>
import { useMention } from 'mentionly'

const {
  editorRef, isOpen, filteredItems, activeIndex,
  select, handlers, getDataParts, clear, loading,
} = useMention({
  triggers: [
    {
      char: '@',
      items: (q) => projects.filter(p => p.label.includes(q)),
      dataPart: (item) => ({ dataType: 'mentioned_ref', projectId: item.id }),
    },
  ],
})

function send() {
  const parts = getDataParts()
  api.sendMessage(parts)
  clear()
}
</script>

<template>
  <div class="my-chat-input">
    <div ref="editorRef" contenteditable v-on="handlers" />
    <MyDropdown v-if="isOpen" :items="filteredItems" :active="activeIndex" :loading="loading" @select="select" />
    <button @click="send">发送</button>
  </div>
</template>

触发器配置

interface MentionTrigger {
  char: string                    // 触发字符
  mode?: 'inline' | 'command'     // 默认 'inline'
  items: MentionItem[]            // 静态数组
    | ((query: string, page?: { offset: number; limit: number })   // 或函数
        => MentionItemsResult | Promise<MentionItemsResult>)
  pagination?: { pageSize: number }  // 启用后端分页(仅对函数源生效)
  debounce?: number               // 异步防抖毫秒数,默认 0
  dataPart?: (item) => Record<string, any>  // 输出转换函数
  schema?: { type: string; mapping: Record<string, string> }  // 声明式映射
  onSelect?: (item) => void       // 命令模式回调
}

// 函数源可返回裸数组,或带显式 hasMore 的对象:
type MentionItemsResult = MentionItem[] | { items: MentionItem[]; hasMore?: boolean }

多触发器

const triggers = [
  { char: '@', items: projects, dataPart: (item) => ({ dataType: 'mentioned_ref', projectId: item.id }) },
  { char: '#', items: tags, schema: { type: 'tag_ref', mapping: { tagId: 'id', tagName: 'label' } } },
  { char: '/', mode: 'command', items: commands, onSelect: (item) => handleCommand(item) },
]

异步数据源

{
  char: '@',
  items: async (query) => {
    const res = await fetch(`/api/search?q=${query}`)
    return res.json()
  },
  debounce: 200,
}

后端分页(加载更多)

配置 pagination.pageSize 即可把函数源变成分页源。此时函数会收到第二个参数 { offset, limit },当用户滚动到列表底部、或键盘导航接近末尾时,下拉列表会自动加载下一页并追加结果。

{
  char: '@',
  pagination: { pageSize: 20 },
  items: async (query, page) => {
    const res = await fetch(`/api/users?q=${query}&offset=${page.offset}&limit=${page.limit}`)
    const users = await res.json()

    // 方式一 —— 返回裸数组;hasMore 按 `length >= limit` 推断
    return users

    // 方式二 —— 由后端显式告知是否还有下一页
    // return { items: users.list, hasMore: users.hasNext }
  },
}
  • 仅对函数源生效 —— 静态数组维持一次性全量渲染,行为不变。
  • 向后兼容 —— 不配置 pagination 时,旧的 (query) => items 约定完全不变(不会传入第二个参数)。
  • loading 对应首屏(替换列表),loadingMore 对应后续页(保留列表、底部显示指示器)。两者都由 useMention / #list 插槽暴露。

MentionInput Props

| Prop | 类型 | 默认值 | 说明 | |------|------|--------|------| | triggers | MentionTrigger[] | 必填 | 触发器配置 | | placeholder | string | '' | 占位文本 | | disabled | boolean | false | 是否禁用 | | maxHeight | string | '200px' | 输入框最大高度 | | submitOnEnter | boolean | true | 是否 Enter 键提交 | | onEnter | (e: KeyboardEvent) => void | - | Enter 时触发(提交前),可 e.preventDefault() 阻止提交/换行 | | popupMode | 'fixed' \| 'cursor' | 'fixed' | 弹窗定位模式 | | popupScrollBehavior | 'reposition' \| 'close' \| 'ignore' | 'reposition' | 滚动时弹窗处理方式 | | teleport | boolean | true | 是否将下拉列表 teleport 到 <body> |

事件

| 事件 | 载荷 | 说明 | |------|------|------| | submit | DataPart[] | 按 Enter 时触发 | | change | ContentPart[] | 内容变化时触发 |

插槽

| 插槽 | Props | 说明 | |------|-------|------| | #list | { items, activeIndex, select, loading, hasMore, loadingMore, loadMore } | 自定义下拉列表 | | #item | { item, active, select } | 自定义候选项渲染 | | #empty | { query } | 无匹配结果提示 | | #loading | {} | 加载中提示(首屏) | | #loading-more | {} | 加载下一页时的“加载更多”指示器 | | #actions | { submit, clear, isEmpty } | 自定义底部操作栏 | | #inner-actions | { submit, clear, isEmpty } | 编辑器内部、输入框下方(如发送按钮) | | #default | { submit, clear, isEmpty, focus, getParts } | 最外层底部,自由放置内容 |

暴露方法

通过 template ref 访问:

const inputRef = ref()

inputRef.value.getParts()       // ContentPart[]
inputRef.value.getDataParts()   // DataPart[]
inputRef.value.getPlainText()   // string
inputRef.value.clear()
inputRef.value.setContent(parts)  // 从 ContentPart[] 恢复内容
inputRef.value.insertMention({
  id: 'ctx-1',
  label: '选中内容',
  dataPart: (item) => ({ dataType: 'selection_ref', sourceId: item.id, title: item.label }),
})
inputRef.value.focus()

insertMention() 支持插入未注册到任何 trigger 列表中的自定义原子节点。

反序列化

编辑已发送的消息:

<script setup>
const inputRef = ref()
const savedParts = [
  { type: 'text', content: '请检查 ' },
  { type: 'mention', triggeredBy: '@', id: '1', label: 'Project Alpha' },
  { type: 'text', content: ' 的部署状态' },
]
onMounted(() => inputRef.value.setContent(savedParts))
</script>

<template>
  <MentionInput ref="inputRef" :triggers="triggers" @submit="onUpdate" />
</template>

弹窗模式

<!-- 固定模式:下拉列表在编辑器上方(默认) -->
<MentionInput :triggers="triggers" popup-mode="fixed" />

<!-- 光标模式:下拉列表跟随光标位置 -->
<MentionInput :triggers="triggers" popup-mode="cursor" />

许可

MIT