@relevaince/mentions 0.1.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 ADDED
@@ -0,0 +1,245 @@
1
+ # @relevaince/mentions
2
+
3
+ A structured mention engine for React. Built on Tiptap/ProseMirror internally, exposed as a single component externally.
4
+
5
+ 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.
6
+
7
+ ## Features
8
+
9
+ - **Multiple trigger characters** — `@`, `#`, `:`, or any custom trigger
10
+ - **Nested suggestions** — drill into workspaces, then pick a file inside
11
+ - **Async providers** — fetch suggestions from any API
12
+ - **Structured output** — returns `{ markdown, tokens, plainText }` on every change
13
+ - **Markdown serialization** — `@[label](id)` token syntax for storage and LLM context
14
+ - **Headless styling** — zero bundled CSS, style via `data-*` attributes with Tailwind or plain CSS
15
+ - **Accessible** — full ARIA combobox pattern with keyboard navigation
16
+ - **SSR compatible** — safe for Next.js with `"use client"` directive
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install @relevaince/mentions
22
+ ```
23
+
24
+ Peer dependencies:
25
+
26
+ ```bash
27
+ npm install react react-dom
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ ```tsx
33
+ import { MentionsInput, type MentionProvider } from "@relevaince/mentions";
34
+
35
+ const workspaceProvider: MentionProvider = {
36
+ trigger: "@",
37
+ name: "Workspaces",
38
+ async getRootItems(query) {
39
+ const res = await fetch(`/api/workspaces?q=${query}`);
40
+ return res.json();
41
+ },
42
+ async getChildren(parent, query) {
43
+ const res = await fetch(`/api/workspaces/${parent.id}/files?q=${query}`);
44
+ return res.json();
45
+ },
46
+ };
47
+
48
+ function Chat() {
49
+ return (
50
+ <MentionsInput
51
+ providers={[workspaceProvider]}
52
+ onChange={(output) => {
53
+ console.log(output.markdown); // "Summarize @[Marketing](ws_123)"
54
+ console.log(output.tokens); // [{ id: "ws_123", type: "workspace", label: "Marketing" }]
55
+ console.log(output.plainText); // "Summarize Marketing"
56
+ }}
57
+ onSubmit={(output) => sendMessage(output)}
58
+ placeholder="Ask anything..."
59
+ />
60
+ );
61
+ }
62
+ ```
63
+
64
+ ## Core concepts
65
+
66
+ ### MentionToken
67
+
68
+ The fundamental data model. Every mention in the editor is a typed token:
69
+
70
+ ```ts
71
+ type MentionToken = {
72
+ id: string; // unique entity identifier
73
+ type: string; // "workspace" | "contract" | "file" | "web" | custom
74
+ label: string; // display text
75
+ data?: unknown; // optional payload, passed through untouched
76
+ };
77
+ ```
78
+
79
+ ### MentionProvider
80
+
81
+ Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down:
82
+
83
+ ```ts
84
+ type MentionProvider = {
85
+ trigger: string; // "@", "#", ":"
86
+ name: string; // human label
87
+ getRootItems: (query: string) => Promise<MentionItem[]>;
88
+ getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
89
+ };
90
+ ```
91
+
92
+ ### MentionItem
93
+
94
+ Items returned by providers:
95
+
96
+ ```ts
97
+ type MentionItem = {
98
+ id: string;
99
+ type: string;
100
+ label: string;
101
+ icon?: ReactNode; // rendered before the label
102
+ description?: string; // secondary text
103
+ hasChildren?: boolean; // when true, selecting drills into a child level
104
+ data?: unknown;
105
+ };
106
+ ```
107
+
108
+ ### MentionsOutput
109
+
110
+ Structured output returned on every change and submit:
111
+
112
+ ```ts
113
+ type MentionsOutput = {
114
+ markdown: string; // "@[Marketing](ws_123) summarize files"
115
+ tokens: MentionToken[]; // [{ id: "ws_123", type: "workspace", label: "Marketing" }]
116
+ plainText: string; // "Marketing summarize files"
117
+ };
118
+ ```
119
+
120
+ ## Props
121
+
122
+ | Prop | Type | Default | Description |
123
+ |------|------|---------|-------------|
124
+ | `value` | `string` | — | Initial markdown content with `@[label](id)` tokens |
125
+ | `providers` | `MentionProvider[]` | **required** | Suggestion providers, one per trigger |
126
+ | `onChange` | `(output: MentionsOutput) => void` | — | Called on every content change |
127
+ | `onSubmit` | `(output: MentionsOutput) => void` | — | Called on Enter / Cmd+Enter |
128
+ | `placeholder` | `string` | `"Type a message..."` | Placeholder text |
129
+ | `autoFocus` | `boolean` | `false` | Focus editor on mount |
130
+ | `disabled` | `boolean` | `false` | Disable editing |
131
+ | `className` | `string` | — | CSS class on the wrapper |
132
+ | `maxLength` | `number` | — | Max plain text character count |
133
+ | `renderItem` | `(item, depth) => ReactNode` | — | Custom suggestion item renderer |
134
+ | `renderChip` | `(token) => ReactNode` | — | Custom inline mention chip renderer |
135
+
136
+ ## Nested mentions
137
+
138
+ The killer feature. When a `MentionItem` has `hasChildren: true`, selecting it drills into the next level using `provider.getChildren()`:
139
+
140
+ ```
141
+ @workspace → choose workspace → choose file inside workspace
142
+ @contract → choose contract → choose clause
143
+ :web → choose provider → type query
144
+ ```
145
+
146
+ Keyboard navigation:
147
+
148
+ | Key | Action |
149
+ |-----|--------|
150
+ | `↑` `↓` | Navigate suggestions |
151
+ | `Enter` `→` | Select / drill into children |
152
+ | `←` `Backspace` | Go back one level |
153
+ | `Escape` | Close suggestions |
154
+
155
+ ## Markdown format
156
+
157
+ Mentions serialize to a compact token syntax:
158
+
159
+ ```
160
+ @[Marketing Workspace](ws_123) summarize the latest files
161
+ ```
162
+
163
+ Use the standalone helpers for server-side processing:
164
+
165
+ ```ts
166
+ import { serializeToMarkdown, parseFromMarkdown } from "@relevaince/mentions";
167
+
168
+ // Parse markdown into a Tiptap-compatible JSON document
169
+ const doc = parseFromMarkdown("Check @[NDA](contract:c_44) for risks");
170
+
171
+ // Serialize a Tiptap JSON document back to markdown
172
+ const md = serializeToMarkdown(doc);
173
+ ```
174
+
175
+ ## Styling
176
+
177
+ Zero bundled CSS. Every element exposes `data-*` attributes for styling:
178
+
179
+ ```css
180
+ /* Mention chips inside the editor */
181
+ [data-mention] { /* base chip */ }
182
+ [data-mention][data-type="workspace"] { /* workspace chip */ }
183
+ [data-mention][data-type="contract"] { /* contract chip */ }
184
+
185
+ /* Suggestion popover */
186
+ [data-suggestions] { /* popover wrapper */ }
187
+ [data-suggestion-item] { /* each item */ }
188
+ [data-suggestion-item-active] { /* highlighted item */ }
189
+ [data-suggestion-breadcrumb] { /* breadcrumb bar (nested) */ }
190
+ [data-suggestion-loading] { /* loading indicator */ }
191
+ ```
192
+
193
+ Example with Tailwind:
194
+
195
+ ```css
196
+ [data-mention] {
197
+ @apply bg-blue-500/10 text-blue-600 rounded px-1 py-0.5 font-medium text-sm;
198
+ }
199
+
200
+ [data-suggestions] {
201
+ @apply bg-white border border-neutral-200 rounded-lg shadow-lg
202
+ min-w-[240px] max-h-[280px] overflow-y-auto py-1;
203
+ }
204
+
205
+ [data-suggestion-item] {
206
+ @apply flex items-center gap-2 px-3 py-1.5 cursor-pointer text-sm;
207
+ }
208
+
209
+ [data-suggestion-item-active] {
210
+ @apply bg-neutral-100;
211
+ }
212
+ ```
213
+
214
+ ## Architecture
215
+
216
+ ```
217
+ <MentionsInput>
218
+ ├── Tiptap Editor (ProseMirror)
219
+ │ ├── MentionNode Extension — inline atom nodes with id/label/type
220
+ │ ├── Suggestion Plugin — multi-trigger detection + popover callbacks
221
+ │ ├── Submit Extension — Enter / Cmd+Enter handling
222
+ │ └── Markdown Serializer — doc ↔ @[label](id) format
223
+ └── SuggestionList (React) — headless popover with ARIA
224
+ ```
225
+
226
+ Consumers interact with a single React component. Tiptap and ProseMirror are internal implementation details.
227
+
228
+ ## Development
229
+
230
+ ```bash
231
+ # Install dependencies
232
+ npm install
233
+
234
+ # Build the library
235
+ npm run build
236
+
237
+ # Run the demo app
238
+ cd demo && npm install && npx vite
239
+ ```
240
+
241
+ The demo app registers mock providers for workspaces, contracts, and web search. It displays live structured output below the editor.
242
+
243
+ ## License
244
+
245
+ MIT
@@ -0,0 +1,127 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { JSONContent } from '@tiptap/core';
4
+
5
+ /**
6
+ * A single suggestion item returned by a provider.
7
+ */
8
+ type MentionItem = {
9
+ /** Unique identifier for this item. */
10
+ id: string;
11
+ /** Entity kind — maps to `MentionToken.type` on insertion. */
12
+ type: string;
13
+ /** Display label shown in the suggestion list. */
14
+ label: string;
15
+ /** Optional icon rendered before the label. */
16
+ icon?: ReactNode;
17
+ /** Optional secondary text. */
18
+ description?: string;
19
+ /** When `true`, selecting this item drills into a child level instead of inserting. */
20
+ hasChildren?: boolean;
21
+ /** Optional payload carried through to `MentionToken.data`. */
22
+ data?: unknown;
23
+ };
24
+ /**
25
+ * Hierarchical suggestion source registered per trigger character.
26
+ *
27
+ * Consumers provide one provider per trigger (e.g. "@", "#", ":").
28
+ * The engine calls `getRootItems` first, then `getChildren` when
29
+ * the user drills into a nested level.
30
+ */
31
+ type MentionProvider = {
32
+ /** The character(s) that activate this provider — e.g. "@", "#", ":". */
33
+ trigger: string;
34
+ /** Human-readable name for this provider (used in ARIA labels). */
35
+ name: string;
36
+ /** Fetch top-level suggestions for the current query. */
37
+ getRootItems: (query: string) => Promise<MentionItem[]>;
38
+ /** Fetch child suggestions when the user drills into `parent`. */
39
+ getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
40
+ };
41
+
42
+ /**
43
+ * Core data model for a mention entity embedded in text.
44
+ *
45
+ * Stored as an inline ProseMirror node — the rendered text
46
+ * is derived from `label`, but the underlying identity is `id` + `type`.
47
+ */
48
+ type MentionToken = {
49
+ /** Unique identifier for the referenced entity. */
50
+ id: string;
51
+ /** Entity kind — e.g. "workspace", "contract", "file", "web", or any custom string. */
52
+ type: string;
53
+ /** Human-readable display label. */
54
+ label: string;
55
+ /** Optional full entity payload — opaque to the library, passed through untouched. */
56
+ data?: unknown;
57
+ };
58
+
59
+ /**
60
+ * Structured output returned on every editor change.
61
+ *
62
+ * Gives consumers three representations of the same content:
63
+ * - `markdown` — serialised with `@[label](id)` tokens, for storage / LLM context.
64
+ * - `tokens` — flat array of every mention in document order.
65
+ * - `plainText` — labels inlined, for display / search indexing.
66
+ */
67
+ type MentionsOutput = {
68
+ /** Markdown string with mention tokens encoded as `@[label](id)`. */
69
+ markdown: string;
70
+ /** Ordered array of all mention tokens in the document. */
71
+ tokens: MentionToken[];
72
+ /** Plain text with mention labels substituted inline. */
73
+ plainText: string;
74
+ };
75
+
76
+ /**
77
+ * Props for the public `<MentionsInput>` component.
78
+ */
79
+ type MentionsInputProps = {
80
+ /** Initial content as a markdown string (with `@[label](id)` tokens). */
81
+ value?: string;
82
+ /** Suggestion providers — one per trigger character. */
83
+ providers: MentionProvider[];
84
+ /** Called on every content change with the structured output. */
85
+ onChange?: (output: MentionsOutput) => void;
86
+ /** Placeholder text shown when the editor is empty. */
87
+ placeholder?: string;
88
+ /** Focus the editor on mount. */
89
+ autoFocus?: boolean;
90
+ /** Disable editing. */
91
+ disabled?: boolean;
92
+ /** CSS class applied to the outermost wrapper. */
93
+ className?: string;
94
+ /** Called when the user presses Enter (or Cmd+Enter). */
95
+ onSubmit?: (output: MentionsOutput) => void;
96
+ /** Maximum character count (plain text length). */
97
+ maxLength?: number;
98
+ /** Custom renderer for suggestion list items. */
99
+ renderItem?: (item: MentionItem, depth: number) => ReactNode;
100
+ /** Custom renderer for inline mention chips. */
101
+ renderChip?: (token: MentionToken) => ReactNode;
102
+ };
103
+
104
+ /**
105
+ * `<MentionsInput>` — the single public component.
106
+ *
107
+ * A structured text editor with typed entity tokens.
108
+ * Consumers register `providers` for each trigger character,
109
+ * and receive structured output via `onChange` and `onSubmit`.
110
+ */
111
+ declare function MentionsInput({ value, providers, onChange, placeholder, autoFocus, disabled, className, onSubmit, maxLength, renderItem, renderChip, }: MentionsInputProps): react_jsx_runtime.JSX.Element;
112
+
113
+ /**
114
+ * Serialize a Tiptap JSON document to a markdown string.
115
+ *
116
+ * Mention nodes are encoded as `@[label](id)` tokens.
117
+ * All other text passes through verbatim.
118
+ */
119
+ declare function serializeToMarkdown(doc: JSONContent): string;
120
+
121
+ /**
122
+ * Parse a markdown string (with `@[label](id)` or `@[label](type:id)` tokens)
123
+ * into a Tiptap-compatible JSON document.
124
+ */
125
+ declare function parseFromMarkdown(markdown: string): JSONContent;
126
+
127
+ export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };
@@ -0,0 +1,127 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { JSONContent } from '@tiptap/core';
4
+
5
+ /**
6
+ * A single suggestion item returned by a provider.
7
+ */
8
+ type MentionItem = {
9
+ /** Unique identifier for this item. */
10
+ id: string;
11
+ /** Entity kind — maps to `MentionToken.type` on insertion. */
12
+ type: string;
13
+ /** Display label shown in the suggestion list. */
14
+ label: string;
15
+ /** Optional icon rendered before the label. */
16
+ icon?: ReactNode;
17
+ /** Optional secondary text. */
18
+ description?: string;
19
+ /** When `true`, selecting this item drills into a child level instead of inserting. */
20
+ hasChildren?: boolean;
21
+ /** Optional payload carried through to `MentionToken.data`. */
22
+ data?: unknown;
23
+ };
24
+ /**
25
+ * Hierarchical suggestion source registered per trigger character.
26
+ *
27
+ * Consumers provide one provider per trigger (e.g. "@", "#", ":").
28
+ * The engine calls `getRootItems` first, then `getChildren` when
29
+ * the user drills into a nested level.
30
+ */
31
+ type MentionProvider = {
32
+ /** The character(s) that activate this provider — e.g. "@", "#", ":". */
33
+ trigger: string;
34
+ /** Human-readable name for this provider (used in ARIA labels). */
35
+ name: string;
36
+ /** Fetch top-level suggestions for the current query. */
37
+ getRootItems: (query: string) => Promise<MentionItem[]>;
38
+ /** Fetch child suggestions when the user drills into `parent`. */
39
+ getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
40
+ };
41
+
42
+ /**
43
+ * Core data model for a mention entity embedded in text.
44
+ *
45
+ * Stored as an inline ProseMirror node — the rendered text
46
+ * is derived from `label`, but the underlying identity is `id` + `type`.
47
+ */
48
+ type MentionToken = {
49
+ /** Unique identifier for the referenced entity. */
50
+ id: string;
51
+ /** Entity kind — e.g. "workspace", "contract", "file", "web", or any custom string. */
52
+ type: string;
53
+ /** Human-readable display label. */
54
+ label: string;
55
+ /** Optional full entity payload — opaque to the library, passed through untouched. */
56
+ data?: unknown;
57
+ };
58
+
59
+ /**
60
+ * Structured output returned on every editor change.
61
+ *
62
+ * Gives consumers three representations of the same content:
63
+ * - `markdown` — serialised with `@[label](id)` tokens, for storage / LLM context.
64
+ * - `tokens` — flat array of every mention in document order.
65
+ * - `plainText` — labels inlined, for display / search indexing.
66
+ */
67
+ type MentionsOutput = {
68
+ /** Markdown string with mention tokens encoded as `@[label](id)`. */
69
+ markdown: string;
70
+ /** Ordered array of all mention tokens in the document. */
71
+ tokens: MentionToken[];
72
+ /** Plain text with mention labels substituted inline. */
73
+ plainText: string;
74
+ };
75
+
76
+ /**
77
+ * Props for the public `<MentionsInput>` component.
78
+ */
79
+ type MentionsInputProps = {
80
+ /** Initial content as a markdown string (with `@[label](id)` tokens). */
81
+ value?: string;
82
+ /** Suggestion providers — one per trigger character. */
83
+ providers: MentionProvider[];
84
+ /** Called on every content change with the structured output. */
85
+ onChange?: (output: MentionsOutput) => void;
86
+ /** Placeholder text shown when the editor is empty. */
87
+ placeholder?: string;
88
+ /** Focus the editor on mount. */
89
+ autoFocus?: boolean;
90
+ /** Disable editing. */
91
+ disabled?: boolean;
92
+ /** CSS class applied to the outermost wrapper. */
93
+ className?: string;
94
+ /** Called when the user presses Enter (or Cmd+Enter). */
95
+ onSubmit?: (output: MentionsOutput) => void;
96
+ /** Maximum character count (plain text length). */
97
+ maxLength?: number;
98
+ /** Custom renderer for suggestion list items. */
99
+ renderItem?: (item: MentionItem, depth: number) => ReactNode;
100
+ /** Custom renderer for inline mention chips. */
101
+ renderChip?: (token: MentionToken) => ReactNode;
102
+ };
103
+
104
+ /**
105
+ * `<MentionsInput>` — the single public component.
106
+ *
107
+ * A structured text editor with typed entity tokens.
108
+ * Consumers register `providers` for each trigger character,
109
+ * and receive structured output via `onChange` and `onSubmit`.
110
+ */
111
+ declare function MentionsInput({ value, providers, onChange, placeholder, autoFocus, disabled, className, onSubmit, maxLength, renderItem, renderChip, }: MentionsInputProps): react_jsx_runtime.JSX.Element;
112
+
113
+ /**
114
+ * Serialize a Tiptap JSON document to a markdown string.
115
+ *
116
+ * Mention nodes are encoded as `@[label](id)` tokens.
117
+ * All other text passes through verbatim.
118
+ */
119
+ declare function serializeToMarkdown(doc: JSONContent): string;
120
+
121
+ /**
122
+ * Parse a markdown string (with `@[label](id)` or `@[label](type:id)` tokens)
123
+ * into a Tiptap-compatible JSON document.
124
+ */
125
+ declare function parseFromMarkdown(markdown: string): JSONContent;
126
+
127
+ export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };