@itsammarb/mention-editor 1.0.1 → 1.0.3
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 +50 -11
- package/dist/styles.css +1 -1
- package/llms.txt +69 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -49,22 +49,61 @@ Hello <@a1b2c3d4-0000-0000-0000-000000000001>, welcome.
|
|
|
49
49
|
|
|
50
50
|
## Props
|
|
51
51
|
|
|
52
|
-
| Prop | Type | Description |
|
|
52
|
+
| Prop | Type | Default | Description |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| `value` | `string` | *(required)* | Wire-format content (controlled — the editor always reflects this string). |
|
|
55
|
+
| `fields` | `{ id: string; label: string }[]` | *(required)* | Fields offered in the `@` suggestion menu; filter/localize before passing in — the component doesn't fetch or translate these itself. |
|
|
56
|
+
| `onChange` | `(value: string) => void` | `undefined` | Called with the new wire-format string on every edit. Omit for a read-only display (combine with `disabled`). |
|
|
57
|
+
| `dir` | `'ltr' \| 'rtl'` | `undefined` (browser infers) | Applied directly as the `dir` HTML attribute on the editable surface — required for correct caret movement and bidi layout with RTL content such as Arabic. |
|
|
58
|
+
| `disabled` | `boolean` | `false` | Makes the editor read-only (`Editable`'s `readOnly`) and dims it via `opacity-60`. |
|
|
59
|
+
| `isError` | `boolean` | `false` | Swaps the border to the invalid-state color (`border-red-500`). Purely visual — doesn't block typing. |
|
|
60
|
+
| `placeholder` | `string` | `undefined` (no placeholder) | Shown only when the document is fully empty (a single empty paragraph) — see [Behavior notes](#behavior-notes). |
|
|
61
|
+
| `rows` | `number` | `undefined` (intrinsic `min-h-11`, ~44px) | Sets `min-height` to `rows * 1.5em`, textarea-`rows`-equivalent. The editor still grows taller than this if content wraps to more lines. |
|
|
62
|
+
| `className` | `string` | `undefined` | Extra class name(s) appended to the root container, after the built-in ones — use this for layout (width, margin) or to override the built-in border/background. |
|
|
63
|
+
|
|
64
|
+
## Exported utilities
|
|
65
|
+
|
|
66
|
+
Everything importable from `@nexcore/mention-editor`:
|
|
67
|
+
|
|
68
|
+
| Export | Kind | Description |
|
|
53
69
|
| --- | --- | --- |
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `className` | `string` | Extra class name(s) on the root container. |
|
|
70
|
+
| `MentionEditor` | component | The editor itself; see [Props](#props). |
|
|
71
|
+
| `MentionEditorProps` | type | Prop types for `MentionEditor`. |
|
|
72
|
+
| `MentionFieldOption` | type | Shape of an entry in `fields`: `{ id: string; label: string }`. |
|
|
73
|
+
| `serialize(nodes: Descendant[]): string` | function | Converts a Slate `Descendant[]` tree to the wire-format string. Useful if you need to inspect/generate content outside the component (e.g. server-side). |
|
|
74
|
+
| `deserialize(value: string, fields?: MentionFieldOption[]): Descendant[]` | function | Inverse of `serialize`. `fields` resolves each mention's label; an id not found in `fields` still renders, falling back to the raw id as its label. |
|
|
75
|
+
| `serializeToDiscordMarkup` | function | Deprecated alias of `serialize`, kept for backwards compatibility with the pre-rename API. Prefer `serialize`. |
|
|
76
|
+
| `INITIAL_VALUE` | constant | The empty-document Slate value (`[{ type: 'paragraph', children: [{ text: '' }] }]`) `MentionEditor` uses internally when `value` is `''`. Useful if you're building your own Slate tooling around this package's types. |
|
|
77
|
+
| `MENTION_OPEN` / `MENTION_CLOSE` | constants | The literal delimiters (`'<@'` / `'>'`) used by the wire format — not configurable, but exported so you can detect/strip mention tokens from raw wire strings yourself without duplicating the regex. |
|
|
63
78
|
|
|
64
79
|
## Styling
|
|
65
80
|
|
|
66
81
|
The default look is authored with Tailwind utility classes internally, but compiled at *this package's* build time into a self-contained `dist/styles.css` — import it once (as shown above) and it works with zero Tailwind setup on your end, whether or not your app uses Tailwind at all. The compiled CSS deliberately excludes Tailwind's Preflight (base element reset), so it can't leak out and restyle your app's own `<h1>`, `<button>`, etc.
|
|
67
82
|
|
|
68
|
-
|
|
83
|
+
Every rendered part carries a stable class name, at normal specificity (plain single-class selectors, so a later rule you write always wins):
|
|
84
|
+
|
|
85
|
+
| Class | Element | Notes |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `.mention-editor` | root `<div>` | Border, rounding, background; gets `className` appended and `opacity-60` when `disabled`. |
|
|
88
|
+
| `.mention-editor__editable` | the `Editable` surface | Padding, text size/color, `min-height`. |
|
|
89
|
+
| `.mention-editor__paragraph` | each `<p>` block | Margin between paragraphs. |
|
|
90
|
+
| `.mention-editor__mention` | a mention `<span>` | Color, underline, `unicode-bidi: isolate` (see [Behavior notes](#behavior-notes)). |
|
|
91
|
+
| `.mention-editor__menu` | suggestion menu `<div>` | Portaled to `document.body`; border, shadow, sizing. |
|
|
92
|
+
| `.mention-editor__menu-item` | each menu row `<div>` | Also gets a highlight class when it's the keyboard-selected row. |
|
|
93
|
+
|
|
94
|
+
Override any of these by targeting the class directly in your own CSS, or via `className` on the root for layout/border/background changes.
|
|
95
|
+
|
|
96
|
+
**Dark mode**: the built-in `dark:` utilities respond to the OS/browser's `prefers-color-scheme: dark`, *not* a manually-toggled `.dark` class (e.g. from `next-themes` or a similar library). If your app drives dark mode via a class rather than OS preference, the shipped dark colors won't follow it — override `.mention-editor`, `.mention-editor__mention`, etc. directly with your own class-scoped CSS in that case.
|
|
69
97
|
|
|
70
98
|
If your own app happens to use Tailwind and you want to reuse its utility classes against this component's internal DOM (beyond what `className` on the root reaches), you can optionally point your app's Tailwind config/`@source` at `node_modules/@nexcore/mention-editor/dist` — but this is purely an opt-in extra, not required for the component to work or look right.
|
|
99
|
+
|
|
100
|
+
## Behavior notes
|
|
101
|
+
|
|
102
|
+
Things that are fixed (not currently exposed as props), so you know what to expect rather than hunt for a setting:
|
|
103
|
+
|
|
104
|
+
- The trigger character is always `@`, and it only fires at the start of a line or after whitespace — typing `user@` mid-word never opens the menu.
|
|
105
|
+
- The suggestion menu shows at most the first 10 matches of `fields`, filtered by a case-insensitive substring match against `label`. There's no built-in async/debounced search — `fields` is expected to already be the candidate list.
|
|
106
|
+
- Keyboard handling in the menu is fixed: `↑`/`↓` to move, `Tab` or `Enter` to select, `Escape` to dismiss.
|
|
107
|
+
- Undo/redo (`Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z`) works out of the box via `slate-history`.
|
|
108
|
+
- A mention is atomic: backspacing immediately after one deletes it whole in a single keystroke, never leaving a partial node.
|
|
109
|
+
- `placeholder` only renders when the document is *exactly* one empty paragraph — it disappears the moment any character (or a mention) is present, and won't reappear just because the visible text looks empty (e.g. a paragraph containing only a mention still counts as non-empty).
|
package/dist/styles.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}:root,:host{--color-red-500:oklch(63.7% .237 25.331);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-600:oklch(54.6% .245 262.881);--color-indigo-50:oklch(96.2% .018 272.314);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-900:oklch(21% .034 264.665);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-white:#fff;--spacing:.25rem;--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--radius-md:.375rem}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.z-\[9999\]{z-index:9999}.m-0{margin:0}.block{display:block}.contents{display:contents}.inline{display:inline}.max-h-\[300px\]{max-height:300px}.min-h-11{min-height:calc(var(--spacing) * 11)}.w-\[280px\]{width:280px}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize{resize:both}.overflow-y-auto{overflow-y:auto}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-gray-300{border-color:var(--color-gray-300)}.border-red-500{border-color:var(--color-red-500)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-white{background-color:var(--color-white)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.text-blue-600{color:var(--color-blue-600)}.text-gray-400{color:var(--color-gray-400)}.text-gray-900{color:var(--color-gray-900)}.underline{text-decoration-line:underline}.decoration-1{text-decoration-thickness:1px}.underline-offset-2{text-underline-offset:2px}.opacity-60{opacity:.6}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.outline-none{--tw-outline-style:none;outline-style:none}.\[unicode-bidi\:isolate\]{unicode-bidi:isolate}@media (prefers-color-scheme:dark){.dark\:border-neutral-700{border-color:var(--color-neutral-700)}.dark\:bg-neutral-700{background-color:var(--color-neutral-700)}.dark\:bg-neutral-800{background-color:var(--color-neutral-800)}.dark\:bg-neutral-900{background-color:var(--color-neutral-900)}.dark\:text-blue-400{color:var(--color-blue-400)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:text-neutral-500{color:var(--color-neutral-500)}}.\[\&\+\&\]\:mt-1+.\[\&\+\&\]\:mt-1{margin-top:var(--spacing)}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}:root,:host{--color-red-500:oklch(63.7% .237 25.331);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-600:oklch(54.6% .245 262.881);--color-indigo-50:oklch(96.2% .018 272.314);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-900:oklch(21% .034 264.665);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-white:#fff;--spacing:.25rem;--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--radius-md:.375rem}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.isolate{isolation:isolate}.z-\[9999\]{z-index:9999}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:0}.block{display:block}.contents{display:contents}.inline{display:inline}.max-h-\[300px\]{max-height:300px}.min-h-11{min-height:calc(var(--spacing) * 11)}.w-\[280px\]{width:280px}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize{resize:both}.overflow-y-auto{overflow-y:auto}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-gray-300{border-color:var(--color-gray-300)}.border-red-500{border-color:var(--color-red-500)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-white{background-color:var(--color-white)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.text-blue-600{color:var(--color-blue-600)}.text-gray-400{color:var(--color-gray-400)}.text-gray-900{color:var(--color-gray-900)}.underline{text-decoration-line:underline}.decoration-1{text-decoration-thickness:1px}.underline-offset-2{text-underline-offset:2px}.opacity-60{opacity:.6}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.outline-none{--tw-outline-style:none;outline-style:none}.\[unicode-bidi\:isolate\]{unicode-bidi:isolate}@media (prefers-color-scheme:dark){.dark\:border-neutral-700{border-color:var(--color-neutral-700)}.dark\:bg-neutral-700{background-color:var(--color-neutral-700)}.dark\:bg-neutral-800{background-color:var(--color-neutral-800)}.dark\:bg-neutral-900{background-color:var(--color-neutral-900)}.dark\:text-blue-400{color:var(--color-blue-400)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:text-neutral-500{color:var(--color-neutral-500)}}.\[\&\+\&\]\:mt-1+.\[\&\+\&\]\:mt-1{margin-top:var(--spacing)}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @itsammarb/mention-editor
|
|
2
|
+
|
|
3
|
+
> A Discord-style `@mention` rich-text editor for React, built on Slate.js. Renders once, in a real contenteditable surface (no mirrored textarea/div overlay), so it doesn't suffer the cursor-drift bugs that affect that approach with mixed RTL/LTR text.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @itsammarb/mention-editor slate slate-react slate-history
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer deps: React `^18.0.0 || ^19.0.0`, `slate` and `slate-react` `^0.94.0`. No Tailwind config needed on the consumer side — styling ships pre-compiled.
|
|
12
|
+
|
|
13
|
+
## Minimal usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { MentionEditor } from '@itsammarb/mention-editor';
|
|
17
|
+
import '@itsammarb/mention-editor/styles.css'; // required once, anywhere in the app
|
|
18
|
+
|
|
19
|
+
const fields = [{ id: 'a1b2c3d4-0000-0000-0000-000000000001', label: 'Landlord Name' }];
|
|
20
|
+
|
|
21
|
+
function Example() {
|
|
22
|
+
const [value, setValue] = useState('Hello <@a1b2c3d4-0000-0000-0000-000000000001>, welcome.');
|
|
23
|
+
return <MentionEditor value={value} fields={fields} onChange={setValue} />;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Wire format (the `value`/`onChange` string, NOT a Slate document)
|
|
28
|
+
|
|
29
|
+
- A mention is written as `<@` + the field's `id` + `>`, e.g. `<@a1b2c3d4-...>`.
|
|
30
|
+
- `\n` in the string is a paragraph break.
|
|
31
|
+
- Any id shape works (GUID, numeric, nanoid, ...) — the closing `>` disambiguates, no escaping/config needed.
|
|
32
|
+
- `serialize(nodes: Descendant[]): string` and `deserialize(value: string, fields?: MentionFieldOption[]): Descendant[]` (both exported) convert between this string and Slate's internal tree, if you need to do that outside the component.
|
|
33
|
+
- An id not present in the `fields` passed to `deserialize` still renders — falls back to showing the raw id as the label instead of dropping the mention.
|
|
34
|
+
|
|
35
|
+
## Props (all on `MentionEditor`)
|
|
36
|
+
|
|
37
|
+
| Prop | Type | Required | Notes |
|
|
38
|
+
| --- | --- | --- | --- |
|
|
39
|
+
| `value` | `string` | yes | Wire-format content; component is fully controlled. |
|
|
40
|
+
| `fields` | `{ id: string; label: string }[]` | yes | Candidates for the `@` menu; already filtered/localized by the caller — no built-in fetching. |
|
|
41
|
+
| `onChange` | `(value: string) => void` | no | Fires on every edit with the new wire string. |
|
|
42
|
+
| `dir` | `'ltr' \| 'rtl'` | no | Sets the real `dir` attribute on the editable surface; needed for correct RTL caret/bidi behavior. |
|
|
43
|
+
| `disabled` | `boolean` | no, default `false` | Read-only + dimmed. |
|
|
44
|
+
| `isError` | `boolean` | no, default `false` | Red border only; doesn't block input. |
|
|
45
|
+
| `placeholder` | `string` | no | Shows only when the doc is exactly one empty paragraph. |
|
|
46
|
+
| `rows` | `number` | no | `min-height = rows * 1.5em`, textarea-rows-equivalent; grows taller if content wraps further. |
|
|
47
|
+
| `className` | `string` | no | Appended to the root container's class list. |
|
|
48
|
+
|
|
49
|
+
## Other exports
|
|
50
|
+
|
|
51
|
+
`MentionEditorProps` (type), `MentionFieldOption` (type), `INITIAL_VALUE` (empty-doc Slate value), `MENTION_OPEN` / `MENTION_CLOSE` (the `'<@'` / `'>'` delimiter constants), `serializeToDiscordMarkup` (deprecated alias of `serialize`).
|
|
52
|
+
|
|
53
|
+
## Behavior that is fixed, not configurable via props
|
|
54
|
+
|
|
55
|
+
- Trigger is always `@`; only fires at line-start or after whitespace (never mid-word, e.g. `user@`).
|
|
56
|
+
- Suggestion menu shows top 10 case-insensitive substring matches on `label`.
|
|
57
|
+
- Menu keys: `↑`/`↓` navigate, `Tab`/`Enter` select, `Escape` dismiss.
|
|
58
|
+
- Undo/redo (`Ctrl/Cmd+Z`) works via `slate-history`.
|
|
59
|
+
- A mention is atomic: one Backspace immediately after it deletes the whole node, never a partial one.
|
|
60
|
+
- Mentions get `unicode-bidi: isolate` so an embedded Latin/digit label can't flip surrounding RTL paragraph reordering.
|
|
61
|
+
- Built-in `dark:` styling follows OS `prefers-color-scheme`, not a manually toggled `.dark` class (e.g. `next-themes`) — override the `.mention-editor*` classes directly if you need class-driven dark mode.
|
|
62
|
+
|
|
63
|
+
## Styling hook classes (all normal, single-class specificity — a later rule wins)
|
|
64
|
+
|
|
65
|
+
`.mention-editor` (root), `.mention-editor__editable`, `.mention-editor__paragraph`, `.mention-editor__mention`, `.mention-editor__menu`, `.mention-editor__menu-item`.
|
|
66
|
+
|
|
67
|
+
## Full docs
|
|
68
|
+
|
|
69
|
+
See `README.md` in this package for the complete prop/export/styling reference and more detail on each point above.
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itsammarb/mention-editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A Discord-style @mention rich-text editor built on Slate.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"llms.txt"
|
|
10
11
|
],
|
|
11
12
|
"exports": {
|
|
12
13
|
".": {
|