@homecode/ui 5.1.18 → 5.3.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/dist/esm/index.js +5 -0
- package/dist/esm/src/components/Autocomplete/Autocomplete.js +13 -1
- package/dist/esm/src/components/InputFile/InputFile.js +5 -0
- package/dist/esm/src/components/PromptComposer/PromptComposer.helpers.js +44 -0
- package/dist/esm/src/components/PromptComposer/PromptComposer.js +35 -0
- package/dist/esm/src/components/PromptComposer/PromptComposer.styl.js +7 -0
- package/dist/esm/src/components/PromptComposer/promptComposerDoc.js +17 -0
- package/dist/esm/src/components/PromptComposer/promptComposerInsert.js +101 -0
- package/dist/esm/src/components/PromptComposer/usePromptComposerEditor.js +192 -0
- package/dist/esm/src/hooks/useListKeyboardControl.js +11 -3
- package/dist/esm/src/tiptap/slash-mention/SlashSuggestionList.js +57 -0
- package/dist/esm/src/tiptap/slash-mention/SlashSuggestionList.styl.js +7 -0
- package/dist/esm/src/tiptap/slash-mention/createSlashMentionExtension.js +202 -0
- package/dist/esm/src/tiptap/slash-mention/defaultChatSlashItems.js +12 -0
- package/dist/esm/types/node_modules/@tiptap/pm/state/index.d.ts +1 -0
- package/dist/esm/types/src/components/PromptComposer/PromptComposer.d.ts +13 -0
- package/dist/esm/types/src/components/PromptComposer/PromptComposer.helpers.d.ts +8 -0
- package/dist/esm/types/src/components/PromptComposer/PromptComposer.types.d.ts +19 -0
- package/dist/esm/types/src/components/PromptComposer/index.d.ts +7 -0
- package/dist/esm/types/src/components/PromptComposer/promptComposerDoc.d.ts +3 -0
- package/dist/esm/types/src/components/PromptComposer/promptComposerInsert.d.ts +42 -0
- package/dist/esm/types/src/components/PromptComposer/usePromptComposerEditor.d.ts +19 -0
- package/dist/esm/types/src/components/index.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +1 -0
- package/dist/esm/types/src/tiptap/slash-mention/SlashSuggestionList.d.ts +11 -0
- package/dist/esm/types/src/tiptap/slash-mention/createSlashMentionExtension.d.ts +21 -0
- package/dist/esm/types/src/tiptap/slash-mention/defaultChatSlashItems.d.ts +4 -0
- package/dist/esm/types/src/tiptap/slash-mention/index.d.ts +5 -0
- package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +28 -0
- package/package.json +8 -1
package/dist/esm/index.js
CHANGED
|
@@ -33,6 +33,9 @@ export { Portal } from './src/components/Portal/Portal.js';
|
|
|
33
33
|
export { PopupMenu } from './src/components/PopupMenu/PopupMenu.js';
|
|
34
34
|
export { Progress } from './src/components/Progress/Progress.js';
|
|
35
35
|
export { ProgressCircular } from './src/components/ProgressCircular/ProgressCircular.js';
|
|
36
|
+
export { PromptComposer } from './src/components/PromptComposer/PromptComposer.js';
|
|
37
|
+
export { usePromptComposerEditor } from './src/components/PromptComposer/usePromptComposerEditor.js';
|
|
38
|
+
export { PROMPT_COMPOSER_COMMAND_CHIP_CLASS, createPromptComposerHandle, getPromptComposerTokenRangeBeforePos, insertPromptComposerContentAtCaret, promptComposerChipHtml, promptComposerMentionNode } from './src/components/PromptComposer/promptComposerInsert.js';
|
|
36
39
|
export { RadioButton } from './src/components/RadioButton/RadioButton.js';
|
|
37
40
|
export { RadioGroup } from './src/components/RadioGroup/RadioGroup.js';
|
|
38
41
|
export { Router, RouterContext, RouterStore } from './src/components/Router/Router.js';
|
|
@@ -57,6 +60,8 @@ export { default as VirtualizedListScroll } from './src/components/Virtualized/L
|
|
|
57
60
|
import * as Virtualized_types from './src/components/Virtualized/Virtualized.types.js';
|
|
58
61
|
export { Virtualized_types as VirtualizedTypes };
|
|
59
62
|
export { Toggle } from './src/components/Toggle/Toggle.js';
|
|
63
|
+
export { DEFAULT_CHAT_SLASH_ITEMS, filterSlashItems } from './src/tiptap/slash-mention/defaultChatSlashItems.js';
|
|
64
|
+
export { createSlashMentionExtension, slashMentionSuggestionRender } from './src/tiptap/slash-mention/createSlashMentionExtension.js';
|
|
60
65
|
import * as i18n from './src/services/i18n.js';
|
|
61
66
|
export { i18n };
|
|
62
67
|
import * as env from './src/tools/env.js';
|
|
@@ -46,6 +46,7 @@ import '../Gallery/Gallery.styl.js';
|
|
|
46
46
|
import '../Heading/Heading.js';
|
|
47
47
|
import '../InputFile/InputFile.styl.js';
|
|
48
48
|
import '../InputFile/Item/Item.styl.js';
|
|
49
|
+
import '../../tiptap/slash-mention/createSlashMentionExtension.js';
|
|
49
50
|
import '../../services/i18n.js';
|
|
50
51
|
import '../Portal/Portal.js';
|
|
51
52
|
import '../Paranja/Paranja.styl.js';
|
|
@@ -56,6 +57,10 @@ import '../Notifications/Notifications.styl.js';
|
|
|
56
57
|
import '../PopupMenu/PopupMenu.styl.js';
|
|
57
58
|
import '../Progress/Progress.styl.js';
|
|
58
59
|
import '../ProgressCircular/ProgressCircular.styl.js';
|
|
60
|
+
import '../PromptComposer/PromptComposer.js';
|
|
61
|
+
import '@tiptap/extensions';
|
|
62
|
+
import '@tiptap/react';
|
|
63
|
+
import '@tiptap/starter-kit';
|
|
59
64
|
import '../RadioGroup/RadioGroupContext.js';
|
|
60
65
|
import '../RadioButton/RadioButton.styl.js';
|
|
61
66
|
import '../RadioGroup/RadioGroup.styl.js';
|
|
@@ -162,6 +167,8 @@ function Autocomplete(props) {
|
|
|
162
167
|
return true;
|
|
163
168
|
};
|
|
164
169
|
const handleSelect = (option) => {
|
|
170
|
+
if (!option)
|
|
171
|
+
return;
|
|
165
172
|
if (selectable) {
|
|
166
173
|
setSelectedId(option.id);
|
|
167
174
|
setSelectedLabel(option.label);
|
|
@@ -191,7 +198,12 @@ function Autocomplete(props) {
|
|
|
191
198
|
const { focusedIndex, setFocusedIndex } = useListKeyboardControl({
|
|
192
199
|
isActive: isPopupOpenForInput,
|
|
193
200
|
itemsCount: displayItems.length,
|
|
194
|
-
onSelect: index =>
|
|
201
|
+
onSelect: index => {
|
|
202
|
+
const option = displayItems[index];
|
|
203
|
+
if (!option)
|
|
204
|
+
return;
|
|
205
|
+
handleSelect(option);
|
|
206
|
+
},
|
|
195
207
|
});
|
|
196
208
|
const fetchOptionsCore = useCallback(async (filter, offset) => {
|
|
197
209
|
const requestKey = `${filter}:${offset}`;
|
|
@@ -55,6 +55,11 @@ import '../Notifications/Notifications.styl.js';
|
|
|
55
55
|
import '../PopupMenu/PopupMenu.styl.js';
|
|
56
56
|
import '../Progress/Progress.styl.js';
|
|
57
57
|
import '../ProgressCircular/ProgressCircular.styl.js';
|
|
58
|
+
import '../PromptComposer/PromptComposer.js';
|
|
59
|
+
import '@tiptap/extensions';
|
|
60
|
+
import '@tiptap/react';
|
|
61
|
+
import '@tiptap/starter-kit';
|
|
62
|
+
import '../../tiptap/slash-mention/createSlashMentionExtension.js';
|
|
58
63
|
import '../RadioGroup/RadioGroupContext.js';
|
|
59
64
|
import '../RadioButton/RadioButton.styl.js';
|
|
60
65
|
import '../RadioGroup/RadioGroup.styl.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Keep in sync with `PromptComposer.styl` max/min heights. */
|
|
2
|
+
const PROMPT_COMPOSER_MAX_HEIGHT_PX = 200;
|
|
3
|
+
const PROMPT_COMPOSER_MIN_HEIGHT_PX = 40;
|
|
4
|
+
function promptComposerSafeEditorDom(editor) {
|
|
5
|
+
if (!editor || editor.isDestroyed)
|
|
6
|
+
return null;
|
|
7
|
+
try {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const dom = editor.view?.dom;
|
|
10
|
+
return dom instanceof HTMLElement ? dom : null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function promptComposerFloorPx(cs) {
|
|
17
|
+
const padY = (Number.parseFloat(cs.paddingTop) || 0) +
|
|
18
|
+
(Number.parseFloat(cs.paddingBottom) || 0);
|
|
19
|
+
const fs = Number.parseFloat(cs.fontSize) || 14;
|
|
20
|
+
const linePx = cs.lineHeight === 'normal'
|
|
21
|
+
? fs * 1.25
|
|
22
|
+
: Number.parseFloat(cs.lineHeight) || fs * 1.25;
|
|
23
|
+
return Math.max(PROMPT_COMPOSER_MIN_HEIGHT_PX, Math.ceil(linePx + padY));
|
|
24
|
+
}
|
|
25
|
+
/** Autosize `.ProseMirror` root: empty -> floor, text -> measured height with clamp. */
|
|
26
|
+
function syncPromptComposerHeight(el, text) {
|
|
27
|
+
const floor = promptComposerFloorPx(getComputedStyle(el));
|
|
28
|
+
el.style.overflowY = 'hidden';
|
|
29
|
+
let contentHeight;
|
|
30
|
+
const empty = text.trim() === '';
|
|
31
|
+
if (empty) {
|
|
32
|
+
contentHeight = floor;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
el.style.height = '0';
|
|
36
|
+
contentHeight = el.scrollHeight;
|
|
37
|
+
}
|
|
38
|
+
const h = Math.min(Math.max(contentHeight, floor), PROMPT_COMPOSER_MAX_HEIGHT_PX);
|
|
39
|
+
el.style.height = `${h}px`;
|
|
40
|
+
el.style.overflowY =
|
|
41
|
+
contentHeight > PROMPT_COMPOSER_MAX_HEIGHT_PX ? 'auto' : 'hidden';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { PROMPT_COMPOSER_MAX_HEIGHT_PX, PROMPT_COMPOSER_MIN_HEIGHT_PX, promptComposerSafeEditorDom, syncPromptComposerHeight };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import { forwardRef, useMemo, useImperativeHandle } from 'react';
|
|
4
|
+
import { EditorContent } from '@tiptap/react';
|
|
5
|
+
import S from './PromptComposer.styl.js';
|
|
6
|
+
import { createPromptComposerHandle } from './promptComposerInsert.js';
|
|
7
|
+
import { usePromptComposerEditor } from './usePromptComposerEditor.js';
|
|
8
|
+
|
|
9
|
+
const PromptComposer = forwardRef(function PromptComposer({ disabled = false, placeholder, className, slashCommandItems, onSlashItemCommand, prefillMessage, attachmentsCount = 0, allowEnterSubmit = true, onSubmit, onChange, }, ref) {
|
|
10
|
+
const { editor } = usePromptComposerEditor({
|
|
11
|
+
disabled,
|
|
12
|
+
placeholder,
|
|
13
|
+
slashCommandItems,
|
|
14
|
+
onSlashItemCommand,
|
|
15
|
+
prefillMessage,
|
|
16
|
+
attachmentsCount,
|
|
17
|
+
allowEnterSubmit,
|
|
18
|
+
onSubmit,
|
|
19
|
+
onChange,
|
|
20
|
+
});
|
|
21
|
+
const unavailableHandle = useMemo(() => ({
|
|
22
|
+
insertAtCaret: () => undefined,
|
|
23
|
+
focus: () => undefined,
|
|
24
|
+
reset: () => undefined,
|
|
25
|
+
setContent: () => undefined,
|
|
26
|
+
getText: () => '',
|
|
27
|
+
getEditor: () => {
|
|
28
|
+
throw new Error('PromptComposer editor is not ready yet');
|
|
29
|
+
},
|
|
30
|
+
}), []);
|
|
31
|
+
useImperativeHandle(ref, () => (editor ? createPromptComposerHandle(editor) : unavailableHandle), [editor, unavailableHandle]);
|
|
32
|
+
return (jsx("div", { className: cn(S.root, className), "data-disabled": disabled ? 'true' : 'false', children: jsx(EditorContent, { editor: editor, className: S.editorMount }) }));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export { PromptComposer };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import styleInject from '../../../node_modules/style-inject/dist/style-inject.es.js';
|
|
2
|
+
|
|
3
|
+
var css_248z = ".PromptComposer_root__gfdXN{width:100%}.PromptComposer_editorMount__B9G-o{background:transparent;border:none;border-radius:0!important;box-shadow:none!important;display:flex;flex:1;flex-direction:column;max-height:200px;min-height:40px;min-width:0;padding:0!important}.PromptComposer_editorMount__B9G-o:focus-within{box-shadow:none!important}.PromptComposer_editorMount__B9G-o .promptComposerEditor{border:none!important;box-shadow:none!important;flex:1;margin:0;max-height:200px!important;min-height:40px!important;outline:none!important;overflow-x:hidden!important;overflow-y:auto!important;padding:var(--p-2) 0 0!important;resize:none!important;white-space:pre-wrap;word-break:break-word}.PromptComposer_editorMount__B9G-o .promptComposerEmptyEditor .promptComposerEmptyNode:before{color:var(--muted-foreground);content:attr(data-placeholder);float:left;height:0;pointer-events:none}";
|
|
4
|
+
var S = {"root":"PromptComposer_root__gfdXN","editorMount":"PromptComposer_editorMount__B9G-o"};
|
|
5
|
+
styleInject(css_248z);
|
|
6
|
+
|
|
7
|
+
export { S as default };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const PROMPT_COMPOSER_EMPTY_DOC = {
|
|
2
|
+
type: 'doc',
|
|
3
|
+
content: [{ type: 'paragraph' }],
|
|
4
|
+
};
|
|
5
|
+
function promptComposerParagraphDoc(text) {
|
|
6
|
+
return {
|
|
7
|
+
type: 'doc',
|
|
8
|
+
content: [
|
|
9
|
+
{
|
|
10
|
+
type: 'paragraph',
|
|
11
|
+
content: text ? [{ type: 'text', text }] : [],
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { PROMPT_COMPOSER_EMPTY_DOC, promptComposerParagraphDoc };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { promptComposerParagraphDoc } from './promptComposerDoc.js';
|
|
2
|
+
|
|
3
|
+
const PROMPT_COMPOSER_COMMAND_CHIP_CLASS = 'prompt-composer-command-chip';
|
|
4
|
+
function getPromptComposerTokenRangeBeforePos(editor, pos) {
|
|
5
|
+
const $pos = editor.state.doc.resolve(pos);
|
|
6
|
+
const textBefore = $pos.parent.textBetween(0, $pos.parentOffset, undefined, '\ufffc');
|
|
7
|
+
const token = textBefore.match(/[^\s]*$/)?.[0] ?? '';
|
|
8
|
+
return { from: pos - token.length, to: pos };
|
|
9
|
+
}
|
|
10
|
+
function escapeHtml(text) {
|
|
11
|
+
return text
|
|
12
|
+
.replaceAll('&', '&')
|
|
13
|
+
.replaceAll('<', '<')
|
|
14
|
+
.replaceAll('>', '>')
|
|
15
|
+
.replaceAll('"', '"');
|
|
16
|
+
}
|
|
17
|
+
function promptComposerChipHtml(text, className = PROMPT_COMPOSER_COMMAND_CHIP_CLASS) {
|
|
18
|
+
const cls = className ? ` class="${className}"` : '';
|
|
19
|
+
return `<span${cls} data-prompt-composer-chip="true">${escapeHtml(text)}</span>`;
|
|
20
|
+
}
|
|
21
|
+
function promptComposerMentionNode(input) {
|
|
22
|
+
const { id, label = id, className = null, color = null, slashChar = '/', } = input;
|
|
23
|
+
return {
|
|
24
|
+
type: 'mention',
|
|
25
|
+
attrs: {
|
|
26
|
+
id,
|
|
27
|
+
label,
|
|
28
|
+
mentionSuggestionChar: slashChar,
|
|
29
|
+
className,
|
|
30
|
+
color,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function insertPromptComposerContentAtCaret(editor, content, options) {
|
|
35
|
+
if (editor.isDestroyed)
|
|
36
|
+
return;
|
|
37
|
+
const { replaceRange, replaceTriggerToken = true, trailingSpace = true, } = options ?? {};
|
|
38
|
+
const { from: selFrom, to: selTo } = editor.state.selection;
|
|
39
|
+
let from;
|
|
40
|
+
let to;
|
|
41
|
+
if (replaceRange) {
|
|
42
|
+
from = replaceRange.from;
|
|
43
|
+
to = replaceRange.to;
|
|
44
|
+
}
|
|
45
|
+
else if (replaceTriggerToken) {
|
|
46
|
+
({ from, to } = getPromptComposerTokenRangeBeforePos(editor, selFrom));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
from = selFrom;
|
|
50
|
+
to = selTo;
|
|
51
|
+
}
|
|
52
|
+
const chain = editor.chain().focus().deleteRange({ from, to });
|
|
53
|
+
if (typeof content === 'string') {
|
|
54
|
+
chain.insertContentAt(from, trailingSpace ? `${content} ` : content);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
chain.insertContentAt(from, content);
|
|
58
|
+
if (trailingSpace) {
|
|
59
|
+
chain.insertContent(' ');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
chain.run();
|
|
63
|
+
queueMicrotask(() => {
|
|
64
|
+
editor.view.dom.ownerDocument?.defaultView
|
|
65
|
+
?.getSelection?.()
|
|
66
|
+
?.collapseToEnd();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function setPromptComposerEditorText(editor, text, options) {
|
|
70
|
+
if (editor.isDestroyed)
|
|
71
|
+
return;
|
|
72
|
+
editor.commands.setContent(promptComposerParagraphDoc(text));
|
|
73
|
+
if (options?.focus) {
|
|
74
|
+
editor.commands.focus(options.atEnd === false ? 'start' : 'end');
|
|
75
|
+
}
|
|
76
|
+
else if (options?.atEnd !== false) {
|
|
77
|
+
queueMicrotask(() => {
|
|
78
|
+
editor.view.dom.ownerDocument?.defaultView
|
|
79
|
+
?.getSelection?.()
|
|
80
|
+
?.collapseToEnd();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function createPromptComposerHandle(editor) {
|
|
85
|
+
return {
|
|
86
|
+
insertAtCaret: (content, options) => insertPromptComposerContentAtCaret(editor, content, options),
|
|
87
|
+
focus: () => {
|
|
88
|
+
editor.commands.focus();
|
|
89
|
+
},
|
|
90
|
+
reset: () => {
|
|
91
|
+
editor.chain().clearContent().focus().run();
|
|
92
|
+
},
|
|
93
|
+
getText: () => editor.getText(),
|
|
94
|
+
setContent: (text, options) => {
|
|
95
|
+
setPromptComposerEditorText(editor, text, options);
|
|
96
|
+
},
|
|
97
|
+
getEditor: () => editor,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { PROMPT_COMPOSER_COMMAND_CHIP_CLASS, createPromptComposerHandle, getPromptComposerTokenRangeBeforePos, insertPromptComposerContentAtCaret, promptComposerChipHtml, promptComposerMentionNode };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { useRef, useCallback, useMemo, useState, useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
import { Placeholder } from '@tiptap/extensions';
|
|
3
|
+
import { useEditor } from '@tiptap/react';
|
|
4
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
5
|
+
import { DEFAULT_CHAT_SLASH_ITEMS } from '../../tiptap/slash-mention/defaultChatSlashItems.js';
|
|
6
|
+
import { createSlashMentionExtension } from '../../tiptap/slash-mention/createSlashMentionExtension.js';
|
|
7
|
+
import { promptComposerSafeEditorDom, syncPromptComposerHeight } from './PromptComposer.helpers.js';
|
|
8
|
+
import { PROMPT_COMPOSER_EMPTY_DOC, promptComposerParagraphDoc } from './promptComposerDoc.js';
|
|
9
|
+
|
|
10
|
+
const PROMPT_COMPOSER_EDITOR_CLASS = 'promptComposerEditor';
|
|
11
|
+
const PROMPT_COMPOSER_EMPTY_EDITOR_CLASS = 'promptComposerEmptyEditor';
|
|
12
|
+
const PROMPT_COMPOSER_EMPTY_NODE_CLASS = 'promptComposerEmptyNode';
|
|
13
|
+
function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, onSlashItemCommand, prefillMessage, attachmentsCount = 0, allowEnterSubmit, onSubmit, onChange, }) {
|
|
14
|
+
const slashOpenRef = useRef(false);
|
|
15
|
+
const onSlashItemCommandRef = useRef(onSlashItemCommand);
|
|
16
|
+
onSlashItemCommandRef.current = onSlashItemCommand;
|
|
17
|
+
const onSubmitRef = useRef(onSubmit);
|
|
18
|
+
onSubmitRef.current = onSubmit;
|
|
19
|
+
const onChangeRef = useRef(onChange);
|
|
20
|
+
onChangeRef.current = onChange;
|
|
21
|
+
const editorRef = useRef(null);
|
|
22
|
+
const suggestionActiveUpdater = useCallback((active) => {
|
|
23
|
+
slashOpenRef.current = active;
|
|
24
|
+
}, []);
|
|
25
|
+
const slashItemsFingerprint = JSON.stringify(slashCommandItems ?? null);
|
|
26
|
+
const slashItemsStable = useMemo(() => slashCommandItems ?? DEFAULT_CHAT_SLASH_ITEMS, [slashCommandItems, slashItemsFingerprint]);
|
|
27
|
+
const placeholderText = useMemo(() => {
|
|
28
|
+
if (placeholder === undefined || placeholder === null) {
|
|
29
|
+
return 'Type a message...';
|
|
30
|
+
}
|
|
31
|
+
const text = String(placeholder).trim();
|
|
32
|
+
return text === '' ? 'Type a message...' : text;
|
|
33
|
+
}, [placeholder]);
|
|
34
|
+
const ariaLabelComposer = useMemo(() => {
|
|
35
|
+
if (placeholder === undefined || placeholder === null)
|
|
36
|
+
return 'Message';
|
|
37
|
+
const text = String(placeholder).trim();
|
|
38
|
+
return text === '' ? 'Message' : text;
|
|
39
|
+
}, [placeholder]);
|
|
40
|
+
const extensions = useMemo(() => {
|
|
41
|
+
const exts = [
|
|
42
|
+
StarterKit.configure({
|
|
43
|
+
heading: false,
|
|
44
|
+
bulletList: false,
|
|
45
|
+
orderedList: false,
|
|
46
|
+
blockquote: false,
|
|
47
|
+
codeBlock: false,
|
|
48
|
+
horizontalRule: false,
|
|
49
|
+
}),
|
|
50
|
+
Placeholder.configure({
|
|
51
|
+
placeholder: placeholderText,
|
|
52
|
+
showOnlyWhenEditable: true,
|
|
53
|
+
emptyEditorClass: PROMPT_COMPOSER_EMPTY_EDITOR_CLASS,
|
|
54
|
+
emptyNodeClass: PROMPT_COMPOSER_EMPTY_NODE_CLASS,
|
|
55
|
+
}),
|
|
56
|
+
];
|
|
57
|
+
if (slashItemsStable.length > 0) {
|
|
58
|
+
exts.push(createSlashMentionExtension({
|
|
59
|
+
items: slashItemsStable,
|
|
60
|
+
suggestionPlacement: 'above',
|
|
61
|
+
onSuggestionUiActiveChange: suggestionActiveUpdater,
|
|
62
|
+
onItemCommand: ctx => onSlashItemCommandRef.current?.(ctx) === true,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
return exts;
|
|
66
|
+
}, [placeholderText, slashItemsStable, suggestionActiveUpdater]);
|
|
67
|
+
const [text, setText] = useState('');
|
|
68
|
+
const editorDomRef = useRef(null);
|
|
69
|
+
const bindEditorDom = useCallback(({ editor }) => {
|
|
70
|
+
const nextText = editor.getText();
|
|
71
|
+
setText(nextText);
|
|
72
|
+
queueMicrotask(() => {
|
|
73
|
+
const dom = promptComposerSafeEditorDom(editor);
|
|
74
|
+
if (!dom)
|
|
75
|
+
return;
|
|
76
|
+
editorDomRef.current = dom;
|
|
77
|
+
syncPromptComposerHeight(dom, nextText);
|
|
78
|
+
});
|
|
79
|
+
}, []);
|
|
80
|
+
const trimmedText = text.trim();
|
|
81
|
+
const trimmedTextRef = useRef(trimmedText);
|
|
82
|
+
trimmedTextRef.current = trimmedText;
|
|
83
|
+
const attachmentsCountRef = useRef(attachmentsCount);
|
|
84
|
+
attachmentsCountRef.current = attachmentsCount;
|
|
85
|
+
const allowEnterSubmitRef = useRef(allowEnterSubmit);
|
|
86
|
+
allowEnterSubmitRef.current = allowEnterSubmit;
|
|
87
|
+
const handleEditorKeyDown = useCallback((_view, event) => {
|
|
88
|
+
if (!(event.key === 'Enter' &&
|
|
89
|
+
!event.shiftKey &&
|
|
90
|
+
!event.metaKey &&
|
|
91
|
+
!event.ctrlKey)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (!allowEnterSubmitRef.current)
|
|
95
|
+
return false;
|
|
96
|
+
if (slashOpenRef.current)
|
|
97
|
+
return false;
|
|
98
|
+
if (!trimmedTextRef.current && attachmentsCountRef.current === 0) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
const activeEditor = editorRef.current;
|
|
103
|
+
if (!activeEditor)
|
|
104
|
+
return true;
|
|
105
|
+
onSubmitRef.current?.(activeEditor.getText(), activeEditor);
|
|
106
|
+
return true;
|
|
107
|
+
}, []);
|
|
108
|
+
const editor = useEditor({
|
|
109
|
+
extensions,
|
|
110
|
+
content: PROMPT_COMPOSER_EMPTY_DOC,
|
|
111
|
+
editable: !disabled,
|
|
112
|
+
immediatelyRender: true,
|
|
113
|
+
editorProps: {
|
|
114
|
+
attributes: {
|
|
115
|
+
spellcheck: 'true',
|
|
116
|
+
'aria-label': ariaLabelComposer,
|
|
117
|
+
class: PROMPT_COMPOSER_EDITOR_CLASS,
|
|
118
|
+
},
|
|
119
|
+
handleKeyDown: handleEditorKeyDown,
|
|
120
|
+
},
|
|
121
|
+
onTransaction: ({ editor: activeEditor }) => {
|
|
122
|
+
const dom = promptComposerSafeEditorDom(activeEditor);
|
|
123
|
+
const nextText = activeEditor.getText();
|
|
124
|
+
if (dom) {
|
|
125
|
+
syncPromptComposerHeight(dom, nextText);
|
|
126
|
+
}
|
|
127
|
+
setText(nextText);
|
|
128
|
+
onChangeRef.current?.(nextText, activeEditor);
|
|
129
|
+
},
|
|
130
|
+
onCreate: bindEditorDom,
|
|
131
|
+
}, [extensions, bindEditorDom, ariaLabelComposer, handleEditorKeyDown]);
|
|
132
|
+
editorRef.current = editor;
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!editor)
|
|
135
|
+
return;
|
|
136
|
+
editor.setEditable(!disabled);
|
|
137
|
+
}, [editor, disabled]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!editor)
|
|
140
|
+
return;
|
|
141
|
+
const bind = () => {
|
|
142
|
+
editorDomRef.current = promptComposerSafeEditorDom(editor);
|
|
143
|
+
};
|
|
144
|
+
bind();
|
|
145
|
+
let rafId = null;
|
|
146
|
+
if (typeof window !== 'undefined') {
|
|
147
|
+
rafId = window.requestAnimationFrame(bind);
|
|
148
|
+
}
|
|
149
|
+
return () => {
|
|
150
|
+
if (rafId != null)
|
|
151
|
+
cancelAnimationFrame(rafId);
|
|
152
|
+
};
|
|
153
|
+
}, [editor]);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!editor || prefillMessage == null)
|
|
156
|
+
return;
|
|
157
|
+
const current = editor.getText();
|
|
158
|
+
const next = prefillMessage;
|
|
159
|
+
if (current === next)
|
|
160
|
+
return;
|
|
161
|
+
if (next === '') {
|
|
162
|
+
editor.commands.setContent(PROMPT_COMPOSER_EMPTY_DOC);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
editor.commands.setContent(promptComposerParagraphDoc(next));
|
|
166
|
+
}
|
|
167
|
+
setText(editor.getText());
|
|
168
|
+
queueMicrotask(() => {
|
|
169
|
+
const dom = promptComposerSafeEditorDom(editor);
|
|
170
|
+
if (dom)
|
|
171
|
+
syncPromptComposerHeight(dom, editor.getText());
|
|
172
|
+
if (!disabled && !editor.isDestroyed) {
|
|
173
|
+
editor.chain().focus('end').run();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}, [editor, prefillMessage, disabled]);
|
|
177
|
+
useLayoutEffect(() => {
|
|
178
|
+
if (!editor)
|
|
179
|
+
return;
|
|
180
|
+
const dom = promptComposerSafeEditorDom(editor);
|
|
181
|
+
if (!dom)
|
|
182
|
+
return;
|
|
183
|
+
syncPromptComposerHeight(dom, text);
|
|
184
|
+
}, [editor, text]);
|
|
185
|
+
return {
|
|
186
|
+
editor,
|
|
187
|
+
text,
|
|
188
|
+
trimmedText,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export { usePromptComposerEditor };
|
|
@@ -26,7 +26,11 @@ const useListKeyboardControl = ({ isActive, itemsCount, onSelect, }) => {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
if (e.key === 'Enter') {
|
|
29
|
-
|
|
29
|
+
if (newIndex < 0 || newIndex >= itemsCount)
|
|
30
|
+
return;
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
e.stopPropagation();
|
|
33
|
+
onSelect?.(newIndex);
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
32
36
|
}, [focusedIndex, itemsCount, onSelect]);
|
|
@@ -37,10 +41,14 @@ const useListKeyboardControl = ({ isActive, itemsCount, onSelect, }) => {
|
|
|
37
41
|
isActive,
|
|
38
42
|
});
|
|
39
43
|
useEffect(() => {
|
|
40
|
-
if (itemsCount
|
|
44
|
+
if (itemsCount <= 0) {
|
|
45
|
+
setFocusedIndex(-1);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (focusedIndex >= itemsCount) {
|
|
41
49
|
setFocusedIndex(itemsCount - 1);
|
|
42
50
|
}
|
|
43
|
-
}, [itemsCount]);
|
|
51
|
+
}, [itemsCount, focusedIndex]);
|
|
44
52
|
return {
|
|
45
53
|
focusedIndex,
|
|
46
54
|
setFocusedIndex,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import { useState, useEffect, useCallback, useImperativeHandle } from 'react';
|
|
4
|
+
import { Menu } from '../../components/Menu/Menu.js';
|
|
5
|
+
import S from './SlashSuggestionList.styl.js';
|
|
6
|
+
|
|
7
|
+
function SlashSuggestionListInner({ items, command, listHandleRef, }) {
|
|
8
|
+
const [selected, setSelected] = useState(0);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
setSelected(s => Math.min(s, Math.max(items.length - 1, 0)));
|
|
11
|
+
}, [items.length]);
|
|
12
|
+
const safeSel = Math.min(selected, Math.max(items.length - 1, 0));
|
|
13
|
+
const onPick = useCallback((index) => {
|
|
14
|
+
const item = items[index];
|
|
15
|
+
if (!item)
|
|
16
|
+
return;
|
|
17
|
+
command(item);
|
|
18
|
+
}, [command, items]);
|
|
19
|
+
const onKeyboardEvent = useCallback((event) => {
|
|
20
|
+
if (items.length === 0)
|
|
21
|
+
return false;
|
|
22
|
+
if (event.key === 'ArrowDown') {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
setSelected(s => Math.min(s + 1, items.length - 1));
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (event.key === 'ArrowUp') {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
setSelected(s => Math.max(s - 1, 0));
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (event.key === 'Enter') {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
const max = Math.max(items.length - 1, 0);
|
|
35
|
+
const idx = Math.min(selected, max);
|
|
36
|
+
const item = items[idx];
|
|
37
|
+
if (item)
|
|
38
|
+
command(item);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (event.key === 'Escape') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}, [command, items, selected]);
|
|
46
|
+
useImperativeHandle(listHandleRef, () => ({ onKeyboardEvent }), [
|
|
47
|
+
onKeyboardEvent,
|
|
48
|
+
]);
|
|
49
|
+
return (jsx("div", { className: S.root, children: jsx(Menu, { className: S.menu, role: "listbox", "aria-label": "Slash commands", children: items.map((item, idx) => (jsxs(Menu.Item, { role: "option", "aria-selected": idx === safeSel, className: cn(S.item, idx === safeSel && S.itemHighlighted), onMouseDown: e => e.preventDefault(), onMouseEnter: () => setSelected(idx), onClick: () => onPick(idx), textOverflow: true, children: [jsxs("span", { className: S.itemLabel, children: ["/", item.label] }), item.description ? (jsx("span", { className: S.itemDesc, children: item.description })) : null] }, `${item.id}-${idx}`))) }) }));
|
|
50
|
+
}
|
|
51
|
+
function SlashSuggestionList(props) {
|
|
52
|
+
if (props.items.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
return (jsx(SlashSuggestionListInner, { items: props.items, command: props.command, listHandleRef: props.listHandleRef }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { SlashSuggestionList };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import styleInject from '../../../node_modules/style-inject/dist/style-inject.es.js';
|
|
2
|
+
|
|
3
|
+
var css_248z = ".SlashSuggestionList_root__9KCb1{background-color:var(--decent-color);border-radius:8px;border-radius:var(--radius-lg,8px);box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 8px 24px rgba(0,0,0,.2);color:var(--foreground);max-width:90vw;min-width:220px;overflow:hidden;position:relative}.SlashSuggestionList_root__9KCb1:before{background-color:var(--accent-color-alpha-50);bottom:0;content:\"\";left:0;pointer-events:none;position:absolute;right:0;top:0}.dark .SlashSuggestionList_root__9KCb1{box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 8px 24px rgba(0,0,0,.5)}.SlashSuggestionList_menu__s-jK3{margin:0;max-height:40vh}.SlashSuggestionList_item__9JBeY{align-items:flex-start;border-radius:var(--p-2);display:flex;flex-direction:column;height:auto!important;justify-content:center;min-height:0;overflow:visible!important;padding:var(--p-1) var(--p-2)!important;position:relative;text-overflow:clip!important;white-space:normal!important;width:100%}.SlashSuggestionList_itemHighlighted__cStZr,.SlashSuggestionList_item__9JBeY:hover{box-shadow:inset 100vw 0 0 0 var(--accent-color-alpha-200)}.SlashSuggestionList_itemLabel__OMaov{font-weight:600}.SlashSuggestionList_itemDesc__fAdcA{color:var(--muted-foreground);font-size:80%;white-space:normal}.SlashSuggestionList_mention__aT45p{align-items:center;background:var(--muted);border-radius:var(--p-1);display:inline-flex;line-height:1.4;margin:0 var(--p-1);padding:0 var(--p-1);-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:baseline;white-space:nowrap}";
|
|
4
|
+
var S = {"root":"SlashSuggestionList_root__9KCb1","menu":"SlashSuggestionList_menu__s-jK3","item":"SlashSuggestionList_item__9JBeY","itemHighlighted":"SlashSuggestionList_itemHighlighted__cStZr","itemLabel":"SlashSuggestionList_itemLabel__OMaov","itemDesc":"SlashSuggestionList_itemDesc__fAdcA","mention":"SlashSuggestionList_mention__aT45p"};
|
|
5
|
+
styleInject(css_248z);
|
|
6
|
+
|
|
7
|
+
export { S as default };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { mergeAttributes } from '@tiptap/core';
|
|
2
|
+
import Mention from '@tiptap/extension-mention';
|
|
3
|
+
import { ReactRenderer } from '@tiptap/react';
|
|
4
|
+
import { SlashSuggestionList } from './SlashSuggestionList.js';
|
|
5
|
+
import S from './SlashSuggestionList.styl.js';
|
|
6
|
+
import { filterSlashItems } from './defaultChatSlashItems.js';
|
|
7
|
+
|
|
8
|
+
const SlashMention = Mention.extend({
|
|
9
|
+
addAttributes() {
|
|
10
|
+
return {
|
|
11
|
+
...this.parent?.(),
|
|
12
|
+
className: {
|
|
13
|
+
default: null,
|
|
14
|
+
parseHTML: element => element.getAttribute('data-slash-class'),
|
|
15
|
+
renderHTML: attributes => {
|
|
16
|
+
if (!attributes.className)
|
|
17
|
+
return {};
|
|
18
|
+
return { 'data-slash-class': attributes.className };
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
color: {
|
|
22
|
+
default: null,
|
|
23
|
+
parseHTML: element => element.getAttribute('data-slash-color'),
|
|
24
|
+
renderHTML: attributes => {
|
|
25
|
+
if (!attributes.color)
|
|
26
|
+
return {};
|
|
27
|
+
return { 'data-slash-color': attributes.color };
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const SUGGESTION_GAP_PX = 4;
|
|
34
|
+
function placeSlashSuggestionPopup(popupElement, clientRect, placement) {
|
|
35
|
+
if (!clientRect)
|
|
36
|
+
return;
|
|
37
|
+
const el = popupElement;
|
|
38
|
+
const popupHeight = el.getBoundingClientRect().height;
|
|
39
|
+
const spaceBelow = window.innerHeight - clientRect.bottom - SUGGESTION_GAP_PX;
|
|
40
|
+
const spaceAbove = clientRect.top - SUGGESTION_GAP_PX;
|
|
41
|
+
let showAbove = placement === 'above';
|
|
42
|
+
if (placement === 'auto') {
|
|
43
|
+
showAbove = spaceBelow < popupHeight && spaceAbove >= spaceBelow;
|
|
44
|
+
}
|
|
45
|
+
const top = showAbove
|
|
46
|
+
? Math.max(SUGGESTION_GAP_PX, clientRect.top - popupHeight - SUGGESTION_GAP_PX)
|
|
47
|
+
: clientRect.bottom + SUGGESTION_GAP_PX;
|
|
48
|
+
el.style.left = `${clientRect.left}px`;
|
|
49
|
+
el.style.top = `${top}px`;
|
|
50
|
+
}
|
|
51
|
+
function slashMentionSuggestionRender(uiRef, placement = 'below') {
|
|
52
|
+
let popup = null;
|
|
53
|
+
const place = (props) => {
|
|
54
|
+
if (!popup?.element)
|
|
55
|
+
return;
|
|
56
|
+
placeSlashSuggestionPopup(popup.element, props.clientRect?.() ?? null, placement);
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
onStart: props => {
|
|
60
|
+
uiRef.current = null;
|
|
61
|
+
popup?.destroy?.();
|
|
62
|
+
popup = new ReactRenderer(SlashSuggestionList, {
|
|
63
|
+
editor: props.editor,
|
|
64
|
+
props: {
|
|
65
|
+
items: props.items,
|
|
66
|
+
command: props.command,
|
|
67
|
+
listHandleRef: uiRef,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
popup.element.style.position = 'fixed';
|
|
71
|
+
popup.element.style.margin = '0';
|
|
72
|
+
popup.element.style.zIndex = '10002';
|
|
73
|
+
document.body.append(popup.element);
|
|
74
|
+
place(props);
|
|
75
|
+
requestAnimationFrame(() => place(props));
|
|
76
|
+
},
|
|
77
|
+
onUpdate: props => {
|
|
78
|
+
if (!popup)
|
|
79
|
+
return;
|
|
80
|
+
popup.updateProps({
|
|
81
|
+
items: props.items,
|
|
82
|
+
command: props.command,
|
|
83
|
+
listHandleRef: uiRef,
|
|
84
|
+
});
|
|
85
|
+
place(props);
|
|
86
|
+
requestAnimationFrame(() => place(props));
|
|
87
|
+
},
|
|
88
|
+
onExit: () => {
|
|
89
|
+
popup?.destroy();
|
|
90
|
+
popup?.element?.remove?.();
|
|
91
|
+
popup = null;
|
|
92
|
+
uiRef.current = null;
|
|
93
|
+
},
|
|
94
|
+
onKeyDown: ({ event }) => uiRef.current?.onKeyboardEvent(event) ?? false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function slashMentionChipStyle(color) {
|
|
98
|
+
if (!color)
|
|
99
|
+
return undefined;
|
|
100
|
+
return `color: ${color}; background-color: color-mix(in srgb, ${color} 30%, transparent);`;
|
|
101
|
+
}
|
|
102
|
+
function insertDefaultMention(editor, range, item, slashChar) {
|
|
103
|
+
const nodeAfter = editor.view.state.selection.$to.nodeAfter;
|
|
104
|
+
const extend = nodeAfter?.text?.startsWith(' ') ? 1 : 0;
|
|
105
|
+
const adjusted = extend ? { ...range, to: range.to + extend } : range;
|
|
106
|
+
editor
|
|
107
|
+
.chain()
|
|
108
|
+
.focus()
|
|
109
|
+
.insertContentAt(adjusted, [
|
|
110
|
+
{
|
|
111
|
+
type: 'mention',
|
|
112
|
+
attrs: {
|
|
113
|
+
id: item.id,
|
|
114
|
+
label: item.label,
|
|
115
|
+
mentionSuggestionChar: slashChar,
|
|
116
|
+
className: item.className ?? null,
|
|
117
|
+
color: item.color ?? null,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{ type: 'text', text: ' ' },
|
|
121
|
+
])
|
|
122
|
+
.run();
|
|
123
|
+
queueMicrotask(() => {
|
|
124
|
+
editor.view.dom.ownerDocument?.defaultView
|
|
125
|
+
?.getSelection?.()
|
|
126
|
+
?.collapseToEnd();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function allowSlashTrigger({ state, range, }) {
|
|
130
|
+
const type = state.schema.nodes['mention'];
|
|
131
|
+
const $from = state.doc.resolve(range.from);
|
|
132
|
+
if (!type || !$from.parent.type.contentMatch.matchType(type))
|
|
133
|
+
return false;
|
|
134
|
+
if ($from.parentOffset === 0)
|
|
135
|
+
return true;
|
|
136
|
+
const before = state.doc.textBetween(range.from - 1, range.from);
|
|
137
|
+
return /\s/.test(before);
|
|
138
|
+
}
|
|
139
|
+
function createSlashMentionExtension({ items: resolvedItems, slashChar = '/', pluginKey, onItemCommand, suggestionPlacement = 'below', onSuggestionUiActiveChange, }) {
|
|
140
|
+
const uiRef = {
|
|
141
|
+
current: null,
|
|
142
|
+
};
|
|
143
|
+
return SlashMention.configure({
|
|
144
|
+
renderText({ node }) {
|
|
145
|
+
return `/${node.attrs.id}`;
|
|
146
|
+
},
|
|
147
|
+
renderHTML({ options, node }) {
|
|
148
|
+
const id = typeof node.attrs.id === 'string'
|
|
149
|
+
? node.attrs.id
|
|
150
|
+
: String(node.attrs.id ?? '');
|
|
151
|
+
const label = typeof node.attrs.label === 'string' && node.attrs.label.trim() !== ''
|
|
152
|
+
? node.attrs.label
|
|
153
|
+
: id;
|
|
154
|
+
const color = typeof node.attrs.color === 'string' ? node.attrs.color : null;
|
|
155
|
+
const className = [S.mention, node.attrs.className]
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join(' ');
|
|
158
|
+
const chipStyle = slashMentionChipStyle(color);
|
|
159
|
+
return [
|
|
160
|
+
'span',
|
|
161
|
+
mergeAttributes({
|
|
162
|
+
'data-type': 'mention',
|
|
163
|
+
'data-slash-command': id,
|
|
164
|
+
class: className,
|
|
165
|
+
...(chipStyle ? { style: chipStyle } : {}),
|
|
166
|
+
}, options.HTMLAttributes),
|
|
167
|
+
label,
|
|
168
|
+
];
|
|
169
|
+
},
|
|
170
|
+
suggestion: {
|
|
171
|
+
char: slashChar,
|
|
172
|
+
pluginKey,
|
|
173
|
+
allowedPrefixes: [' ', '\n', '\t', '\r'],
|
|
174
|
+
allow: props => allowSlashTrigger(props),
|
|
175
|
+
items: ({ query }) => Promise.resolve(filterSlashItems(resolvedItems, query)),
|
|
176
|
+
command: ({ editor, range, props }) => {
|
|
177
|
+
const item = props;
|
|
178
|
+
if (onItemCommand?.({ editor, range, item }) === true) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
insertDefaultMention(editor, range, item, slashChar);
|
|
182
|
+
return null;
|
|
183
|
+
},
|
|
184
|
+
render: () => {
|
|
185
|
+
const menu = slashMentionSuggestionRender(uiRef, suggestionPlacement);
|
|
186
|
+
return {
|
|
187
|
+
...menu,
|
|
188
|
+
onStart: props => {
|
|
189
|
+
onSuggestionUiActiveChange?.(true);
|
|
190
|
+
menu.onStart?.(props);
|
|
191
|
+
},
|
|
192
|
+
onExit: props => {
|
|
193
|
+
menu.onExit?.(props);
|
|
194
|
+
onSuggestionUiActiveChange?.(false);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export { createSlashMentionExtension, slashMentionSuggestionRender };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Empty default; app passes slashCommandItems to enable menu. */
|
|
2
|
+
const DEFAULT_CHAT_SLASH_ITEMS = [];
|
|
3
|
+
function filterSlashItems(items, query) {
|
|
4
|
+
const q = query.trim().toLowerCase();
|
|
5
|
+
if (!q)
|
|
6
|
+
return items;
|
|
7
|
+
return items.filter(item => item.id.toLowerCase().includes(q) ||
|
|
8
|
+
item.label.toLowerCase().includes(q) ||
|
|
9
|
+
(item.description?.toLowerCase().includes(q) ?? false));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { DEFAULT_CHAT_SLASH_ITEMS, filterSlashItems };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from 'prosemirror-state';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PromptComposerHandle } from './promptComposerInsert';
|
|
2
|
+
export type { PromptComposerHandle, PromptComposerInsertOptions, } from './promptComposerInsert';
|
|
3
|
+
export declare const PromptComposer: import("react").ForwardRefExoticComponent<import("react").HTMLAttributes<HTMLDivElement> & {
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
slashCommandItems?: import(".").SlashCommandItem[];
|
|
7
|
+
onSlashItemCommand?: import(".").SlashOnItemCommand;
|
|
8
|
+
prefillMessage?: string;
|
|
9
|
+
attachmentsCount?: number;
|
|
10
|
+
allowEnterSubmit?: boolean;
|
|
11
|
+
onSubmit?: (text: string, editor: import("@tiptap/core").Editor) => void;
|
|
12
|
+
onChange?: (text: string, editor: import("@tiptap/core").Editor) => void;
|
|
13
|
+
} & import("react").RefAttributes<PromptComposerHandle>>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Keep in sync with `PromptComposer.styl` max/min heights. */
|
|
2
|
+
export declare const PROMPT_COMPOSER_MAX_HEIGHT_PX = 200;
|
|
3
|
+
export declare const PROMPT_COMPOSER_MIN_HEIGHT_PX = 40;
|
|
4
|
+
export declare function promptComposerSafeEditorDom(editor: {
|
|
5
|
+
readonly isDestroyed?: boolean;
|
|
6
|
+
} | null | undefined): HTMLElement | null;
|
|
7
|
+
/** Autosize `.ProseMirror` root: empty -> floor, text -> measured height with clamp. */
|
|
8
|
+
export declare function syncPromptComposerHeight(el: HTMLElement, text: string): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/core';
|
|
2
|
+
import type { HTMLAttributes } from 'react';
|
|
3
|
+
import type { SlashCommandItem, SlashOnItemCommand } from '../../tiptap/slash-mention';
|
|
4
|
+
export type Props = HTMLAttributes<HTMLDivElement> & {
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
slashCommandItems?: SlashCommandItem[];
|
|
8
|
+
onSlashItemCommand?: SlashOnItemCommand;
|
|
9
|
+
prefillMessage?: string | null;
|
|
10
|
+
/** Staged attachment count - Enter can submit even when text empty. */
|
|
11
|
+
attachmentsCount?: number;
|
|
12
|
+
/** Mobile usually sets false so Enter inserts newline. */
|
|
13
|
+
allowEnterSubmit?: boolean;
|
|
14
|
+
/** Called when user presses Enter to submit. */
|
|
15
|
+
onSubmit?: (text: string, editor: Editor) => void;
|
|
16
|
+
/** Called on every transaction with plain text value. */
|
|
17
|
+
onChange?: (text: string, editor: Editor) => void;
|
|
18
|
+
};
|
|
19
|
+
export type PromptComposerProps = Props;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { PromptComposer } from './PromptComposer';
|
|
2
|
+
export type { PromptComposerProps } from './PromptComposer.types';
|
|
3
|
+
export { usePromptComposerEditor } from './usePromptComposerEditor';
|
|
4
|
+
export type { UsePromptComposerEditorOptions } from './usePromptComposerEditor';
|
|
5
|
+
export { getPromptComposerTokenRangeBeforePos, createPromptComposerHandle, insertPromptComposerContentAtCaret, promptComposerChipHtml, promptComposerMentionNode, PROMPT_COMPOSER_COMMAND_CHIP_CLASS, } from './promptComposerInsert';
|
|
6
|
+
export type { PromptComposerHandle, PromptComposerInsertOptions, PromptComposerMentionNodeInput, } from './promptComposerInsert';
|
|
7
|
+
export type { SlashCommandItem, SlashItemCommandContext, SlashOnItemCommand, SlashSuggestionPlacement, } from '../../tiptap/slash-mention';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Editor, JSONContent } from '@tiptap/core';
|
|
2
|
+
export type PromptComposerInsertOptions = {
|
|
3
|
+
/** Explicit document range to replace before insert. */
|
|
4
|
+
replaceRange?: {
|
|
5
|
+
from: number;
|
|
6
|
+
to: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* If no replaceRange, delete token before caret back to whitespace/start.
|
|
10
|
+
* Default true.
|
|
11
|
+
*/
|
|
12
|
+
replaceTriggerToken?: boolean;
|
|
13
|
+
/** Add whitespace after inserted content. Default true. */
|
|
14
|
+
trailingSpace?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export declare const PROMPT_COMPOSER_COMMAND_CHIP_CLASS = "prompt-composer-command-chip";
|
|
17
|
+
export type PromptComposerHandle = {
|
|
18
|
+
insertAtCaret: (content: string | JSONContent, options?: PromptComposerInsertOptions) => void;
|
|
19
|
+
focus: () => void;
|
|
20
|
+
reset: () => void;
|
|
21
|
+
getText: () => string;
|
|
22
|
+
setContent: (text: string, options?: {
|
|
23
|
+
focus?: boolean;
|
|
24
|
+
atEnd?: boolean;
|
|
25
|
+
}) => void;
|
|
26
|
+
getEditor: () => Editor;
|
|
27
|
+
};
|
|
28
|
+
export declare function getPromptComposerTokenRangeBeforePos(editor: Editor, pos: number): {
|
|
29
|
+
from: number;
|
|
30
|
+
to: number;
|
|
31
|
+
};
|
|
32
|
+
export declare function promptComposerChipHtml(text: string, className?: string): string;
|
|
33
|
+
export type PromptComposerMentionNodeInput = {
|
|
34
|
+
id: string;
|
|
35
|
+
label?: string;
|
|
36
|
+
className?: string | null;
|
|
37
|
+
color?: string | null;
|
|
38
|
+
slashChar?: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function promptComposerMentionNode(input: PromptComposerMentionNodeInput): JSONContent;
|
|
41
|
+
export declare function insertPromptComposerContentAtCaret(editor: Editor, content: string | JSONContent, options?: PromptComposerInsertOptions): void;
|
|
42
|
+
export declare function createPromptComposerHandle(editor: Editor): PromptComposerHandle;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Editor } from '@tiptap/core';
|
|
2
|
+
import type { SlashCommandItem, SlashOnItemCommand } from '../../tiptap/slash-mention';
|
|
3
|
+
export type UsePromptComposerEditorOptions = {
|
|
4
|
+
disabled: boolean;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
slashCommandItems?: SlashCommandItem[];
|
|
7
|
+
onSlashItemCommand?: SlashOnItemCommand;
|
|
8
|
+
prefillMessage?: string | null;
|
|
9
|
+
attachmentsCount?: number;
|
|
10
|
+
allowEnterSubmit: boolean;
|
|
11
|
+
onSubmit?: (text: string, editor: Editor) => void;
|
|
12
|
+
onChange?: (text: string, editor: Editor) => void;
|
|
13
|
+
};
|
|
14
|
+
export type UsePromptComposerEditorResult = {
|
|
15
|
+
editor: Editor | null;
|
|
16
|
+
text: string;
|
|
17
|
+
trimmedText: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function usePromptComposerEditor({ disabled, placeholder, slashCommandItems, onSlashItemCommand, prefillMessage, attachmentsCount, allowEnterSubmit, onSubmit, onChange, }: UsePromptComposerEditorOptions): UsePromptComposerEditorResult;
|
|
@@ -33,6 +33,7 @@ export * from './Portal/Portal';
|
|
|
33
33
|
export * from './PopupMenu/PopupMenu';
|
|
34
34
|
export * from './Progress/Progress';
|
|
35
35
|
export * from './ProgressCircular/ProgressCircular';
|
|
36
|
+
export * from './PromptComposer';
|
|
36
37
|
export * from './RadioButton/RadioButton';
|
|
37
38
|
export * from './RadioGroup/RadioGroup';
|
|
38
39
|
export * from './Router/Router';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
import type { SlashCommandItem } from './types';
|
|
3
|
+
export type SlashSuggestionListHandle = {
|
|
4
|
+
onKeyboardEvent: (event: KeyboardEvent) => boolean;
|
|
5
|
+
};
|
|
6
|
+
export type SlashSuggestionListProps = {
|
|
7
|
+
items: SlashCommandItem[];
|
|
8
|
+
command: (item: SlashCommandItem) => void;
|
|
9
|
+
listHandleRef: RefObject<SlashSuggestionListHandle | null>;
|
|
10
|
+
};
|
|
11
|
+
export declare function SlashSuggestionList(props: SlashSuggestionListProps): JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
import type { SuggestionProps } from '@tiptap/suggestion';
|
|
3
|
+
import { type SlashSuggestionListHandle } from './SlashSuggestionList';
|
|
4
|
+
import type { CreateSlashMentionExtensionOptions, SlashCommandItem, SlashSuggestionPlacement } from './types';
|
|
5
|
+
export declare function slashMentionSuggestionRender(uiRef: MutableRefObject<SlashSuggestionListHandle | null>, placement?: SlashSuggestionPlacement): {
|
|
6
|
+
onStart?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
|
|
7
|
+
onUpdate?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
|
|
8
|
+
onExit?: (props: SuggestionProps<SlashCommandItem, SlashCommandItem>) => void;
|
|
9
|
+
onKeyDown?: (p: {
|
|
10
|
+
view: unknown;
|
|
11
|
+
event: KeyboardEvent;
|
|
12
|
+
range: {
|
|
13
|
+
from: number;
|
|
14
|
+
to: number;
|
|
15
|
+
};
|
|
16
|
+
}) => boolean;
|
|
17
|
+
};
|
|
18
|
+
export type CreateSlashMentionExtensionConfiguredOptions = CreateSlashMentionExtensionOptions & {
|
|
19
|
+
onSuggestionUiActiveChange?: (active: boolean) => void;
|
|
20
|
+
};
|
|
21
|
+
export declare function createSlashMentionExtension({ items: resolvedItems, slashChar, pluginKey, onItemCommand, suggestionPlacement, onSuggestionUiActiveChange, }: CreateSlashMentionExtensionConfiguredOptions): import("@tiptap/core").Node<import("@tiptap/extension-mention").MentionOptions<any, import("@tiptap/extension-mention").MentionNodeAttrs>, any>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SlashCommandItem } from './types';
|
|
2
|
+
/** Empty default; app passes slashCommandItems to enable menu. */
|
|
3
|
+
export declare const DEFAULT_CHAT_SLASH_ITEMS: SlashCommandItem[];
|
|
4
|
+
export declare function filterSlashItems(items: SlashCommandItem[], query: string): SlashCommandItem[];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { SlashCommandItem, SlashOnItemCommand, SlashItemCommandContext, SlashSuggestionPlacement, CreateSlashMentionExtensionOptions, } from './types';
|
|
2
|
+
export { DEFAULT_CHAT_SLASH_ITEMS, filterSlashItems, } from './defaultChatSlashItems';
|
|
3
|
+
export { createSlashMentionExtension, slashMentionSuggestionRender, } from './createSlashMentionExtension';
|
|
4
|
+
export type { CreateSlashMentionExtensionConfiguredOptions } from './createSlashMentionExtension';
|
|
5
|
+
export type { SlashSuggestionListHandle } from './SlashSuggestionList';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Editor, Range } from '@tiptap/core';
|
|
2
|
+
export type SlashCommandItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
/** Extra class on inserted mention chip (merged with `slash-mention`). */
|
|
7
|
+
className?: string;
|
|
8
|
+
/** CSS color for chip text; background uses 30% color blend. */
|
|
9
|
+
color?: string;
|
|
10
|
+
};
|
|
11
|
+
export type SlashItemCommandContext = {
|
|
12
|
+
item: SlashCommandItem;
|
|
13
|
+
/** Present when picked from slash menu; omitted on submit match. */
|
|
14
|
+
editor?: Editor;
|
|
15
|
+
range?: Range;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Return true to skip default mention insert and handle command in app.
|
|
19
|
+
*/
|
|
20
|
+
export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
|
|
21
|
+
export type SlashSuggestionPlacement = 'below' | 'above' | 'auto';
|
|
22
|
+
export type CreateSlashMentionExtensionOptions = {
|
|
23
|
+
items: SlashCommandItem[];
|
|
24
|
+
slashChar?: string;
|
|
25
|
+
pluginKey?: import('@tiptap/pm/state').PluginKey;
|
|
26
|
+
onItemCommand?: SlashOnItemCommand;
|
|
27
|
+
suggestionPlacement?: SlashSuggestionPlacement;
|
|
28
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homecode/ui",
|
|
3
|
-
"version": "5.1
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"description": "React UI components library",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"tests": "jest",
|
|
@@ -60,6 +60,13 @@
|
|
|
60
60
|
"@homecode/ui": "^4.17.2",
|
|
61
61
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
|
62
62
|
"@rollup/plugin-typescript": "^11.0.0",
|
|
63
|
+
"@tiptap/core": "^3.22.1",
|
|
64
|
+
"@tiptap/extension-mention": "^3.22.1",
|
|
65
|
+
"@tiptap/extensions": "^3.22.1",
|
|
66
|
+
"@tiptap/pm": "^3.22.1",
|
|
67
|
+
"@tiptap/react": "^3.22.1",
|
|
68
|
+
"@tiptap/starter-kit": "^3.22.1",
|
|
69
|
+
"@tiptap/suggestion": "^3.22.1",
|
|
63
70
|
"@svgr/webpack": "^8.1.0",
|
|
64
71
|
"classnames": "^2.3.2",
|
|
65
72
|
"compareq": "^1.2.2",
|