@sybilion/uilib 1.3.36 → 1.3.38

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 (39) hide show
  1. package/dist/esm/components/ui/Chat/Chat.types.js +6 -1
  2. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +7 -10
  3. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +2 -1
  4. package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +6 -2
  5. package/dist/esm/components/ui/Chat/ChatSheet/ChatSheet.js +2 -1
  6. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +33 -15
  7. package/dist/esm/index.js +1 -1
  8. package/dist/esm/tiptap/slash-mention/createSlashMentionExtension.js +48 -14
  9. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +6 -1
  10. package/dist/esm/types/src/components/ui/Chat/Chat.types.test.d.ts +1 -0
  11. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
  12. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +3 -6
  13. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +1 -1
  14. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.d.ts +3 -2
  15. package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSheet.d.ts +1 -1
  16. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +5 -2
  17. package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -1
  18. package/dist/esm/types/src/docs/docsHeaderActions.d.ts +2 -1
  19. package/dist/esm/types/src/tiptap/slash-mention/createSlashMentionExtension.d.ts +3 -3
  20. package/dist/esm/types/src/tiptap/slash-mention/index.d.ts +1 -1
  21. package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +9 -1
  22. package/package.json +1 -1
  23. package/src/components/ui/Chat/Chat.types.test.ts +32 -0
  24. package/src/components/ui/Chat/Chat.types.ts +13 -1
  25. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +24 -46
  26. package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +6 -8
  27. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +2 -0
  28. package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +11 -2
  29. package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +2 -0
  30. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +147 -109
  31. package/src/components/ui/Chat/index.ts +5 -1
  32. package/src/docs/docsHeaderActions.tsx +3 -2
  33. package/src/docs/pages/ChatAttachmentsDropzonePage.tsx +0 -5
  34. package/src/docs/pages/ChatPage.tsx +0 -5
  35. package/src/docs/pages/ChatSlashCommandsPage.tsx +43 -14
  36. package/src/docs/pages/ChatUserCsvAttachmentPage.tsx +0 -5
  37. package/src/tiptap/slash-mention/createSlashMentionExtension.ts +65 -11
  38. package/src/tiptap/slash-mention/index.ts +1 -0
  39. package/src/tiptap/slash-mention/types.ts +10 -1
@@ -8,6 +8,7 @@ import {
8
8
  type SlashCommandItem,
9
9
  } from '#uilib/components/ui/Chat';
10
10
  import { PageContentSection } from '#uilib/components/ui/Page';
11
+ import type { SlashItemCommandContext } from '#uilib/tiptap/slash-mention/types';
11
12
  import { ScrollRef } from '@homecode/ui';
12
13
 
13
14
  import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
@@ -15,18 +16,23 @@ import { DocsHeaderActions } from '../docsHeaderActions';
15
16
 
16
17
  const NO_QUICK_REPLY_KEYS: ReadonlySet<string> = new Set();
17
18
 
19
+ const DOCS_SAMPLE_COMMAND_ID = 'sample-command';
20
+
18
21
  /** Sample items so the docs demo still shows a `/` palette (`DEFAULT_CHAT_SLASH_ITEMS` is empty). */
19
22
  const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
20
23
  {
21
- id: 'sample-command',
22
- label: 'sample-command',
24
+ id: DOCS_SAMPLE_COMMAND_ID,
25
+ label: DOCS_SAMPLE_COMMAND_ID,
23
26
  description:
24
- 'Demo onlydefine `slashCommandItems` in your app to list real commands.',
27
+ 'Demo handlerclears composer and runs `onSlashItemCommand` (no mention insert).',
25
28
  },
26
29
  ];
27
30
 
31
+ const SAMPLE_COMMAND_REPLY_TEXT =
32
+ 'Sample command ran via `onSlashItemCommand` — composer cleared, no mention inserted.';
33
+
28
34
  const ASSISTANT_REPLY_TEXT =
29
- 'Demo reply. Picked slash text is `/sample-command` via TipTap Mention (plain-text round-trip with renderText).';
35
+ 'Demo reply for a normal message. Slash picks with a custom handler skip mention insert.';
30
36
 
