@sybilion/uilib 1.3.32 → 1.3.35

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.
Files changed (56) hide show
  1. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +2 -2
  2. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.helpers.js +18 -4
  3. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +39 -52
  4. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.styl.js +2 -2
  5. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptComposer.js +25 -0
  6. package/dist/esm/components/ui/Chat/ChatPrompt/chatPromptDoc.js +17 -0
  7. package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +163 -0
  8. package/dist/esm/components/ui/Tooltip/Tooltip.js +1 -1
  9. package/dist/esm/components/ui/Tooltip/Tooltip.styl.js +1 -1
  10. package/dist/esm/index.js +2 -0
  11. package/dist/esm/tiptap/slash-mention/SlashSuggestionList.js +53 -0
  12. package/dist/esm/tiptap/slash-mention/SlashSuggestionList.styl.js +7 -0
  13. package/dist/esm/tiptap/slash-mention/createSlashMentionExtension.js +151 -0
  14. package/dist/esm/tiptap/slash-mention/defaultChatSlashItems.js +12 -0
  15. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +3 -0
  16. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
  17. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +5 -0
  18. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +1 -1
  19. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.helpers.d.ts +6 -0
  20. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.d.ts +13 -0
  21. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/chatPromptDoc.d.ts +3 -0
  22. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.d.ts +20 -0
  23. package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -0
  24. package/dist/esm/types/src/components/ui/Input/Input.d.ts +1 -1
  25. package/dist/esm/types/src/docs/pages/ChatSlashCommandsPage.d.ts +1 -0
  26. package/dist/esm/types/src/index.d.ts +1 -0
  27. package/dist/esm/types/src/tiptap/slash-mention/SlashSuggestionList.d.ts +12 -0
  28. package/dist/esm/types/src/tiptap/slash-mention/createSlashMentionExtension.d.ts +21 -0
  29. package/dist/esm/types/src/tiptap/slash-mention/defaultChatSlashItems.d.ts +4 -0
  30. package/dist/esm/types/src/tiptap/slash-mention/index.d.ts +5 -0
  31. package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +25 -0
  32. package/package.json +15 -1
  33. package/src/components/ui/Chat/Chat.types.ts +4 -0
  34. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +4 -0
  35. package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +5 -0
  36. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.helpers.ts +43 -0
  37. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl +33 -5
  38. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl.d.ts +2 -1
  39. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +50 -106
  40. package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.tsx +93 -0
  41. package/src/components/ui/Chat/ChatPrompt/chatPromptDoc.ts +18 -0
  42. package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +214 -0
  43. package/src/components/ui/Chat/index.ts +1 -0
  44. package/src/components/ui/Tooltip/Tooltip.styl +1 -0
  45. package/src/components/ui/Tooltip/Tooltip.tsx +1 -1
  46. package/src/docs/pages/ChatSlashCommandsPage.tsx +139 -0
  47. package/src/docs/pages/TooltipPage.tsx +1 -1
  48. package/src/docs/registry.ts +6 -0
  49. package/src/index.ts +1 -0
  50. package/src/tiptap/slash-mention/SlashSuggestionList.styl +48 -0
  51. package/src/tiptap/slash-mention/SlashSuggestionList.styl.d.ts +11 -0
  52. package/src/tiptap/slash-mention/SlashSuggestionList.tsx +109 -0
  53. package/src/tiptap/slash-mention/createSlashMentionExtension.ts +217 -0
  54. package/src/tiptap/slash-mention/defaultChatSlashItems.ts +18 -0
  55. package/src/tiptap/slash-mention/index.ts +16 -0
  56. package/src/tiptap/slash-mention/types.ts +29 -0
@@ -1,22 +1,11 @@
1
1
  import cn from 'classnames';
2
- import {
3
- ChangeEvent,
4
- FormEvent,
5
- useEffect,
6
- useLayoutEffect,
7
- useRef,
8
- useState,
9
- } from 'react';
2
+ import { FormEvent, useCallback, useRef } from 'react';
10
3
 
11
- import useEvent from '#uilib/hooks/useEvent';
12
- import { PaperclipIcon, SendHorizontalIcon } from 'lucide-react';
13
-
14
- import { Button } from '../../Button';
15
- import { Input } from '../../Input';
16
4
  import type { ChatPromptProps } from '../Chat.types';
