@itsammarb/mention-editor 1.0.4 → 1.0.5

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
@@ -60,6 +60,7 @@ Hello <@a1b2c3d4-0000-0000-0000-000000000001>, welcome.
60
60
  | `placeholder` | `string` | `undefined` (no placeholder) | Shown only when the document is fully empty (a single empty paragraph) — see [Behavior notes](#behavior-notes). |
61
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
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
+ | `colors` | `MentionEditorColors` | `undefined` (built-in defaults) | Per-instance color overrides — see [Theme colors](#theme-colors). Omitted keys keep their default. |
63
64
 
64
65
  ## Exported utilities
65
66
 
@@ -70,6 +71,7 @@ Everything importable from `@itsammarb/mention-editor`:
70
71
  | `MentionEditor` | component | The editor itself; see [Props](#props). |
71
72
  | `MentionEditorProps` | type | Prop types for `MentionEditor`. |
72
73
  | `MentionFieldOption` | type | Shape of an entry in `fields`: `{ id: string; label: string }`. |
74
+ | `MentionEditorColors` | type | Shape of the `colors` prop — see [Theme colors](#theme-colors). |
73
75
  | `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
76
  | `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
77
  | `serializeToDiscordMarkup` | function | Deprecated alias of `serialize`, kept for backwards compatibility with the pre-rename API. Prefer `serialize`. |
@@ -95,37 +97,43 @@ Override any of these by targeting the class directly in your own CSS, or via `c
95
97
 
96
98
  ### Theme colors
97
99
 
98
- Every color is also a CSS custom property, so you can retheme the whole component with a handful of variable declarations instead of overriding individual classes. Set them anywhere that's an ancestor of the whole page — `:root`, `html`, `body`, or a global stylesheet rule — **not** scoped to `.mention-editor` itself: the suggestion menu is rendered through a React portal directly into `document.body`, so it sits *outside* `.mention-editor` in the real DOM and wouldn't inherit a variable scoped there.
100
+ Pass a `colors` prop to override any of the built-in colors, per instance no external CSS needed:
99
101
 
100
- ```css
101
- /* anywhere in your global CSS, loaded in any order relative to this package's styles.css */
102
- :root {
103
- --mention-editor-mention-color: #16a34a; /* green mention text/underline */
104
- --mention-editor-mention-bg: #dcfce7; /* optional pill-style highlight behind a mention */
105
- --mention-editor-border-color: #db2777;
106
- --mention-editor-menu-highlight-bg: #fde68a; /* selected/hovered suggestion row */
107
- }
102
+ ```tsx
103
+ <MentionEditor
104
+ value={value}
105
+ fields={fields}
106
+ onChange={setValue}
107
+ colors={{
108
+ mentionColor: '#16a34a', // green mention text/underline
109
+ mentionBg: '#dcfce7', // optional pill-style highlight behind a mention
110
+ borderColor: '#db2777',
111
+ menuHighlightBg: '#fde68a', // selected/hovered suggestion row
112
+ }}
113
+ />
108
114
  ```
109
115
 
110
- | Variable | Controls | Light default | Dark default |
116
+ Omitted keys keep their built-in default, and different instances on the same page can have entirely different `colors` without affecting each other — including each instance's own suggestion menu, even though the menu renders through a React portal into `document.body` (a sibling of the editor in the real DOM, not a descendant): `MentionEditor` forwards `colors` to it directly rather than relying on CSS inheritance.
117
+
118
+ | `colors` key | Controls | Light default | Dark default |
111
119
  | --- | --- | --- | --- |
112
- | `--mention-editor-bg` | Editor container background | white | neutral-900 |
113
- | `--mention-editor-text-color` | Editor body text color | gray-900 | gray-100 |
114
- | `--mention-editor-border-color` | Container border (normal state) | gray-300 | neutral-700 |
115
- | `--mention-editor-border-color-error` | Container border when `isError` | red-500 | red-500 (same in both) |
116
- | `--mention-editor-placeholder-color` | Placeholder text color | gray-400 | neutral-500 |
117
- | `--mention-editor-mention-color` | A mention's text/underline color | blue-600 | blue-400 |
118
- | `--mention-editor-mention-bg` | Background behind a mention (e.g. a pill highlight) | transparent | transparent |
119
- | `--mention-editor-menu-bg` | Suggestion menu background | white | neutral-800 |
120
- | `--mention-editor-menu-border-color` | Suggestion menu border | gray-300 | neutral-700 |
121
- | `--mention-editor-menu-text-color` | Suggestion menu row text | gray-900 | gray-100 |
122
- | `--mention-editor-menu-highlight-bg` | Selected/hovered suggestion row background | indigo-50 | neutral-700 |
123
-
124
- Setting a variable applies it in both light and dark mode (the fallback is what differs by scheme, not the override) — if you want genuinely different override colors per scheme, redeclare the variable yourself inside your own `@media (prefers-color-scheme: dark)` block.
125
-
126
- These defaults aren't asserted anywhere as a real `:root` rule in this package's stylesheet they're inline `var(--x, fallback)` fallbacks on each utility class instead. That's deliberate: a real default declaration and a consumer override would both be equal-specificity `:root` rules, so whichever stylesheet happened to load *last* would silently win regardless of intent. Reading the variable with an inline fallback means there's only ever one real assignment (yours, if you set one), so load order can't matter.
127
-
128
- **Dark mode**: the built-in dark defaults 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, set the variables above directly (they'll then apply regardless of scheme), or scope your own overrides under your app's dark-mode class selector.
120
+ | `bg` | Editor container background | white | neutral-900 |
121
+ | `textColor` | Editor body text color | gray-900 | gray-100 |
122
+ | `borderColor` | Container border (normal state) | gray-300 | neutral-700 |
123
+ | `borderColorError` | Container border when `isError` | red-500 | red-500 (same in both) |
124
+ | `placeholderColor` | Placeholder text color | gray-400 | neutral-500 |
125
+ | `mentionColor` | A mention's text/underline color | blue-600 | blue-400 |
126
+ | `mentionBg` | Background behind a mention (e.g. a pill highlight) | transparent | transparent |
127
+ | `menuBg` | Suggestion menu background | white | neutral-800 |
128
+ | `menuBorderColor` | Suggestion menu border | gray-300 | neutral-700 |
129
+ | `menuTextColor` | Suggestion menu row text | gray-900 | gray-100 |
130
+ | `menuHighlightBg` | Selected/hovered suggestion row background | indigo-50 | neutral-700 |
131
+
132
+ A `colors` value applies in both light and dark mode (the fallback is what differs by scheme, not your override) — if you want genuinely different colors per scheme, read `window.matchMedia('(prefers-color-scheme: dark)')` (or your app's own dark-mode state) and pass a different `colors` object accordingly.
133
+
134
+ **Dark mode**: the built-in dark defaults (used when `colors` doesn't set a given key) 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, either pass `colors` explicitly (bypassing the scheme-based default entirely), or drive it from your own dark-mode state as described above.
135
+
136
+ **Advanced — global CSS override**: under the hood, `colors` works by setting CSS custom properties (`--mention-editor-*`) inline. You can also set these directly in your own global CSS (`:root { --mention-editor-mention-color: ...; }`) as a page-wide fallback for instances that don't pass `colors` at all an inline `colors` value always takes precedence over a global CSS one. Global CSS declarations must be scoped to `:root`/`html`/`body` (not `.mention-editor`) for the same portal reason as above.
129
137
 
130
138
  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/@itsammarb/mention-editor/dist` — but this is purely an opt-in extra, not required for the component to work or look right.
131
139
 
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { MentionFieldOption } from './types';
2
+ import { MentionFieldOption, MentionEditorColors } from './types';
3
3
  export interface MentionEditorProps {
4
4
  /** Wire-format string, e.g. `"Hello <@a1b2c3d4-...>, pay rent."` */
5
5
  value: string;
@@ -13,5 +13,7 @@ export interface MentionEditorProps {
13
13
  /** Approximate visible height, textarea-`rows`-equivalent. */
14
14
  rows?: number;
15
15
  className?: string;
16
+ /** Per-instance color overrides; omitted keys keep the built-in default. */
17
+ colors?: MentionEditorColors;
16
18
  }
17
19
  export declare function MentionEditor(props: MentionEditorProps): React.JSX.Element;
@@ -8,9 +8,19 @@ const slate_react_1 = require("slate-react");
8
8
  const slate_history_1 = require("slate-history");
9
9
  const types_1 = require("./types");
10
10
  const serialize_1 = require("./serialize");
11
+ const colors_1 = require("./colors");
11
12
  const MentionMenu_1 = require("./MentionMenu");
13
+ const EDITOR_COLOR_KEYS = [
14
+ 'bg',
15
+ 'textColor',
16
+ 'borderColor',
17
+ 'borderColorError',
18
+ 'placeholderColor',
19
+ 'mentionColor',
20
+ 'mentionBg',
21
+ ];
12
22
  function MentionEditor(props) {
13
- const { value, fields, onChange, dir, disabled = false, isError = false, placeholder, rows, className, } = props;
23
+ const { value, fields, onChange, dir, disabled = false, isError = false, placeholder, rows, className, colors, } = props;
14
24
  // `generation` forces a full remount of the Slate editor instance whenever the
15
25
  // `value` prop changes for a reason *other* than our own onChange echoing back
16
26
  // down (e.g. the consumer swaps to editing a different record). See the
@@ -149,7 +159,10 @@ function MentionEditor(props) {
149
159
  .filter(Boolean)
150
160
  .join(' ');
151
161
  const editableStyle = rows ? { minHeight: `${rows * 1.5}em` } : undefined;
152
- return ((0, jsx_runtime_1.jsx)(slate_react_1.Slate, { editor: editor, value: slateValue, onChange: handleChange, children: (0, jsx_runtime_1.jsxs)("div", { className: rootClassName, children: [(0, jsx_runtime_1.jsx)(slate_react_1.Editable, { className: "mention-editor__editable min-h-11 px-3 py-2 text-base leading-6 outline-none text-(--mention-editor-text-color,var(--color-gray-900)) dark:text-(--mention-editor-text-color,var(--color-gray-100))", style: editableStyle, dir: dir, readOnly: disabled, onKeyDown: handleKeyDown, placeholder: placeholder, renderElement: (elementProps) => (0, jsx_runtime_1.jsx)(Element, { ...elementProps }), renderPlaceholder: renderPlaceholder }), target && ((0, jsx_runtime_1.jsx)(MentionMenu_1.MentionMenu, { fields: filteredFields, selectedIndex: index, onSelect: selectField, onHover: setIndex, targetRect: menuTargetRect }))] }) }, generation));
162
+ // Set on the root: cascades naturally to Editable/mention/placeholder since
163
+ // they're real DOM descendants (unlike the portaled menu -- see MentionMenu).
164
+ const rootStyle = (0, colors_1.colorsToCssVars)(colors, EDITOR_COLOR_KEYS);
165
+ return ((0, jsx_runtime_1.jsx)(slate_react_1.Slate, { editor: editor, value: slateValue, onChange: handleChange, children: (0, jsx_runtime_1.jsxs)("div", { className: rootClassName, style: rootStyle, children: [(0, jsx_runtime_1.jsx)(slate_react_1.Editable, { className: "mention-editor__editable min-h-11 px-3 py-2 text-base leading-6 outline-none text-(--mention-editor-text-color,var(--color-gray-900)) dark:text-(--mention-editor-text-color,var(--color-gray-100))", style: editableStyle, dir: dir, readOnly: disabled, onKeyDown: handleKeyDown, placeholder: placeholder, renderElement: (elementProps) => (0, jsx_runtime_1.jsx)(Element, { ...elementProps }), renderPlaceholder: renderPlaceholder }), target && ((0, jsx_runtime_1.jsx)(MentionMenu_1.MentionMenu, { fields: filteredFields, selectedIndex: index, onSelect: selectField, onHover: setIndex, targetRect: menuTargetRect, colors: colors }))] }) }, generation));
153
166
  }
154
167
  // slate-react's default placeholder renders at a fixed dim-black opacity,
155
168
  // which reads poorly in dark mode. Overriding it lets the placeholder use
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
- import { MentionFieldOption } from './types';
2
+ import { MentionFieldOption, MentionEditorColors } from './types';
3
3
  interface MentionMenuProps {
4
4
  fields: MentionFieldOption[];
5
5
  selectedIndex: number;
6
6
  onSelect: (field: MentionFieldOption) => void;
7
7
  onHover: (index: number) => void;
8
8
  targetRect: DOMRect | null;
9
+ colors?: MentionEditorColors;
9
10
  }
10
11
  export declare const MentionMenu: React.FC<MentionMenuProps>;
11
12
  export {};
@@ -3,7 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MentionMenu = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_dom_1 = require("react-dom");
6
- const MentionMenu = ({ fields, selectedIndex, onSelect, onHover, targetRect, }) => {
6
+ const colors_1 = require("./colors");
7
+ const MENU_COLOR_KEYS = [
8
+ 'menuBg',
9
+ 'menuBorderColor',
10
+ 'menuTextColor',
11
+ 'menuHighlightBg',
12
+ ];
13
+ const MentionMenu = ({ fields, selectedIndex, onSelect, onHover, targetRect, colors, }) => {
7
14
  if (!targetRect || fields.length === 0)
8
15
  return null;
9
16
  // Rendered into document.body via a portal and positioned with `fixed` using
@@ -13,10 +20,15 @@ const MentionMenu = ({ fields, selectedIndex, onSelect, onHover, targetRect, })
13
20
  // another modal: portaling to <body> means there's no transformed/positioned
14
21
  // ancestor between the menu and the viewport to throw off `fixed` coordinates.
15
22
  // The caller (MentionEditor) keeps `targetRect` fresh across scroll/resize.
23
+ //
24
+ // `colors` has to be applied here too, not just on `.mention-editor` -- this
25
+ // element is a portal child of `document.body`, a sibling of `.mention-editor`
26
+ // in the real DOM, so it wouldn't inherit color overrides set there.
16
27
  return (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { className: "mention-editor__menu z-9999 w-70 max-h-75 overflow-y-auto rounded-md border py-1 shadow-lg border-(--mention-editor-menu-border-color,var(--color-gray-300)) dark:border-(--mention-editor-menu-border-color,var(--color-neutral-700)) bg-(--mention-editor-menu-bg,var(--color-white)) dark:bg-(--mention-editor-menu-bg,var(--color-neutral-800))", style: {
17
28
  position: 'fixed',
18
29
  top: targetRect.bottom + 8,
19
30
  left: targetRect.left,
31
+ ...(0, colors_1.colorsToCssVars)(colors, MENU_COLOR_KEYS),
20
32
  }, children: fields.map((field, i) => ((0, jsx_runtime_1.jsx)("div", { className: 'mention-editor__menu-item cursor-pointer px-3 py-2' +
21
33
  ' text-(--mention-editor-menu-text-color,var(--color-gray-900))' +
22
34
  ' dark:text-(--mention-editor-menu-text-color,var(--color-gray-100))' +
@@ -0,0 +1,9 @@
1
+ import type { CSSProperties } from 'react';
2
+ import { MentionEditorColors } from './types';
3
+ /**
4
+ * Builds an inline `style` object setting only the CSS custom properties for
5
+ * the given keys that are actually present in `colors` -- omitted keys are
6
+ * left unset entirely, so the built-in fallback in the compiled CSS still
7
+ * applies for them.
8
+ */
9
+ export declare const colorsToCssVars: (colors: MentionEditorColors | undefined, keys: (keyof MentionEditorColors)[]) => CSSProperties;
package/dist/colors.js ADDED
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.colorsToCssVars = void 0;
4
+ // Maps each `colors` prop key to the CSS custom property the compiled
5
+ // Tailwind classes read via `var(--x, <built-in fallback>)`. Setting the
6
+ // property inline (rather than requiring a consumer to write global CSS)
7
+ // wins over the fallback automatically -- and, being inline, wins regardless
8
+ // of stylesheet load order, since there's no competing external rule to race.
9
+ const COLOR_VARS = {
10
+ bg: '--mention-editor-bg',
11
+ textColor: '--mention-editor-text-color',
12
+ borderColor: '--mention-editor-border-color',
13
+ borderColorError: '--mention-editor-border-color-error',
14
+ placeholderColor: '--mention-editor-placeholder-color',
15
+ mentionColor: '--mention-editor-mention-color',
16
+ mentionBg: '--mention-editor-mention-bg',
17
+ menuBg: '--mention-editor-menu-bg',
18
+ menuBorderColor: '--mention-editor-menu-border-color',
19
+ menuTextColor: '--mention-editor-menu-text-color',
20
+ menuHighlightBg: '--mention-editor-menu-highlight-bg',
21
+ };
22
+ /**
23
+ * Builds an inline `style` object setting only the CSS custom properties for
24
+ * the given keys that are actually present in `colors` -- omitted keys are
25
+ * left unset entirely, so the built-in fallback in the compiled CSS still
26
+ * applies for them.
27
+ */
28
+ const colorsToCssVars = (colors, keys) => {
29
+ if (!colors)
30
+ return {};
31
+ const style = {};
32
+ for (const key of keys) {
33
+ const value = colors[key];
34
+ if (value)
35
+ style[COLOR_VARS[key]] = value;
36
+ }
37
+ return style;
38
+ };
39
+ exports.colorsToCssVars = colorsToCssVars;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { MentionEditor } from './MentionEditor';
2
2
  export type { MentionEditorProps } from './MentionEditor';
3
3
  export { serialize, deserialize, serializeToDiscordMarkup } from './serialize';
4
- export type { MentionFieldOption } from './types';
4
+ export type { MentionFieldOption, MentionEditorColors } from './types';
5
5
  export { INITIAL_VALUE, MENTION_OPEN, MENTION_CLOSE } from './types';
package/dist/types.d.ts CHANGED
@@ -27,3 +27,16 @@ declare module 'slate' {
27
27
  export declare const INITIAL_VALUE: Descendant[];
28
28
  export declare const MENTION_OPEN = "<@";
29
29
  export declare const MENTION_CLOSE = ">";
30
+ export interface MentionEditorColors {
31
+ bg?: string;
32
+ textColor?: string;
33
+ borderColor?: string;
34
+ borderColorError?: string;
35
+ placeholderColor?: string;
36
+ mentionColor?: string;
37
+ mentionBg?: string;
38
+ menuBg?: string;
39
+ menuBorderColor?: string;
40
+ menuTextColor?: string;
41
+ menuHighlightBg?: string;
42
+ }
package/llms.txt CHANGED
@@ -45,10 +45,11 @@ function Example() {
45
45
  | `placeholder` | `string` | no | Shows only when the doc is exactly one empty paragraph. |
46
46
  | `rows` | `number` | no | `min-height = rows * 1.5em`, textarea-rows-equivalent; grows taller if content wraps further. |
47
47
  | `className` | `string` | no | Appended to the root container's class list. |
48
+ | `colors` | `MentionEditorColors` | no | Per-instance color overrides; see Theme colors below. Omitted keys keep the default. |
48
49
 
49
50
  ## Other exports
50
51
 
51
- `MentionEditorProps` (type), `MentionFieldOption` (type), `INITIAL_VALUE` (empty-doc Slate value), `MENTION_OPEN` / `MENTION_CLOSE` (the `'<@'` / `'>'` delimiter constants), `serializeToDiscordMarkup` (deprecated alias of `serialize`).
52
+ `MentionEditorProps` (type), `MentionFieldOption` (type), `MentionEditorColors` (type), `INITIAL_VALUE` (empty-doc Slate value), `MENTION_OPEN` / `MENTION_CLOSE` (the `'<@'` / `'>'` delimiter constants), `serializeToDiscordMarkup` (deprecated alias of `serialize`).
52
53
 
53
54
  ## Behavior that is fixed, not configurable via props
54
55
 
@@ -64,13 +65,11 @@ function Example() {
64
65
 
65
66
  `.mention-editor` (root), `.mention-editor__editable`, `.mention-editor__paragraph`, `.mention-editor__mention`, `.mention-editor__menu`, `.mention-editor__menu-item`.
66
67
 
67
- ## Theme colors (CSS custom properties)
68
+ ## Theme colors
68
69
 
69
- Set these at `:root`/`html`/`body` scope in your own global CSS (not scoped to `.mention-editor` the suggestion menu portals to `document.body`, outside it) to retheme without touching classes:
70
+ Use the `colors` prop, not CSS, to retheme -- e.g. `colors={{ mentionColor: '#16a34a', mentionBg: '#dcfce7', borderColor: '#db2777', menuHighlightBg: '#fde68a' }}`. Keys: `bg`, `textColor`, `borderColor`, `borderColorError`, `placeholderColor`, `mentionColor`, `mentionBg`, `menuBg`, `menuBorderColor`, `menuTextColor`, `menuHighlightBg`. Omitted keys keep the built-in default (which itself differs light vs. dark, following OS `prefers-color-scheme`). Each editor instance's `colors` is independent, and is correctly forwarded to that instance's own suggestion menu even though the menu renders through a React portal into `document.body`.
70
71
 
71
- `--mention-editor-bg`, `--mention-editor-text-color`, `--mention-editor-border-color`, `--mention-editor-border-color-error`, `--mention-editor-placeholder-color`, `--mention-editor-mention-color`, `--mention-editor-mention-bg`, `--mention-editor-menu-bg`, `--mention-editor-menu-border-color`, `--mention-editor-menu-text-color`, `--mention-editor-menu-highlight-bg`.
72
-
73
- Each has a light/dark default baked in as a `var(--x, fallback)` on the relevant utility class (no competing `:root` declaration in this package's own CSS), so setting one always wins regardless of stylesheet load order, and applies in both light and dark mode unless you scope your own override inside a `prefers-color-scheme` media query.
72
+ Advanced: `colors` works by setting `--mention-editor-*` CSS custom properties inline; you can also set these directly in your own global CSS at `:root`/`html`/`body` scope (not `.mention-editor` -- the portaled menu sits outside it) as a page-wide fallback. An inline `colors` value always wins over a global CSS one.
74
73
 
75
74
  ## Full docs
76
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itsammarb/mention-editor",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "A Discord-style @mention rich-text editor built on Slate.js",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",