31
37
  function makeMessage(role: MessageRole, text: string): Message {
32
38
  return {
@@ -56,11 +62,34 @@ export default function ChatSlashCommandsPage() {
56
62
  messages.length > 0 &&
57
63
  messages[messages.length - 1]?.role === MessageRole.USER;
58
64
 
65
+ const runSampleCommand = useCallback(() => {
66
+ setMessages(prev => [
67
+ ...prev,
68
+ makeMessage(MessageRole.ASSISTANT, SAMPLE_COMMAND_REPLY_TEXT),
69
+ ]);
70
+ }, []);
71
+
72
+ const onSlashItemCommand = useCallback(
73
+ ({ item }: SlashItemCommandContext) => {
74
+ if (item.id !== DOCS_SAMPLE_COMMAND_ID) {
75
+ return false;
76
+ }
77
+ queueMicrotask(() => runSampleCommand());
78
+ return true;
79
+ },
80
+ [runSampleCommand],
81
+ );
82
+
59
83
  const onSubmit = useCallback(
60
84
  (raw: string) => {
61
85
  const text = raw.trim();
62
86
  if (!text || isLoading) return;
63
87
 
88
+ if (text === `/${DOCS_SAMPLE_COMMAND_ID}`) {
89
+ runSampleCommand();
90
+ return;
91
+ }
92
+
64
93
  setMessages(prev => [...prev, makeMessage(MessageRole.USER, text)]);
65
94
  setIsLoading(true);
66
95
 
@@ -76,7 +105,7 @@ export default function ChatSlashCommandsPage() {
76
105
  setIsLoading(false);
77
106
  }, 900);
78
107
  },
79
- [isLoading],
108
+ [isLoading, runSampleCommand],
80
109
  );
81
110
 
82
111
  return (
@@ -84,8 +113,10 @@ export default function ChatSlashCommandsPage() {
84
113
  <AppPageHeader
85
114
  breadcrumbs={[{ label: 'Chat' }, { label: 'Chat slash commands' }]}
86
115
  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 />}
116
+ subheader={`Slash palette uses TipTap Mention with "/" as the trigger. Pass slashCommandItems from the app; optional onSlashItemCommand can clear the composer and run an action instead of inserting a mention.`}
117
+ actions={
118
+ <DocsHeaderActions slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS} />
119
+ }
89
120
  />
90
121
  <PageContentSection>
91
122
  <p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
@@ -95,7 +126,9 @@ export default function ChatSlashCommandsPage() {
95
126
  </Link>{' '}
96
127
  shell below: scrolling history, empty state, disclaimer, composer.
97
128
  Type <kbd className="font-mono">/</kbd> at line start or after a
98
- space; pick with arrows + Enter or click, then send.
129
+ space; pick <kbd className="font-mono">sample-command</kbd> to run the
130
+ custom handler, or send{' '}
131
+ <kbd className="font-mono">/sample-command</kbd> with Enter.
99
132
  </p>
100
133
  <ChatChrome
101
134
  showResizeHandle={false}
@@ -109,13 +142,8 @@ export default function ChatSlashCommandsPage() {
109
142
  isLoading={isLoading}
110
143
  scriptContinueLabel={undefined}
111
144
  onScriptContinue={undefined}
112
- showBranchActionsRow={false}
113
145
  showSyntheticBranchButtons={false}
114
146
  unusedBranchKeys={[]}
115
- isScriptComplete={false}
116
- onGenerateDashboard={undefined}
117
- generatingDashboard={false}
118
- onGenerateDashboardClick={() => {}}
119
147
  showInlinePresets={false}
120
148
  isLastMessageFromUser={isLastMessageFromUser}
121
149
  scrollRef={scrollRef}
@@ -123,11 +151,12 @@ export default function ChatSlashCommandsPage() {
123
151
  onPromptSubmit={onSubmit}
124
152
  onChatDeleted={() => {}}
125
153
  slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS}
154
+ onSlashItemCommand={onSlashItemCommand}
126
155
  promptPlaceholder='Ask something or type "/" for demo commands…'
127
156
  emptyState={{
128
157
  title: 'Try a slash command',
129
158
  description:
130
- 'This demo mounts one sample slash item. Production: pass slashCommandItems to ChatChrome so `/` opens your palette.',
159
+ 'Pick sample-command from the palette or send /sample-command onSlashItemCommand clears the composer and runs the demo action.',
131
160
  additionalContent: (
132
161
  <p>Optional empty-state slot via additionalContent.</p>
133
162
  ),
@@ -148,13 +148,8 @@ export default function ChatUserCsvAttachmentPage() {
148
148
  isLoading={isLoading}
149
149
  scriptContinueLabel={undefined}
150
150
  onScriptContinue={undefined}
151
- showBranchActionsRow={false}
152
151
  showSyntheticBranchButtons={false}
153
152
  unusedBranchKeys={[]}
154
- isScriptComplete={false}
155
- onGenerateDashboard={undefined}
156
- generatingDashboard={false}
157
- onGenerateDashboardClick={() => {}}
158
153
  showInlinePresets={false}
159
154
  isLastMessageFromUser={isLastMessageFromUser}
160
155
  scrollRef={scrollRef}
@@ -14,10 +14,42 @@ import { filterSlashItems } from './defaultChatSlashItems';
14
14
  import type {
15
15
  CreateSlashMentionExtensionOptions,
16
16
  SlashCommandItem,
17
+ SlashSuggestionPlacement,
17
18
  } from './types';
18
19
 
20
+ const SUGGESTION_GAP_PX = 4;
21
+
22
+ function placeSlashSuggestionPopup(
23
+ popupElement: HTMLElement,
24
+ clientRect: DOMRect | null | undefined,
25
+ placement: SlashSuggestionPlacement,
26
+ ): void {
27
+ if (!clientRect) return;
28
+
29
+ const el = popupElement;
30
+ const popupHeight = el.getBoundingClientRect().height;
31
+ const spaceBelow = window.innerHeight - clientRect.bottom - SUGGESTION_GAP_PX;
32
+ const spaceAbove = clientRect.top - SUGGESTION_GAP_PX;
33
+
34
+ let showAbove = placement === 'above';
35
+ if (placement === 'auto') {
36
+ showAbove = spaceBelow < popupHeight && spaceAbove >= spaceBelow;
37
+ }
38
+
39
+ const top = showAbove
40
+ ? Math.max(
41
+ SUGGESTION_GAP_PX,
42
+ clientRect.top - popupHeight - SUGGESTION_GAP_PX,
43
+ )
44
+ : clientRect.bottom + SUGGESTION_GAP_PX;
45
+
46
+ el.style.left = `${clientRect.left}px`;
47
+ el.style.top = `${top}px`;
48
+ }
49
+
19
50
  export function slashMentionSuggestionRender(
20
51
  uiRef: MutableRefObject<SlashSuggestionListHandle | null>,
52
+ placement: SlashSuggestionPlacement = 'below',
21
53
  ): {
22
54
  onStart?: (
23
55
  props: SuggestionProps<SlashCommandItem, SlashCommandItem>,
@@ -45,11 +77,11 @@ export function slashMentionSuggestionRender(
45
77
  >,
46
78
  ) => {
47
79
  if (!popup?.element) return;
48
- const rect = props.clientRect?.();
49
- if (!rect) return;
50
- const el = popup.element;
51
- el.style.left = `${rect.left}px`;
52
- el.style.top = `${rect.bottom + 4}px`;
80
+ placeSlashSuggestionPopup(
81
+ popup.element,
82
+ props.clientRect?.() ?? null,
83
+ placement,
84
+ );
53
85
  };
54
86
 
55
87
  return {
@@ -69,6 +101,7 @@ export function slashMentionSuggestionRender(
69
101
  popup.element.style.zIndex = '10002';
70
102
  document.body.append(popup.element);
71
103
  place(props);
104
+ requestAnimationFrame(() => place(props));
72
105
  },
73
106
  onUpdate: props => {
74
107
  if (!popup) return;
@@ -78,6 +111,7 @@ export function slashMentionSuggestionRender(
78
111
  listHandleRef: uiRef,
79
112
  });
80
113
  place(props);
114
+ requestAnimationFrame(() => place(props));
81
115
  },
82
116
  onExit: () => {
83
117
  popup?.destroy();
@@ -90,6 +124,28 @@ export function slashMentionSuggestionRender(
90
124
  };
91
125
  }
92
126
 
127
+ function clearSlashTriggerEditor(
128
+ editor: Editor,
129
+ range: { from: number; to: number },
130
+ ): void {
131
+ if (editor.isDestroyed) return;
132
+ try {
133
+ editor.chain().focus().deleteRange(range).clearContent().run();
134
+ } catch {
135
+ // Editor view may be tearing down during suggestion exit.
136
+ }
137
+ }
138
+
139
+ function collapseEditorSelectionEnd(editor: Editor): void {
140
+ if (editor.isDestroyed) return;
141
+ try {
142
+ editor.view?.dom?.ownerDocument?.defaultView
143
+ ?.getSelection?.()
144
+ ?.collapseToEnd();
145
+ } catch {
146
+ // view.dom throws when editor is not mounted
147
+ }
148
+ }
93
149
  function insertDefaultMention(
94
150
  editor: Editor,
95
151
  range: { from: number; to: number },
@@ -149,6 +205,7 @@ export function createSlashMentionExtension({
149
205
  slashChar = '/',
150
206
  pluginKey,
151
207
  onItemCommand,
208
+ suggestionPlacement = 'below',
152
209
  onSuggestionUiActiveChange,
153
210
  }: CreateSlashMentionExtensionConfiguredOptions) {
154
211
  const uiRef: MutableRefObject<SlashSuggestionListHandle | null> = {
@@ -188,18 +245,15 @@ export function createSlashMentionExtension({
188
245
  command: ({ editor, range, props }) => {
189
246
  const item = props as SlashCommandItem;
190
247
  if (onItemCommand?.({ editor, range, item }) === true) {
191
- queueMicrotask(() => {
192
- editor.view.dom.ownerDocument?.defaultView
193
- ?.getSelection?.()
194
- ?.collapseToEnd();
195
- });
248
+ clearSlashTriggerEditor(editor, range);
249
+ queueMicrotask(() => collapseEditorSelectionEnd(editor));
196
250
  return null;
197
251
  }
198
252
  insertDefaultMention(editor, range, item, slashChar);
199
253
  return null;
200
254
  },
201
255
  render: () => {
202
- const menu = slashMentionSuggestionRender(uiRef);
256
+ const menu = slashMentionSuggestionRender(uiRef, suggestionPlacement);
203
257
  return {
204
258
  ...menu,
205
259
  onStart: props => {
@@ -2,6 +2,7 @@ export type {
2
2
  SlashCommandItem,
3
3
  SlashOnItemCommand,
4
4
  SlashItemCommandContext,
5
+ SlashSuggestionPlacement,
5
6
  CreateSlashMentionExtensionOptions,
6
7
  } from './types';
7
8
  export {
@@ -13,10 +13,14 @@ export type SlashItemCommandContext = {
13
13
  };
14
14
 
15
15
  /**
16
- * If provided, run before default mention insertion. Return true to skip inserting a mention node.
16
+ * If provided, run when a slash item is picked. Return true to skip mention insert
17
+ * (extension clears the trigger text from the composer).
17
18
  */
18
19
  export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
19
20
 
21
+ /** Where the slash palette opens relative to the caret. */
22
+ export type SlashSuggestionPlacement = 'below' | 'above' | 'auto';
23
+
20
24
  export type CreateSlashMentionExtensionOptions = {
21
25
  /** Items shown in the slash menu (filtered by query after `/`). */
22
26
  items: SlashCommandItem[];
@@ -26,4 +30,9 @@ export type CreateSlashMentionExtensionOptions = {
26
30
  pluginKey?: import('@tiptap/pm/state').PluginKey;
27
31
  /** Custom handler (e.g. insert a block node instead of a mention). */
28
32
  onItemCommand?: SlashOnItemCommand;
33
+ /**
34
+ * Palette position vs caret. Default `below`.
35
+ * Use `above` for bottom-anchored composers (chat prompt); `auto` flips by viewport space.
36
+ */
37
+ suggestionPlacement?: SlashSuggestionPlacement;
29
38
  };