17
- import { syncChatPromptTextareaHeight } from './ChatPrompt.helpers';
18
5
  import S from './ChatPrompt.styl';
19
6
  import { ChatPromptAttachments } from './ChatPromptAttachments';
7
+ import { ChatPromptComposer } from './ChatPromptComposer';
8
+ import { useChatPromptEditor } from './useChatPromptEditor';
20
9
 
21
10
  export function ChatPrompt({
22
11
  onSubmit,
@@ -24,117 +13,72 @@ export function ChatPrompt({
24
13
  className,
25
14
  footer,
26
15
  prefillMessage,
16
+ slashCommandItems,
27
17
  attachments = [],
28
18
  onRemoveAttachment,
29
19
  disabled = false,
30
20
  attachmentAccept,
31
21
  onAttachmentFiles,
32
22
  }: ChatPromptProps) {
33
- const [message, setMessage] = useState('');
34
- const inputRef = useRef<HTMLTextAreaElement>(null);
35
- const fileInputRef = useRef<HTMLInputElement>(null);
36
- const showAttachButton = Boolean(attachmentAccept && onAttachmentFiles);
37
-
38
- useLayoutEffect(() => {
39
- const el = inputRef.current;
40
- if (!el) return;
41
- syncChatPromptTextareaHeight(el, message);
42
- }, [message]);
23
+ const attachmentsCount = attachments.length;
43
24
 
44
- useEffect(() => {
45
- if (prefillMessage != null && prefillMessage !== '') {
46
- setMessage(prefillMessage);
47
- }
48
- }, [prefillMessage]);
25
+ const emitSubmitRef = useRef(() => {});
26
+ const { editor, trimmedMessage, resetAfterSend, handleComposerKeyDown } =
27
+ useChatPromptEditor({
28
+ disabled,
29
+ placeholder,
30
+ slashCommandItems,
31
+ prefillMessage,
32
+ attachmentsCount,
33
+ onEnterSubmit: () => emitSubmitRef.current(),
34
+ });
49
35
 
50
- const handleSubmit = (e: FormEvent | KeyboardEvent) => {
51
- const trimmedMessage = message.trim();
52
- const hasAttachments = attachments.length > 0;
36
+ const emitSubmitAndClear = useCallback(() => {
37
+ if (!editor) return;
38
+ if (!trimmedMessage && attachmentsCount === 0) return;
53
39
 
54
- if (trimmedMessage || hasAttachments) {
55
- e.preventDefault();
56
- onSubmit(trimmedMessage, hasAttachments ? attachments : undefined);
57
- setMessage('');
58
- }
59
- };
40
+ const msg = trimmedMessage;
41
+ resetAfterSend();
42
+ onSubmit(msg, attachmentsCount > 0 ? attachments : undefined);
43
+ }, [
44
+ attachments,
45
+ attachmentsCount,
46
+ editor,
47
+ onSubmit,
48
+ resetAfterSend,
49
+ trimmedMessage,
50
+ ]);
60
51
 
61
- const handleFileInputChange = (e: ChangeEvent<HTMLInputElement>) => {
62
- const files = Array.from(e.target.files ?? []);
63
- e.target.value = '';
64
- if (files.length > 0) {
65
- onAttachmentFiles?.(files);
66
- }
67
- };
52
+ emitSubmitRef.current = emitSubmitAndClear;
68
53
 
69
- useEvent({
70
- event: 'keydown',
71
- callback: (e: KeyboardEvent) => {
72
- if (e.key === 'Enter' && !(e.metaKey || e.shiftKey || e.ctrlKey)) {
73
- e.preventDefault();
74
- handleSubmit(e);
75
- setMessage('');
76
- }
54
+ const handleSubmitForm = useCallback(
55
+ (e?: FormEvent) => {
56
+ e?.preventDefault();
57
+ emitSubmitAndClear();
77
58
  },
78
- });
59
+ [emitSubmitAndClear],
60
+ );
61
+
62
+ if (!editor) {
63
+ return null;
64
+ }
79
65
 
80
66
  return (
81
- <form onSubmit={handleSubmit} className={cn(S.root, className)}>
67
+ <form onSubmit={handleSubmitForm} className={cn(S.root, className)}>
82
68
  <ChatPromptAttachments
83
69
  attachments={attachments}
84
70
  onRemove={index => onRemoveAttachment?.(index)}
85
71
  disabled={disabled}
86
72
  />
87
- <div className={S.composer}>
88
- {showAttachButton ? (
89
- <>
90
- <input
91
- ref={fileInputRef}
92
- type="file"
93
- accept={attachmentAccept}
94
- multiple
95
- className={S.fileInput}
96
- disabled={disabled}
97
- onChange={handleFileInputChange}
98
- />
99
- <Button
100
- type="button"
101
- variant="ghost"
102
- icon
103
- size="sm"
104
- className={S.attachButton}
105
- aria-label="Attach file"
106
- disabled={disabled}
107
- onClick={e => {
108
- e.preventDefault();
109
- fileInputRef.current?.click();
110
- }}
111
- >
112
- <PaperclipIcon size={16} />
113
- </Button>
114
- </>
115
- ) : null}
116
-
117
- <Input
118
- ref={inputRef}
119
- type="textarea"
120
- rows={1}
121
- value={message}
122
- onChange={e => setMessage(e.target.value)}
123
- placeholder={placeholder || 'Type a message...'}
124
- className={cn(S.input)}
125
- />
126
-
127
- <div className={S.submitColumn}>
128
- <Button
129
- type="submit"
130
- size="sm"
131
- disabled={disabled || (!message.trim() && attachments.length === 0)}
132
- >
133
- <SendHorizontalIcon size={16} />
134
- </Button>
135
- </div>
136
- </div>
137
-
73
+ <ChatPromptComposer
74
+ editor={editor}
75
+ disabled={disabled}
76
+ trimmedMessage={trimmedMessage}
77
+ attachments={attachments}
78
+ attachmentAccept={attachmentAccept}
79
+ onAttachmentFiles={onAttachmentFiles}
80
+ onComposerKeyDown={handleComposerKeyDown}
81
+ />
138
82
  {footer}
139
83
  </form>
140
84
  );
@@ -0,0 +1,93 @@
1
+ import {
2
+ type ChangeEvent,
3
+ type KeyboardEvent as ReactKeyboardEvent,
4
+ useCallback,
5
+ useRef,
6
+ } from 'react';
7
+
8
+ import type { Editor } from '@tiptap/core';
9
+ import { EditorContent } from '@tiptap/react';
10
+ import { PaperclipIcon, SendHorizontalIcon } from 'lucide-react';
11
+
12
+ import { Button } from '../../Button';
13
+ import type { ChatAttachmentDropItem } from '../Chat.types';
14
+ import S from './ChatPrompt.styl';
15
+
16
+ export type ChatPromptComposerProps = {
17
+ editor: Editor;
18
+ disabled: boolean;
19
+ trimmedMessage: string;
20
+ attachments: ChatAttachmentDropItem[];
21
+ attachmentAccept?: string;
22
+ onAttachmentFiles?: (files: File[]) => void;
23
+ onComposerKeyDown: (event: ReactKeyboardEvent) => void;
24
+ };
25
+
26
+ export function ChatPromptComposer({
27
+ editor,
28
+ disabled,
29
+ trimmedMessage,
30
+ attachments,
31
+ attachmentAccept,
32
+ onAttachmentFiles,
33
+ onComposerKeyDown,
34
+ }: ChatPromptComposerProps) {
35
+ const fileInputRef = useRef<HTMLInputElement>(null);
36
+ const showAttachButton = Boolean(attachmentAccept && onAttachmentFiles);
37
+
38
+ const handleFileInputChange = useCallback(
39
+ (e: ChangeEvent<HTMLInputElement>) => {
40
+ const files = Array.from(e.target.files ?? []);
41
+ e.target.value = '';
42
+ if (files.length > 0) {
43
+ onAttachmentFiles?.(files);
44
+ }
45
+ },
46
+ [onAttachmentFiles],
47
+ );
48
+
49
+ const canSubmit = Boolean(trimmedMessage || attachments.length > 0);
50
+
51
+ return (
52
+ <div className={S.composer}>
53
+ {showAttachButton ? (
54
+ <>
55
+ <input
56
+ ref={fileInputRef}
57
+ type="file"
58
+ accept={attachmentAccept}
59
+ multiple
60
+ className={S.fileInput}
61
+ disabled={disabled}
62
+ onChange={handleFileInputChange}
63
+ />
64
+ <Button
65
+ type="button"
66
+ variant="ghost"
67
+ icon
68
+ size="sm"
69
+ className={S.attachButton}
70
+ aria-label="Attach file"
71
+ disabled={disabled}
72
+ onClick={e => {
73
+ e.preventDefault();
74
+ fileInputRef.current?.click();
75
+ }}
76
+ >
77
+ <PaperclipIcon size={16} />
78
+ </Button>
79
+ </>
80
+ ) : null}
81
+
82
+ <div className={S.editorWrap} onKeyDown={onComposerKeyDown}>
83
+ <EditorContent editor={editor} className={S.editorMount} />
84
+ </div>
85
+
86
+ <div className={S.submitColumn}>
87
+ <Button type="submit" size="sm" disabled={disabled || !canSubmit}>
88
+ <SendHorizontalIcon size={16} />
89
+ </Button>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,18 @@
1
+ import type { JSONContent } from '@tiptap/core';
2
+
3
+ export const CHAT_PROMPT_EMPTY_DOC: JSONContent = {
4
+ type: 'doc',
5
+ content: [{ type: 'paragraph' }],
6
+ };
7
+
8
+ export function chatPromptParagraphDoc(text: string): JSONContent {
9
+ return {
10
+ type: 'doc',
11
+ content: [
12
+ {
13
+ type: 'paragraph',
14
+ content: text ? [{ type: 'text', text }] : [],
15
+ },
16
+ ],
17
+ };
18
+ }
@@ -0,0 +1,214 @@
1
+ import {
2
+ type KeyboardEvent as ReactKeyboardEvent,
3
+ useCallback,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+
11
+ import {
12
+ DEFAULT_CHAT_SLASH_ITEMS,
13
+ createSlashMentionExtension,
14
+ } from '#uilib/tiptap/slash-mention';
15
+ import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
16
+ import type { AnyExtension, Editor } from '@tiptap/core';
17
+ import { Placeholder } from '@tiptap/extensions';
18
+ import { useEditor } from '@tiptap/react';
19
+ import StarterKit from '@tiptap/starter-kit';
20
+
21
+ import {
22
+ chatPromptSafeEditorDom,
23
+ syncChatPromptComposerHeight,
24
+ } from './ChatPrompt.helpers';
25
+ import { CHAT_PROMPT_EMPTY_DOC, chatPromptParagraphDoc } from './chatPromptDoc';
26
+
27
+ export type UseChatPromptEditorOptions = {
28
+ disabled: boolean;
29
+ placeholder?: string;
30
+ slashCommandItems?: SlashCommandItem[];
31
+ prefillMessage?: string | null;
32
+ /** Staged attachment count — Enter-to-send when text empty but files present. */
33
+ attachmentsCount?: number;
34
+ /** Called when user presses Enter to send (after guards). */
35
+ onEnterSubmit: () => void;
36
+ };
37
+
38
+ export type UseChatPromptEditorResult = {
39
+ editor: Editor | null;
40
+ trimmedMessage: string;
41
+ resetAfterSend: () => void;
42
+ handleComposerKeyDown: (event: ReactKeyboardEvent) => void;
43
+ };
44
+
45
+ export function useChatPromptEditor({
46
+ disabled,
47
+ placeholder,
48
+ slashCommandItems,
49
+ prefillMessage,
50
+ attachmentsCount = 0,
51
+ onEnterSubmit,
52
+ }: UseChatPromptEditorOptions): UseChatPromptEditorResult {
53
+ const slashOpenRef = useRef(false);
54
+ const suggestionActiveUpdater = useCallback((active: boolean) => {
55
+ slashOpenRef.current = active;
56
+ }, []);
57
+
58
+ const slashFinger = JSON.stringify(slashCommandItems ?? null);
59
+ const slashItemsStable = useMemo(() => {
60
+ return slashCommandItems ?? DEFAULT_CHAT_SLASH_ITEMS;
61
+ }, [slashCommandItems, slashFinger]);
62
+
63
+ const placeholderText = useMemo(() => {
64
+ if (placeholder === undefined || placeholder === null) {
65
+ return 'Type a message…';
66
+ }
67
+ const t = String(placeholder).trim();
68
+ return t === '' ? 'Type a message…' : t;
69
+ }, [placeholder]);
70
+
71
+ const ariaLabelComposer = useMemo(() => {
72
+ if (placeholder === undefined || placeholder === null) return 'Message';
73
+ const t = String(placeholder).trim();
74
+ return t === '' ? 'Message' : t;
75
+ }, [placeholder]);
76
+
77
+ const extensions = useMemo(() => {
78
+ const exts: AnyExtension[] = [
79
+ StarterKit.configure({
80
+ heading: false,
81
+ bulletList: false,
82
+ orderedList: false,
83
+ blockquote: false,
84
+ codeBlock: false,
85
+ horizontalRule: false,
86
+ }),
87
+ Placeholder.configure({
88
+ placeholder: placeholderText,
89
+ showOnlyWhenEditable: true,
90
+ showOnlyCurrent: false,
91
+ }),
92
+ ];
93
+ if (slashItemsStable.length > 0) {
94
+ exts.push(
95
+ createSlashMentionExtension({
96
+ items: slashItemsStable,
97
+ onSuggestionUiActiveChange: suggestionActiveUpdater,
98
+ }),
99
+ );
100
+ }
101
+ return exts;
102
+ }, [slashItemsStable, placeholderText, suggestionActiveUpdater]);
103
+
104
+ const [plainDraft, setPlainDraft] = useState('');
105
+
106
+ const editorDomRef = useRef<HTMLElement | null>(null);
107
+
108
+ const bindEditorDom = useCallback(({ editor }: { editor: Editor }) => {
109
+ setPlainDraft(editor.getText());
110
+ queueMicrotask(() => {
111
+ const dom = chatPromptSafeEditorDom(editor);
112
+ if (!dom) return;
113
+ editorDomRef.current = dom;
114
+ syncChatPromptComposerHeight(dom, editor.getText());
115
+ });
116
+ }, []);
117
+
118
+ const trimmedMessage = plainDraft.trim();
119
+
120
+ const editor = useEditor(
121
+ {
122
+ extensions,
123
+ content: CHAT_PROMPT_EMPTY_DOC,
124
+ editable: !disabled,
125
+ immediatelyRender: true,
126
+ editorProps: {
127
+ attributes: {
128
+ spellcheck: 'true',
129
+ 'aria-label': ariaLabelComposer,
130
+ },
131
+ },
132
+ onTransaction: ({ editor: ed }) => {
133
+ const dom = chatPromptSafeEditorDom(ed);
134
+ if (dom) {
135
+ syncChatPromptComposerHeight(dom, ed.getText());
136
+ }
137
+ setPlainDraft(ed.getText());
138
+ },
139
+ onCreate: bindEditorDom,
140
+ },
141
+ [extensions, disabled, bindEditorDom, ariaLabelComposer],
142
+ );
143
+
144
+ useEffect(() => {
145
+ if (!editor) return;
146
+ editor.setEditable(!disabled);
147
+ }, [editor, disabled]);
148
+
149
+ useEffect(() => {
150
+ if (!editor) return;
151
+ const bind = () => {
152
+ editorDomRef.current = chatPromptSafeEditorDom(editor);
153
+ };
154
+ bind();
155
+ let rafId: number | null = null;
156
+ if (typeof window !== 'undefined') {
157
+ rafId = window.requestAnimationFrame(bind);
158
+ }
159
+ return () => {
160
+ if (rafId != null) cancelAnimationFrame(rafId);
161
+ };
162
+ }, [editor]);
163
+
164
+ useEffect(() => {
165
+ if (!editor || prefillMessage == null || prefillMessage === '') return;
166
+ editor.commands.setContent(chatPromptParagraphDoc(prefillMessage));
167
+ const t = editor.getText();
168
+ setPlainDraft(t);
169
+ queueMicrotask(() => {
170
+ const dom = chatPromptSafeEditorDom(editor);
171
+ if (dom) syncChatPromptComposerHeight(dom, t);
172
+ });
173
+ }, [editor, prefillMessage]);
174
+
175
+ useLayoutEffect(() => {
176
+ if (!editor) return;
177
+ const dom = chatPromptSafeEditorDom(editor);
178
+ if (!dom) return;
179
+ syncChatPromptComposerHeight(dom, plainDraft);
180
+ }, [editor, plainDraft]);
181
+
182
+ const onEnterSubmitRef = useRef(onEnterSubmit);
183
+ onEnterSubmitRef.current = onEnterSubmit;
184
+
185
+ const resetAfterSend = useCallback(() => {
186
+ if (!editor) return;
187
+ editor.commands.clearContent();
188
+ queueMicrotask(() => {
189
+ const dom = chatPromptSafeEditorDom(editor);
190
+ if (dom) syncChatPromptComposerHeight(dom, '');
191
+ });
192
+ setPlainDraft('');
193
+ }, [editor]);
194
+
195
+ const handleComposerKeyDown = useCallback(
196
+ (e: ReactKeyboardEvent) => {
197
+ if (!(e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey))
198
+ return;
199
+ if (!editorDomRef.current?.contains(e.target as Node)) return;
200
+ if (slashOpenRef.current) return;
201
+ if (!trimmedMessage && attachmentsCount === 0) return;
202
+ e.preventDefault();
203
+ onEnterSubmitRef.current();
204
+ },
205
+ [attachmentsCount, trimmedMessage],
206
+ );
207
+
208
+ return {
209
+ editor,
210
+ trimmedMessage,
211
+ resetAfterSend,
212
+ handleComposerKeyDown,
213
+ };
214
+ }
@@ -40,4 +40,5 @@ export type {
40
40
  UserTextFileAttachment,
41
41
  } from './Chat.types';
42
42
  export { MessageRole } from './Chat.types';
43
+ export type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
43
44
  export { CsvIcon } from '../../icons/CsvIcon/CsvIcon';
@@ -3,6 +3,7 @@
3
3
  .tooltipContent
4
4
  z-index 50
5
5
  width fit-content
6
+ min-width 100%
6
7
  padding 6px 12px
7
8
  border-radius var(--p-3)
8
9
  background-color var(--popover)
@@ -129,7 +129,7 @@ function applyOverTriggerStyles(
129
129
  wrapper.style.setProperty('min-width', '0', 'important');
130
130
  wrapper.style.setProperty(
131
131
  'width',
132
- `${rect.width + paddingLeft + paddingRight + borderLeft + borderRight + 20}px`,
132
+ `${rect.width + paddingLeft + paddingRight + borderLeft + borderRight}px`,
133
133
  'important',
134
134
  );
135
135
 
@@ -0,0 +1,139 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+
4
+ import {
5
+ ChatChrome,
6
+ Message,
7
+ MessageRole,
8
+ type SlashCommandItem,
9
+ } from '#uilib/components/ui/Chat';
10
+ import { PageContentSection } from '#uilib/components/ui/Page';
11
+ import { ScrollRef } from '@homecode/ui';
12
+
13
+ import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
14
+ import { DocsHeaderActions } from '../docsHeaderActions';
15
+
16
+ const NO_QUICK_REPLY_KEYS: ReadonlySet<string> = new Set();
17
+
18
+ /** Sample items so the docs demo still shows a `/` palette (`DEFAULT_CHAT_SLASH_ITEMS` is empty). */
19
+ const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
20
+ {
21
+ id: 'sample-command',
22
+ label: 'sample-command',
23
+ description:
24
+ 'Demo only — define `slashCommandItems` in your app to list real commands.',
25
+ },
26
+ ];
27
+
28
+ const ASSISTANT_REPLY_TEXT =
29
+ 'Demo reply. Picked slash text is `/sample-command` via TipTap Mention (plain-text round-trip with renderText).';
30
+
31
+ function makeMessage(role: MessageRole, text: string): Message {
32
+ return {
33
+ id: crypto.randomUUID(),
34
+ role,
35
+ text,
36
+ timestamp: Date.now(),
37
+ };
38
+ }
39
+
40
+ export default function ChatSlashCommandsPage() {
41
+ const [messages, setMessages] = useState<Message[]>([]);
42
+ const [isLoading, setIsLoading] = useState(false);
43
+ const replyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
44
+ const scrollRef = useRef<ScrollRef>(null);
45
+
46
+ useEffect(() => {
47
+ return () => {
48
+ if (replyTimeoutRef.current != null) {
49
+ clearTimeout(replyTimeoutRef.current);
50
+ }
51
+ };
52
+ }, []);
53
+
54
+ const isEmpty = messages.length === 0 && !isLoading;
55
+ const isLastMessageFromUser =
56
+ messages.length > 0 &&
57
+ messages[messages.length - 1]?.role === MessageRole.USER;
58
+
59
+ const onSubmit = useCallback(
60
+ (raw: string) => {
61
+ const text = raw.trim();
62
+ if (!text || isLoading) return;
63
+
64
+ setMessages(prev => [...prev, makeMessage(MessageRole.USER, text)]);
65
+ setIsLoading(true);
66
+
67
+ if (replyTimeoutRef.current != null) {
68
+ clearTimeout(replyTimeoutRef.current);
69
+ }
70
+ replyTimeoutRef.current = setTimeout(() => {
71
+ replyTimeoutRef.current = null;
72
+ setMessages(prev => [
73
+ ...prev,
74
+ makeMessage(MessageRole.ASSISTANT, ASSISTANT_REPLY_TEXT),
75
+ ]);
76
+ setIsLoading(false);
77
+ }, 900);
78
+ },
79
+ [isLoading],
80
+ );
81
+
82
+ return (
83
+ <>
84
+ <AppPageHeader
85
+ breadcrumbs={[{ label: 'Chat' }, { label: 'Chat slash commands' }]}
86
+ title="Chat slash commands"
87
+ subheader={`Slash palette uses TipTap Mention with "/" as the trigger. Library default item list is empty — pass slashCommandItems from the app (this page uses a sample item for the demo).`}
88
+ actions={<DocsHeaderActions />}
89
+ />
90
+ <PageContentSection>
91
+ <p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
92
+ Same{' '}
93
+ <Link className="underline underline-offset-2" to="/docs/chat">
94
+ Chat
95
+ </Link>{' '}
96
+ shell below: scrolling history, empty state, disclaimer, composer.
97
+ Type <kbd className="font-mono">/</kbd> at line start or after a
98
+ space; pick with arrows + Enter or click, then send.
99
+ </p>
100
+ <ChatChrome
101
+ showResizeHandle={false}
102
+ resizeHandle={undefined}
103
+ onClose={undefined}
104
+ isEmpty={isEmpty}
105
+ renderPresets={() => null}
106
+ messages={messages}
107
+ onQuickReply={() => {}}
108
+ suppressedQuickReplyKeys={NO_QUICK_REPLY_KEYS}
109
+ isLoading={isLoading}
110
+ scriptContinueLabel={undefined}
111
+ onScriptContinue={undefined}
112
+ showBranchActionsRow={false}
113
+ showSyntheticBranchButtons={false}
114
+ unusedBranchKeys={[]}
115
+ isScriptComplete={false}
116
+ onGenerateDashboard={undefined}
117
+ generatingDashboard={false}
118
+ onGenerateDashboardClick={() => {}}
119
+ showInlinePresets={false}
120
+ isLastMessageFromUser={isLastMessageFromUser}
121
+ scrollRef={scrollRef}
122
+ effectiveScopeId="docs-chat-slash-inline"
123
+ onPromptSubmit={onSubmit}
124
+ onChatDeleted={() => {}}
125
+ slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS}
126
+ promptPlaceholder='Ask something or type "/" for demo commands…'
127
+ emptyState={{
128
+ title: 'Try a slash command',
129
+ description:
130
+ 'This demo mounts one sample slash item. Production: pass slashCommandItems to ChatChrome so `/` opens your palette.',
131
+ additionalContent: (
132
+ <p>Optional empty-state slot via additionalContent.</p>
133
+ ),
134
+ }}
135
+ />
136
+ </PageContentSection>
137
+ </>
138
+ );
139
+ }
@@ -41,7 +41,7 @@ export default function TooltipPage() {
41
41
  <p style={{ margin: '0 0 8px', fontWeight: 600 }}>Over trigger</p>
42
42
  <div
43
43
  style={{
44
- maxWidth: 280,
44
+ maxWidth: 500,
45
45
  border: '1px dashed var(--border)',
46
46
  padding: 8,
47
47
  }}