@thebuoyant-tsdev/mui-ts-library 3.13.0 → 3.14.1

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.de.md CHANGED
@@ -434,6 +434,13 @@ Dieses Projekt folgt [Semantic Versioning](https://semver.org/). In der Praxis b
434
434
 
435
435
  ## Changelog
436
436
 
437
+ ### [3.14.0] — 2026-07-02
438
+
439
+ **Hinzugefügt**
440
+ - RichTextEditor: `@`-Mention-Autocomplete (`mentionItems`, `onMentionSearch`, `mentionTriggerChar`, `translation.mentionNoResults`). Dropdown am Cursor verankert, Tastaturnavigation (↑/↓/Enter/Escape), im HTML-Output als `<span data-type="mention" data-id="…">` serialisiert. Neuer Export: `MentionItem`. Neue Peer-Abhängigkeit: `@tiptap/extension-mention`. Siehe [Vollständiger Changelog](https://github.com/thebuoyant/mui-ts-library/blob/main/CHANGELOG.md) für Details.
441
+
442
+ ---
443
+
437
444
  ### [3.13.0] — 2026-06-29
438
445
 
439
446
  **Hinzugefügt**
package/README.md CHANGED
@@ -434,6 +434,13 @@ This project follows [Semantic Versioning](https://semver.org/). In practice:
434
434
 
435
435
  ## Changelog
436
436
 
437
+ ### [3.14.0] — 2026-07-02
438
+
439
+ **Added**
440
+ - RichTextEditor: `@`-mention autocomplete (`mentionItems`, `onMentionSearch`, `mentionTriggerChar`, `translation.mentionNoResults`). Dropdown anchored to the cursor, keyboard navigation (↑/↓/Enter/Escape), serialised as `<span data-type="mention" data-id="…">` in HTML output. New export: `MentionItem`. New peer dependency: `@tiptap/extension-mention`. See [Full Changelog](https://github.com/thebuoyant/mui-ts-library/blob/main/CHANGELOG.md) for details.
441
+
442
+ ---
443
+
437
444
  ### [3.13.0] — 2026-06-29
438
445
 
439
446
  **Added**
@@ -303,8 +303,7 @@ function x({ value: o, onChange: x, onChangeCommitted: S, defaultFormat: ae = "h
303
303
  disabled: T,
304
304
  onChange: (e) => ye(e.target.value),
305
305
  sx: {
306
- minWidth: 72,
307
- fontSize: "0.75rem",
306
+ minWidth: 80,
308
307
  flexShrink: 0
309
308
  },
310
309
  children: [
@@ -5,4 +5,4 @@ declare module "@tiptap/core" {
5
5
  }
6
6
  }
7
7
  import { type RichTextEditorProps } from "./RichTextEditor.types";
8
- export declare function RichTextEditor({ disabled, error, height, helperText, maxCharacters, name, placeholder, readonly, showCharacterCount, showToolbar, showWordCount, toolbarConfig, translation, value, width, onBlur, onChange, onFocus, onMarkdownChange, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function RichTextEditor({ disabled, error, height, helperText, maxCharacters, mentionItems, mentionTriggerChar, name, onMentionSearch, placeholder, readonly, showCharacterCount, showToolbar, showWordCount, toolbarConfig, translation, value, width, onBlur, onChange, onFocus, onMarkdownChange, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -1,56 +1,83 @@
1
1
  import { normalizeSize as e } from "../shared/normalizeSize.js";
2
2
  import { DEFAULT_RICH_TEXT_EDITOR_TOOLBAR_CONFIG as t, DEFAULT_RICH_TEXT_EDITOR_TRANSLATION as n } from "./RichTextEditor.types.js";
3
- import { RichTextEditorContent as r } from "./RichTextEditorContent.js";
4
- import { RichTextEditorToolbar as i } from "./RichTextEditorToolbar.js";
5
- import { RichTextEditorFooter as a } from "./RichTextEditorFooter.js";
6
- import { useEffect as o, useRef as s, useState as c } from "react";
7
- import { Box as l, Divider as u, Paper as d } from "@mui/material";
8
- import { Fragment as f, jsx as p, jsxs as m } from "react/jsx-runtime";
9
- import { useEditor as h } from "@tiptap/react";
10
- import { StarterKit as g } from "@tiptap/starter-kit";
11
- import { TextStyle as _ } from "@tiptap/extension-text-style";
12
- import { Color as v } from "@tiptap/extension-color";
13
- import { Highlight as ee } from "@tiptap/extension-highlight";
14
- import { Placeholder as y } from "@tiptap/extension-placeholder";
15
- import { CharacterCount as b } from "@tiptap/extension-character-count";
16
- import { TableKit as x } from "@tiptap/extension-table";
17
- import { Image as S } from "@tiptap/extension-image";
18
- import { Markdown as C } from "tiptap-markdown";
3
+ import { buildMentionSuggestion as r } from "./RichTextEditorMentionSuggestion.js";
4
+ import { RichTextEditorContent as i } from "./RichTextEditorContent.js";
5
+ import { RichTextEditorToolbar as a } from "./RichTextEditorToolbar.js";
6
+ import { RichTextEditorFooter as ee } from "./RichTextEditorFooter.js";
7
+ import { useEffect as o, useMemo as te, useRef as s, useState as c } from "react";
8
+ import { Box as l, Divider as u, Paper as ne } from "@mui/material";
9
+ import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
10
+ import { useEditor as re } from "@tiptap/react";
11
+ import { Mention as m } from "@tiptap/extension-mention";
12
+ import { StarterKit as h } from "@tiptap/starter-kit";
13
+ import { TextStyle as ie } from "@tiptap/extension-text-style";
14
+ import { Color as g } from "@tiptap/extension-color";
15
+ import { Highlight as _ } from "@tiptap/extension-highlight";
16
+ import { Placeholder as ae } from "@tiptap/extension-placeholder";
17
+ import { CharacterCount as v } from "@tiptap/extension-character-count";
18
+ import { TableKit as oe } from "@tiptap/extension-table";
19
+ import { Image as y } from "@tiptap/extension-image";
20
+ import { Markdown as b } from "tiptap-markdown";
19
21
  //#region src/components/rich-text-editor/RichTextEditor.tsx
20
- function w({ disabled: w = !1, error: T = !1, height: E, helperText: D, maxCharacters: O, name: k, placeholder: A, readonly: j = !1, showCharacterCount: M = !1, showToolbar: N = !0, showWordCount: P = !1, toolbarConfig: F, translation: I, value: L, width: R, onBlur: z, onChange: B, onFocus: V, onMarkdownChange: te }) {
22
+ function x({ disabled: x = !1, error: S = !1, height: se, helperText: C, maxCharacters: w, mentionItems: T, mentionTriggerChar: E = "@", name: D, onMentionSearch: O, placeholder: k, readonly: A = !1, showCharacterCount: j = !1, showToolbar: M = !0, showWordCount: N = !1, toolbarConfig: P, translation: F, value: I, width: L, onBlur: R, onChange: z, onFocus: B, onMarkdownChange: V }) {
21
23
  let H = {
22
24
  ...n,
23
- ...I
25
+ ...F
24
26
  }, U = {
25
27
  ...t,
26
- ...F
27
- }, [W, G] = c(!1), [K, q] = c(!1), J = s(K);
28
+ ...P
29
+ }, [W, G] = c(!1), [K, ce] = c(!1), q = s(K), J = s(T ?? []);
28
30
  o(() => {
29
- J.current = K;
31
+ J.current = T ?? [];
32
+ }, [T]), o(() => {
33
+ q.current = K;
30
34
  }, [K]);
31
- let Y = e(E), X = e(R), Z = Y === "auto", Q = Z ? void 0 : Y ?? 200, ne = O !== void 0 && O > 0 || M || P, $ = h({
35
+ let Y = e(se), le = e(L), X = Y === "auto", ue = X ? void 0 : Y ?? 200, Z = T !== void 0 || O !== void 0, Q = te(() => Z ? m.configure({
36
+ HTMLAttributes: { class: "rte-mention" },
37
+ renderHTML({ options: e, node: t }) {
38
+ return [
39
+ "span",
40
+ {
41
+ ...e.HTMLAttributes,
42
+ "data-id": t.attrs.id,
43
+ "data-label": t.attrs.label
44
+ },
45
+ `${E}${t.attrs.label}`
46
+ ];
47
+ },
48
+ suggestion: r({
49
+ getItems: () => J.current,
50
+ onMentionSearch: O,
51
+ noResultsLabel: H.mentionNoResults ?? "No results"
52
+ })
53
+ }) : null, [
54
+ Z,
55
+ E,
56
+ O
57
+ ]), de = w !== void 0 && w > 0 || j || N, $ = re({
32
58
  shouldRerenderOnTransaction: !0,
33
59
  extensions: [
34
- g.configure({ link: { openOnClick: !1 } }),
35
- _,
36
- v,
37
- ee.configure({ multicolor: !0 }),
38
- C.configure({
60
+ h.configure({ link: { openOnClick: !1 } }),
61
+ ie,
62
+ g,
63
+ _.configure({ multicolor: !0 }),
64
+ b.configure({
39
65
  transformPastedText: !0,
40
66
  transformCopiedText: !1
41
67
  }),
42
- y.configure({ placeholder: A ?? "" }),
43
- ...U.showTableButton ? [x] : [],
44
- ...U.showImageButton ? [S.configure({
68
+ ae.configure({ placeholder: k ?? "" }),
69
+ ...U.showTableButton ? [oe] : [],
70
+ ...U.showImageButton ? [y.configure({
45
71
  inline: !1,
46
72
  allowBase64: !0
47
73
  })] : [],
48
- ...ne ? O !== void 0 && O > 0 ? [b.configure({ limit: O })] : [b] : []
74
+ ...Q ? [Q] : [],
75
+ ...de ? w !== void 0 && w > 0 ? [v.configure({ limit: w })] : [v] : []
49
76
  ],
50
- content: L ?? "",
51
- editable: !w && !j,
77
+ content: I ?? "",
78
+ editable: !x && !A,
52
79
  editorProps: { handlePaste(e, t) {
53
- if (!J.current) return !1;
80
+ if (!q.current) return !1;
54
81
  let n = t.clipboardData?.getData("text/plain");
55
82
  if (!n) return !1;
56
83
  t.preventDefault();
@@ -58,26 +85,26 @@ function w({ disabled: w = !1, error: T = !1, height: E, helperText: D, maxChara
58
85
  return e.dispatch(r.tr.insertText(n, r.selection.from, r.selection.to)), !0;
59
86
  } },
60
87
  onUpdate({ editor: e }) {
61
- B?.(e.getHTML()), te?.(e.storage.markdown.getMarkdown());
88
+ z?.(e.getHTML()), V?.(e.storage.markdown.getMarkdown());
62
89
  },
63
90
  onBlur() {
64
- z?.();
91
+ R?.();
65
92
  },
66
93
  onFocus() {
67
- V?.();
94
+ B?.();
68
95
  }
69
96
  });
70
97
  o(() => {
71
- !$ || L === void 0 || $.getHTML() !== L && $.commands.setContent(L, { emitUpdate: !1 });
72
- }, [$, L]), o(() => {
73
- $ && $.setEditable(!w && !j);
98
+ !$ || I === void 0 || $.getHTML() !== I && $.commands.setContent(I, { emitUpdate: !1 });
99
+ }, [$, I]), o(() => {
100
+ $ && $.setEditable(!x && !A);
74
101
  }, [
75
102
  $,
76
- w,
77
- j
103
+ x,
104
+ A
78
105
  ]);
79
- let re = $?.storage.characterCount?.characters?.() ?? 0, ie = $?.storage.characterCount?.words?.() ?? 0, ae = M || O !== void 0 && O > 0 || P || !!D;
80
- return /* @__PURE__ */ m(l, {
106
+ let fe = $?.storage.characterCount?.characters?.() ?? 0, pe = $?.storage.characterCount?.words?.() ?? 0, me = j || w !== void 0 && w > 0 || N || !!C;
107
+ return /* @__PURE__ */ p(l, {
81
108
  sx: W ? {
82
109
  position: "fixed",
83
110
  top: 0,
@@ -90,60 +117,60 @@ function w({ disabled: w = !1, error: T = !1, height: E, helperText: D, maxChara
90
117
  bgcolor: "background.default",
91
118
  p: 1
92
119
  } : {
93
- width: X ?? "100%",
94
- ...Z ? {
120
+ width: le ?? "100%",
121
+ ...X ? {
95
122
  display: "flex",
96
123
  flexDirection: "column",
97
124
  flex: 1
98
125
  } : {}
99
126
  },
100
127
  children: [
101
- /* @__PURE__ */ m(d, {
128
+ /* @__PURE__ */ p(ne, {
102
129
  variant: "outlined",
103
130
  sx: {
104
131
  display: "flex",
105
132
  flexDirection: "column",
106
133
  overflow: "hidden",
107
- ...W || Z ? { flex: 1 } : { height: Q },
108
- borderColor: T ? "error.main" : void 0,
134
+ ...W || X ? { flex: 1 } : { height: ue },
135
+ borderColor: S ? "error.main" : void 0,
109
136
  "&:focus-within": {
110
- borderColor: T ? "error.main" : "primary.main",
137
+ borderColor: S ? "error.main" : "primary.main",
111
138
  borderWidth: 2
112
139
  }
113
140
  },
114
- children: [N && !j && /* @__PURE__ */ m(f, { children: [/* @__PURE__ */ p(i, {
141
+ children: [M && !A && /* @__PURE__ */ p(d, { children: [/* @__PURE__ */ f(a, {
115
142
  editor: $,
116
143
  toolbarConfig: U,
117
144
  translation: H,
118
- disabled: w,
145
+ disabled: x,
119
146
  isFullscreen: W,
120
147
  onToggleFullscreen: () => G((e) => !e),
121
148
  pasteAsPlainText: K,
122
- onTogglePasteAsPlainText: () => q((e) => !e)
123
- }), /* @__PURE__ */ p(u, {})] }), /* @__PURE__ */ p(r, {
149
+ onTogglePasteAsPlainText: () => ce((e) => !e)
150
+ }), /* @__PURE__ */ f(u, {})] }), /* @__PURE__ */ f(i, {
124
151
  editor: $,
125
- error: T,
126
- disabled: w,
127
- readonly: j
152
+ error: S,
153
+ disabled: x,
154
+ readonly: A
128
155
  })]
129
156
  }),
130
- ae && /* @__PURE__ */ p(a, {
131
- helperText: D,
132
- error: T,
133
- showCharacterCount: M || O !== void 0 && O > 0,
134
- charCount: re,
135
- maxCharacters: O && O > 0 ? O : void 0,
136
- showWordCount: P,
137
- wordCount: ie,
157
+ me && /* @__PURE__ */ f(ee, {
158
+ helperText: C,
159
+ error: S,
160
+ showCharacterCount: j || w !== void 0 && w > 0,
161
+ charCount: fe,
162
+ maxCharacters: w && w > 0 ? w : void 0,
163
+ showWordCount: N,
164
+ wordCount: pe,
138
165
  translation: H
139
166
  }),
140
- k && /* @__PURE__ */ p("input", {
167
+ D && /* @__PURE__ */ f("input", {
141
168
  type: "hidden",
142
- name: k,
169
+ name: D,
143
170
  value: $ ? $.getHTML() : ""
144
171
  })
145
172
  ]
146
173
  });
147
174
  }
148
175
  //#endregion
149
- export { w as RichTextEditor };
176
+ export { x as RichTextEditor };
@@ -1,3 +1,7 @@
1
+ export type MentionItem = {
2
+ id: string;
3
+ label: string;
4
+ };
1
5
  export type RichTextEditorToolbarConfig = {
2
6
  showBold?: boolean;
3
7
  showItalic?: boolean;
@@ -99,6 +103,8 @@ export type RichTextEditorTranslation = {
99
103
  markdownDialogCopy?: string;
100
104
  /** @since 3.8.0 */
101
105
  markdownDialogCopied?: string;
106
+ /** Text shown in mention dropdown when no items match the query @since 3.14.0 */
107
+ mentionNoResults?: string;
102
108
  };
103
109
  export declare const DEFAULT_RICH_TEXT_EDITOR_TRANSLATION: Required<RichTextEditorTranslation>;
104
110
  export type RichTextEditorProps = {
@@ -121,6 +127,24 @@ export type RichTextEditorProps = {
121
127
  value?: string;
122
128
  /** Breite des Editors. Zahlen → px. "auto" oder leer → 100% des Elternelements. */
123
129
  width?: number | string;
130
+ /**
131
+ * List of items for @-mention autocomplete. The Mention extension is only active when this prop is provided.
132
+ * Used for client-side filtering when `onMentionSearch` is not provided.
133
+ * @since 3.14.0
134
+ */
135
+ mentionItems?: MentionItem[];
136
+ /**
137
+ * Custom search/filter function called on each keystroke after @.
138
+ * When provided, `mentionItems` is ignored for filtering — return the relevant subset.
139
+ * Supports async for server-side search.
140
+ * @since 3.14.0
141
+ */
142
+ onMentionSearch?: (query: string) => MentionItem[] | Promise<MentionItem[]>;
143
+ /**
144
+ * Character that triggers the mention autocomplete popup. Default: "@"
145
+ * @since 3.14.0
146
+ */
147
+ mentionTriggerChar?: string;
124
148
  onBlur?: () => void;
125
149
  onChange?: (value: string) => void;
126
150
  onFocus?: () => void;
@@ -79,7 +79,8 @@ var e = {
79
79
  markdownDialogApply: "Apply",
80
80
  markdownDialogCancel: "Cancel",
81
81
  markdownDialogCopy: "Copy",
82
- markdownDialogCopied: "Copied!"
82
+ markdownDialogCopied: "Copied!",
83
+ mentionNoResults: "No results"
83
84
  };
84
85
  //#endregion
85
86
  export { e as DEFAULT_RICH_TEXT_EDITOR_TOOLBAR_CONFIG, t as DEFAULT_RICH_TEXT_EDITOR_TRANSLATION };
@@ -0,0 +1,11 @@
1
+ import type { MentionItem } from "./RichTextEditor.types";
2
+ export type MentionListProps = {
3
+ items: MentionItem[];
4
+ noResultsLabel: string;
5
+ clientRect: (() => DOMRect | null) | null;
6
+ onSelect: (item: MentionItem) => void;
7
+ };
8
+ export type MentionListRef = {
9
+ onKeyDown: (event: KeyboardEvent) => boolean;
10
+ };
11
+ export declare const RichTextEditorMentionList: import("react").ForwardRefExoticComponent<MentionListProps & import("react").RefAttributes<MentionListRef>>;
@@ -0,0 +1,54 @@
1
+ import { forwardRef as e, useEffect as t, useImperativeHandle as n, useRef as r, useState as i } from "react";
2
+ import { List as a, ListItemButton as o, Paper as s, Popper as c, Typography as l } from "@mui/material";
3
+ import { jsx as u } from "react/jsx-runtime";
4
+ //#region src/components/rich-text-editor/RichTextEditorMentionList.tsx
5
+ var d = e(function({ items: e, noResultsLabel: d, clientRect: f, onSelect: p }, m) {
6
+ let [h, g] = i(0), [_, v] = i(e), y = r(null);
7
+ e !== _ && (v(e), g(0)), t(() => {
8
+ (y.current?.children[h])?.scrollIntoView({ block: "nearest" });
9
+ }, [h]), n(m, () => ({ onKeyDown(t) {
10
+ return t.key === "ArrowUp" ? (g((t) => (t - 1 + e.length) % e.length), !0) : t.key === "ArrowDown" ? (g((t) => (t + 1) % e.length), !0) : t.key === "Enter" ? (e[h] && p(e[h]), !0) : !1;
11
+ } }));
12
+ let b = f ? { getBoundingClientRect: f } : null;
13
+ return /* @__PURE__ */ u(c, {
14
+ open: !!b,
15
+ anchorEl: b,
16
+ placement: "bottom-start",
17
+ modifiers: [{
18
+ name: "offset",
19
+ options: { offset: [0, 4] }
20
+ }],
21
+ style: { zIndex: 1400 },
22
+ children: /* @__PURE__ */ u(s, {
23
+ elevation: 4,
24
+ sx: {
25
+ maxHeight: 240,
26
+ overflow: "auto",
27
+ minWidth: 180
28
+ },
29
+ children: e.length === 0 ? /* @__PURE__ */ u(l, {
30
+ variant: "body2",
31
+ sx: {
32
+ px: 2,
33
+ py: 1,
34
+ color: "text.secondary"
35
+ },
36
+ children: d
37
+ }) : /* @__PURE__ */ u(a, {
38
+ ref: y,
39
+ dense: !0,
40
+ disablePadding: !0,
41
+ children: e.map((e, t) => /* @__PURE__ */ u(o, {
42
+ selected: t === h,
43
+ onMouseDown: (t) => {
44
+ t.preventDefault(), p(e);
45
+ },
46
+ onMouseEnter: () => g(t),
47
+ children: e.label
48
+ }, e.id))
49
+ })
50
+ })
51
+ });
52
+ });
53
+ //#endregion
54
+ export { d as RichTextEditorMentionList };
@@ -0,0 +1,9 @@
1
+ import type { SuggestionOptions } from "@tiptap/suggestion";
2
+ import type { MentionItem } from "./RichTextEditor.types";
3
+ type BuildSuggestionOptions = {
4
+ getItems: () => MentionItem[];
5
+ onMentionSearch?: (query: string) => MentionItem[] | Promise<MentionItem[]>;
6
+ noResultsLabel: string;
7
+ };
8
+ export declare function buildMentionSuggestion({ getItems, onMentionSearch, noResultsLabel, }: BuildSuggestionOptions): Partial<SuggestionOptions<MentionItem>>;
9
+ export {};
@@ -0,0 +1,45 @@
1
+ import { RichTextEditorMentionList as e } from "./RichTextEditorMentionList.js";
2
+ import { ReactRenderer as t } from "@tiptap/react";
3
+ //#region src/components/rich-text-editor/RichTextEditorMentionSuggestion.ts
4
+ function n({ getItems: n, onMentionSearch: r, noResultsLabel: i }) {
5
+ return {
6
+ items: async ({ query: e }) => {
7
+ if (r) return r(e);
8
+ let t = n(), i = e.toLowerCase();
9
+ return t.filter((e) => e.label.toLowerCase().includes(i));
10
+ },
11
+ render() {
12
+ let n = null;
13
+ return {
14
+ onStart(r) {
15
+ n = new t(e, {
16
+ props: {
17
+ items: r.items,
18
+ noResultsLabel: i,
19
+ clientRect: r.clientRect,
20
+ onSelect: (e) => r.command({
21
+ id: e.id,
22
+ label: e.label
23
+ })
24
+ },
25
+ editor: r.editor
26
+ }), document.body.appendChild(n.element);
27
+ },
28
+ onUpdate(e) {
29
+ n?.updateProps({
30
+ items: e.items,
31
+ clientRect: e.clientRect
32
+ });
33
+ },
34
+ onKeyDown(e) {
35
+ return e.event.key === "Escape" ? (n?.destroy(), n = null, !0) : n?.ref?.onKeyDown(e.event) ?? !1;
36
+ },
37
+ onExit() {
38
+ n?.element.remove(), n?.destroy(), n = null;
39
+ }
40
+ };
41
+ }
42
+ };
43
+ }
44
+ //#endregion
45
+ export { n as buildMentionSuggestion };