@sybilion/uilib 1.3.43 → 1.3.46

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/assets/logo.svg +1 -1
  2. package/dist/esm/components/ui/Chart/Chart.styl.js +1 -1
  3. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +7 -1
  4. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +7 -5
  5. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.styl.js +1 -1
  6. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptComposer.js +8 -4
  7. package/dist/esm/components/ui/Chat/ChatPrompt/chatPromptComposerInsert.js +67 -0
  8. package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +24 -17
  9. package/dist/esm/components/ui/Logo/Logo.styl.js +1 -1
  10. package/dist/esm/components/ui/Logo/logo.svg.js +1 -1
  11. package/dist/esm/components/ui/Page/PageFooter/PageFooter.js +3 -2
  12. package/dist/esm/components/ui/Page/PageHeader/PageHeader.js +1 -1
  13. package/dist/esm/components/ui/TextWithDeferTooltip/TextWithDeferTooltip.js +23 -1
  14. package/dist/esm/components/widgets/DriverCard/DriverCard.js +1 -1
  15. package/dist/esm/components/widgets/DriversComparisonChart/DriversComparisonChart.js +1 -0
  16. package/dist/esm/components/widgets/PerformanceChart/PerformanceTable.js +1 -0
  17. package/dist/esm/index.js +1 -0
  18. package/dist/esm/tiptap/slash-mention/SlashSuggestionList.styl.js +2 -2
  19. package/dist/esm/tiptap/slash-mention/createSlashMentionExtension.js +45 -28
  20. package/dist/esm/types/src/components/ui/Chat/Chat.d.ts +1 -2
  21. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +1 -1
  22. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +3 -1
  23. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.d.ts +3 -3
  24. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.types.d.ts +2 -0
  25. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/chatPromptComposerInsert.d.ts +35 -0
  26. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.d.ts +0 -2
  27. package/dist/esm/types/src/components/ui/Chat/index.d.ts +3 -0
  28. package/dist/esm/types/src/components/ui/Page/PageFooter/PageFooter.d.ts +1 -2
  29. package/dist/esm/types/src/docs/pages/ChatSheetPage.d.ts +1 -0
  30. package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +7 -2
  31. package/package.json +1 -1
  32. package/src/components/ui/Chart/Chart.styl +2 -1
  33. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +8 -1
  34. package/src/components/ui/Chat/Chat.types.ts +1 -1
  35. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl +26 -1
  36. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +86 -65
  37. package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.tsx +31 -13
  38. package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.types.ts +11 -0
  39. package/src/components/ui/Chat/ChatPrompt/chatPromptComposerInsert.ts +122 -0
  40. package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +34 -21
  41. package/src/components/ui/Chat/index.ts +9 -0
  42. package/src/components/ui/Logo/Logo.styl +1 -0
  43. package/src/components/ui/Logo/logo.svg +1 -1
  44. package/src/components/ui/Page/PageFooter/PageFooter.tsx +2 -3
  45. package/src/components/ui/Page/PageHeader/PageHeader.tsx +1 -1
  46. package/src/components/ui/TextWithDeferTooltip/TextWithDeferTooltip.tsx +35 -4
  47. package/src/components/widgets/DriverCard/DriverCard.tsx +5 -1
  48. package/src/docs/DocsShell.tsx +1 -6
  49. package/src/docs/pages/ChatSheetPage.tsx +61 -0
  50. package/src/docs/pages/ChatSlashCommandsPage.tsx +46 -120
  51. package/src/docs/pages/TooltipPage.tsx +0 -31
  52. package/src/docs/registry.ts +6 -0
  53. package/src/tiptap/slash-mention/SlashSuggestionList.styl +4 -0
  54. package/src/tiptap/slash-mention/SlashSuggestionList.styl.d.ts +1 -0
  55. package/src/tiptap/slash-mention/createSlashMentionExtension.ts +46 -27
  56. package/src/tiptap/slash-mention/types.ts +7 -2
@@ -1,5 +1,4 @@
1
1
  import {
2
- type KeyboardEvent as ReactKeyboardEvent,
3
2
  useCallback,
4
3
  useEffect,
5
4
  useLayoutEffect,
@@ -43,7 +42,6 @@ export type UseChatPromptEditorResult = {
43
42
  editor: Editor | null;
44
43
  trimmedMessage: string;
45
44
  resetAfterSend: () => void;
46
- handleComposerKeyDown: (event: ReactKeyboardEvent) => void;
47
45
  };
48
46
 
49
47
  export function useChatPromptEditor({
@@ -94,7 +92,6 @@ export function useChatPromptEditor({
94
92
  Placeholder.configure({
95
93
  placeholder: placeholderText,
96
94
  showOnlyWhenEditable: true,
97
- showOnlyCurrent: false,
98
95
  }),
99
96
  ];
100
97
  if (slashItemsStable.length > 0) {
@@ -126,6 +123,38 @@ export function useChatPromptEditor({
126
123
 
127
124
  const trimmedMessage = plainDraft.trim();
128
125
 
126
+ const trimmedMessageRef = useRef(trimmedMessage);
127
+ trimmedMessageRef.current = trimmedMessage;
128
+
129
+ const attachmentsCountRef = useRef(attachmentsCount);
130
+ attachmentsCountRef.current = attachmentsCount;
131
+
132
+ const onEnterSubmitRef = useRef(onEnterSubmit);
133
+ onEnterSubmitRef.current = onEnterSubmit;
134
+
135
+ const handleEditorKeyDown = useCallback(
136
+ (_view: unknown, event: KeyboardEvent) => {
137
+ if (
138
+ !(
139
+ event.key === 'Enter' &&
140
+ !event.shiftKey &&
141
+ !event.metaKey &&
142
+ !event.ctrlKey
143
+ )
144
+ ) {
145
+ return false;
146
+ }
147
+ if (slashOpenRef.current) return false;
148
+ if (!trimmedMessageRef.current && attachmentsCountRef.current === 0) {
149
+ return false;
150
+ }
151
+ event.preventDefault();
152
+ onEnterSubmitRef.current();
153
+ return true;
154
+ },
155
+ [],
156
+ );
157
+
129
158
  const editor = useEditor(
130
159
  {
131
160
  extensions,
@@ -137,6 +166,7 @@ export function useChatPromptEditor({
137
166
  spellcheck: 'true',
138
167
  'aria-label': ariaLabelComposer,
139
168
  },
169
+ handleKeyDown: handleEditorKeyDown,
140
170
  },
141
171
  onTransaction: ({ editor: ed }) => {
142
172
  const dom = chatPromptSafeEditorDom(ed);
@@ -147,7 +177,7 @@ export function useChatPromptEditor({
147
177
  },
148
178
  onCreate: bindEditorDom,
149
179
  },
150
- [extensions, bindEditorDom, ariaLabelComposer],
180
+ [extensions, bindEditorDom, ariaLabelComposer, handleEditorKeyDown],
151
181
  );
152
182
 
153
183
  useEffect(() => {
@@ -188,9 +218,6 @@ export function useChatPromptEditor({
188
218
  syncChatPromptComposerHeight(dom, plainDraft);
189
219
  }, [editor, plainDraft]);
190
220
 
191
- const onEnterSubmitRef = useRef(onEnterSubmit);
192
- onEnterSubmitRef.current = onEnterSubmit;
193
-
194
221
  const resetAfterSend = useCallback(() => {
195
222
  if (!editor) return;
196
223
  editor.chain().clearContent().focus().run();
@@ -201,23 +228,9 @@ export function useChatPromptEditor({
201
228
  setPlainDraft('');
202
229
  }, [editor]);
203
230
 
204
- const handleComposerKeyDown = useCallback(
205
- (e: ReactKeyboardEvent) => {
206
- if (!(e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey))
207
- return;
208
- if (!editorDomRef.current?.contains(e.target as Node)) return;
209
- if (slashOpenRef.current) return;
210
- if (!trimmedMessage && attachmentsCount === 0) return;
211
- e.preventDefault();
212
- onEnterSubmitRef.current();
213
- },
214
- [attachmentsCount, trimmedMessage],
215
- );
216
-
217
231
  return {
218
232
  editor,
219
233
  trimmedMessage,
220
234
  resetAfterSend,
221
- handleComposerKeyDown,
222
235
  };
223
236
  }
@@ -27,6 +27,15 @@ export type {
27
27
  } from './ChatSheet/useChatPanelChromeModel';
28
28
  export { ChatMessage } from './ChatMessage';
29
29
  export { ChatPrompt } from './ChatPrompt';
30
+ export type { ChatPromptComposerHandle } from './ChatPrompt/ChatPromptComposer';
31
+ export {
32
+ CHAT_PROMPT_COMMAND_CHIP_CLASS,
33
+ chatPromptChipHtml,
34
+ createChatPromptComposerHandle,
35
+ getChatPromptTokenRangeBeforePos,
36
+ insertChatPromptContentAtCaret,
37
+ } from './ChatPrompt/chatPromptComposerInsert';
38
+ export type { ChatPromptComposerInsertOptions } from './ChatPrompt/ChatPromptComposer.types';
30
39
  export { ChatPresets } from './ChatPresets';
31
40
  export type {
32
41
  ChatEmptyStateConfig,
@@ -9,6 +9,7 @@
9
9
  flex-shrink 0
10
10
  height 24px
11
11
  width auto
12
+ color var(--brand-color)
12
13
 
13
14
  .text
14
15
  font-family var(--font-family-heading)
@@ -1,3 +1,3 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
2
- <path fill="#C259FF" d="M13.43 24.023h-2.836v-2.836h2.836zm-2.835-2.844H5.63v-2.74h4.965Zm10.527-2.74h-2.726v2.74h-4.965v-2.74h4.952v-4.965h2.74zm-15.5 0h-2.74v-4.965h2.74ZM2.87 13.462H.034v-2.836H2.87Zm21.096 0H21.13v-2.836h2.836zM5.617 10.635h-2.74V5.669h2.74zm15.505 0h-2.74V5.669h2.74zM10.588 2.927v2.74H5.623v-2.74Zm2.842 0h4.96v2.74h-4.966v-2.74h-2.83V.09h2.836z" style="stroke-width:.13252" />
2
+ <path fill="currentColor" d="M13.43 24.023h-2.836v-2.836h2.836zm-2.835-2.844H5.63v-2.74h4.965Zm10.527-2.74h-2.726v2.74h-4.965v-2.74h4.952v-4.965h2.74zm-15.5 0h-2.74v-4.965h2.74ZM2.87 13.462H.034v-2.836H2.87Zm21.096 0H21.13v-2.836h2.836zM5.617 10.635h-2.74V5.669h2.74zm15.505 0h-2.74V5.669h2.74zM10.588 2.927v2.74H5.623v-2.74Zm2.842 0h4.96v2.74h-4.966v-2.74h-2.83V.09h2.836z" style="stroke-width:.13252" />
3
3
  </svg>
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { Link } from 'react-router-dom';
3
3
 
4
+ import { Logo } from '#uilib/components/ui/Logo';
4
5
  import { GlobeIcon, MailIcon } from 'lucide-react';
5
6
 
6
7
  import S from './PageFooter.styl';
@@ -14,7 +15,6 @@ export interface PageFooterLinkItem {
14
15
  }
15
16
 
16
17
  export interface PageFooterProps {
17
- logo?: ReactNode;
18
18
  /** Rendered between logo block and copyright (e.g. debug UI from the app). */
19
19
  children?: ReactNode;
20
20
  /** When non-empty, version badge links here (e.g. `/releases`). */
@@ -55,7 +55,6 @@ export function PageFooter({
55
55
  versionLabel,
56
56
  homeTo = '/',
57
57
  brandText = 'Sybilion',
58
- logo,
59
58
  websiteHref = 'https://sybilion.com',
60
59
  mailHref = 'mailto:support@sybilion.com',
61
60
  copyrightText = '© 2026 Sybilion. All rights reserved.',
@@ -84,7 +83,7 @@ export function PageFooter({
84
83
  <div className={S.line}>
85
84
  <div className={S.logo}>
86
85
  <Link to={homeTo}>
87
- {logo}
86
+ <Logo />
88
87
  &nbsp;{brandText}
89
88
  </Link>
90
89
  {versionLink !== '' && (
@@ -54,7 +54,7 @@ export function PageHeader({
54
54
  <div className={S.title}>
55
55
  <h1>{title}</h1>
56
56
  {subheader && (
57
- <TextWithDeferTooltip className={S.subheader}>
57
+ <TextWithDeferTooltip className={S.subheader} overTrigger>
58
58
  {subheader}
59
59
  </TextWithDeferTooltip>
60
60
  )}
@@ -1,4 +1,4 @@
1
- import { useRef, useState } from 'react';
1
+ import { CSSProperties, useRef, useState } from 'react';
2
2
 
3
3
  import {
4
4
  Tooltip,
@@ -18,7 +18,9 @@ function TextWithDeferTooltip({
18
18
  ...props
19
19
  }: TextWithDeferTooltipProps) {
20
20
  const [withTooltip, setWithTooltip] = useState(false);
21
+ const [tooltipStyles, setTooltipStyles] = useState<CSSProperties>({});
21
22
  const ref = useRef<HTMLDivElement>(null);
23
+ const cachedFontSizeRef = useRef<string | null>(null);
22
24
 
23
25
  const handleMouseEnter = () => {
24
26
  if (!ref.current) return;
@@ -29,6 +31,31 @@ function TextWithDeferTooltip({
29
31
  ref.current.scrollHeight - ref.current.clientHeight > 3;
30
32
 
31
33
  if (isOverflowingHorizontally || isOverflowingVertically) {
34
+ const styles: CSSProperties = {
35
+ fontSize: cachedFontSizeRef.current || undefined,
36
+ };
37
+
38
+ if (ref.current) {
39
+ const {
40
+ width: rectWidth,
41
+ left,
42
+ top,
43
+ } = ref.current.getBoundingClientRect();
44
+
45
+ styles.width = `${width ?? rectWidth}px`;
46
+
47
+ if (!cachedFontSizeRef.current) {
48
+ const { fontSize } = window.getComputedStyle(ref.current);
49
+ cachedFontSizeRef.current = fontSize;
50
+ styles.fontSize = fontSize;
51
+ }
52
+
53
+ if (overTrigger) {
54
+ styles.transform = `translate(${left}px, ${top}px) !important`;
55
+ }
56
+ }
57
+
58
+ setTooltipStyles(styles);
32
59
  setWithTooltip(true);
33
60
  }
34
61
  };
@@ -45,14 +72,18 @@ function TextWithDeferTooltip({
45
72
  );
46
73
 
47
74
  if (withTooltip) {
75
+ const tooltipSide = overTrigger ? 'bottom' : side;
76
+
48
77
  return (
49
78
  <Tooltip open={withTooltip} onOpenChange={setWithTooltip}>
50
79
  <TooltipTrigger asChild>{textElement}</TooltipTrigger>
51
80
  <TooltipContent
52
- side={side}
81
+ side={tooltipSide}
82
+ style={{
83
+ ...(maxWidth !== undefined && { maxWidth: `${maxWidth}px` }),
84
+ ...tooltipStyles,
85
+ }}
53
86
  overTrigger={overTrigger}
54
- maxWidth={maxWidth}
55
- style={width !== undefined ? { width: `${width}px` } : undefined}
56
87
  >
57
88
  {children}
58
89
  </TooltipContent>
@@ -177,7 +177,11 @@ export function DriverCard({
177
177
  id={id}
178
178
  label={<TooltipTrigger asChild>{nameElem}</TooltipTrigger>}
179
179
  />
180
- <TooltipContent side="left" className={S.tooltipContent}>
180
+ <TooltipContent
181
+ side="left"
182
+ className={S.tooltipContent}
183
+ overTrigger
184
+ >
181
185
  <div className={S.tooltipTitle}>{name}</div>
182
186
  </TooltipContent>
183
187
  </Tooltip>
@@ -1,7 +1,6 @@
1
1
  import { Outlet, useLocation, useNavigate } from 'react-router-dom';
2
2
 
3
3
  import { AppHeaderHost } from '#uilib/components/ui/AppHeader';
4
- import { LogoMark } from '#uilib/components/ui/Logo';
5
4
  import { AppShell, AppShellMainContent } from '#uilib/components/ui/Page';
6
5
  import { PageFooter } from '#uilib/components/ui/Page/PageFooter/PageFooter';
7
6
  import { PageScroll } from '#uilib/components/ui/Page/PageScroll/PageScroll';
@@ -23,11 +22,7 @@ export function DocsShell() {
23
22
  <AppShellMainContent
24
23
  header={<AppHeaderHost />}
25
24
  footer={
26
- <PageFooter
27
- logo={<LogoMark />}
28
- versionLink="/releases"
29
- versionLabel="1.0.0-docs"
30
- />
25
+ <PageFooter versionLink="/releases" versionLabel="1.0.0-docs" />
31
26
  }
32
27
  >
33
28
  <SybilionAppHeader
@@ -0,0 +1,61 @@
1
+ import type { SlashCommandItem } from '#uilib/components/ui/Chat';
2
+ import { PageContentSection } from '#uilib/components/ui/Page';
3
+ import { MessageSquare } from 'lucide-react';
4
+
5
+ import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
6
+ import { DOCS_CHAT_USER_KEY } from '../docsConstants';
7
+ import { DocsHeaderActions } from '../docsHeaderActions';
8
+
9
+ const DOCS_CHAT_SHEET_SCOPE_ID = `${DOCS_CHAT_USER_KEY}-docs-chat-sheet-portal`;
10
+
11
+ const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
12
+ {
13
+ id: 'sample-command',
14
+ label: 'sample-command',
15
+ description: 'Demo slash item for portal ChatSheet regression.',
16
+ },
17
+ ];
18
+
19
+ export default function ChatSheetPage() {
20
+ return (
21
+ <>
22
+ <AppPageHeader
23
+ breadcrumbs={[{ label: 'Chat' }, { label: 'Chat sheet' }]}
24
+ title="Chat sheet (portal)"
25
+ subheader="Same integration as design-demo: ChatSheet in page header actions, chat panel portaled into the shell sidebar slot. No inline ChatChrome on this page."
26
+ actions={
27
+ <DocsHeaderActions
28
+ scopeId={DOCS_CHAT_SHEET_SCOPE_ID}
29
+ slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS}
30
+ triggerLabel={
31
+ <>
32
+ <MessageSquare size={20} />
33
+ &nbsp;AI Assistant
34
+ </>
35
+ }
36
+ />
37
+ }
38
+ />
39
+ <PageContentSection>
40
+ <p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
41
+ Open <strong>AI Assistant</strong> in the header. The composer runs
42
+ inside the portaled chat panel (not inline on this page).
43
+ </p>
44
+ <h3 style={{ marginBottom: 8, fontSize: 14, fontWeight: 600 }}>
45
+ Regression checklist
46
+ </h3>
47
+ <ul
48
+ style={{ margin: 0, paddingLeft: 20, fontSize: 14, lineHeight: 1.6 }}
49
+ >
50
+ <li>Plain Enter with text → submit and clear composer</li>
51
+ <li>
52
+ Plain Enter must not create empty lines with duplicated placeholders
53
+ </li>
54
+ <li>Shift+Enter with text → multiline, no placeholder after text</li>
55
+ <li>Empty Enter → no submit</li>
56
+ <li>Submit button and slash menu still work</li>
57
+ </ul>
58
+ </PageContentSection>
59
+ </>
60
+ );
61
+ }
@@ -8,7 +8,6 @@ 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';
12
11
  import { ScrollRef } from '@homecode/ui';
13
12
 
14
13
  import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
@@ -16,65 +15,40 @@ import { DocsHeaderActions } from '../docsHeaderActions';
16
15
 
17
16
  const NO_QUICK_REPLY_KEYS: ReadonlySet<string> = new Set();
18
17
 
19
- const DOCS_SAMPLE_COMMAND_ID = 'sample-command';
20
-
21
- /** Sample items so the docs demo still shows a `/` palette (`DEFAULT_CHAT_SLASH_ITEMS` is empty). */
22
- const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
18
+ /** Sample items — default TipTap mention insert; optional `className` / `color` style the chip. */
19
+ const DOCS_SLASH_ITEMS: SlashCommandItem[] = [
20
+ {
21
+ id: 'generate-dashboard',
22
+ label: 'Generate dashboard',
23
+ description: 'Insert /generate-dashboard',
24
+ color: 'var(--brand-color-600)',
25
+ },
23
26
  {
24
- id: DOCS_SAMPLE_COMMAND_ID,
25
- label: DOCS_SAMPLE_COMMAND_ID,
26
- description:
27
- 'Demo handler — clears composer and runs `onSlashItemCommand` (no mention insert).',
27
+ id: 'performance-chart',
28
+ label: 'Performance chart (bigger font)',
29
+ description: 'Insert /PerformanceChart',
30
+ className: 'docs-slash-mention-accent',
31
+ color: '#16a34a',
32
+ },
33
+ {
34
+ id: 'mention-fallback',
35
+ label: 'Mention fallback',
36
+ description: 'Default mention styling only',
37
+ color: 'var(--foreground)',
28
38
  },
29
39
  ];
30
40
 
31
- const SAMPLE_COMMAND_PROGRESS_TEXT = 'Running sample command…';
32
-
33
- const SAMPLE_COMMAND_SUCCESS_TEXT = '✅ Sample command complete.';
34
-
35
- function makeMessage(
36
- role: MessageRole,
37
- text: string,
38
- options?: { inProgress?: boolean },
39
- ): Message {
41
+ function makeMessage(role: MessageRole, text: string): Message {
40
42
  return {
41
43
  id: crypto.randomUUID(),
42
44
  role,
43
45
  text,
44
46
  timestamp: Date.now(),
45
- ...(options?.inProgress ? { inProgress: true } : {}),
46
47
  };
47
48
  }
48
49
 
49
- /** Local-state equivalent of chat-context `updateMessageById`. */
50
- function updateMessageInPlace(
51
- messages: Message[],
52
- messageId: string,
53
- patch: { role?: MessageRole; text?: string; inProgress?: boolean },
54
- ): Message[] {
55
- return messages.map(message => {
56
- if (message.id !== messageId) return message;
57
- const next: Message = { ...message };
58
- if (patch.role != null) {
59
- next.role = patch.role;
60
- }
61
- if (patch.text != null) {
62
- next.text = patch.text;
63
- }
64
- if (patch.inProgress === true) {
65
- next.inProgress = true;
66
- } else if (
67
- patch.inProgress === false ||
68
- (patch.role != null && patch.role !== MessageRole.SYSTEM)
69
- ) {
70
- delete next.inProgress;
71
- }
72
- return next;
73
- });
74
- }
75
-
76
50
  const ASSISTANT_REPLY_TEXT =
77
- 'Demo reply for a normal message. Slash picks with a custom handler skip mention insert.';
51
+ 'Demo reply. Slash picks insert inline mention chips; plain text on send uses each item id (e.g. /generate-dashboard).';
78
52
 
79
53
  export default function ChatSlashCommandsPage() {
80
54
  const [messages, setMessages] = useState<Message[]>([]);
@@ -95,84 +69,33 @@ export default function ChatSlashCommandsPage() {
95
69
  messages.length > 0 &&
96
70
  messages[messages.length - 1]?.role === MessageRole.USER;
97
71
 
98
- const runSampleCommand = useCallback(() => {
99
- const progressId = crypto.randomUUID();
72
+ const onSubmit = useCallback((raw: string) => {
73
+ const text = raw.trim();
74
+ if (!text) return;
75
+
76
+ setMessages(prev => [...prev, makeMessage(MessageRole.USER, text)]);
100
77
  setIsLoading(true);
101
- setMessages(prev => [
102
- ...prev,
103
- {
104
- id: progressId,
105
- role: MessageRole.SYSTEM,
106
- text: SAMPLE_COMMAND_PROGRESS_TEXT,
107
- timestamp: Date.now(),
108
- inProgress: true,
109
- },
110
- ]);
111
78
 
112
79
  if (replyTimeoutRef.current != null) {
113
80
  clearTimeout(replyTimeoutRef.current);
114
81
  }
115
82
  replyTimeoutRef.current = setTimeout(() => {
116
83
  replyTimeoutRef.current = null;
117
- setMessages(prev =>
118
- updateMessageInPlace(prev, progressId, {
119
- role: MessageRole.ASSISTANT,
120
- text: SAMPLE_COMMAND_SUCCESS_TEXT,
121
- inProgress: false,
122
- }),
123
- );
84
+ setMessages(prev => [
85
+ ...prev,
86
+ makeMessage(MessageRole.ASSISTANT, ASSISTANT_REPLY_TEXT),
87
+ ]);
124
88
  setIsLoading(false);
125
- }, 1200);
89
+ }, 900);
126
90
  }, []);
127
91
 
128
- const onSlashItemCommand = useCallback(
129
- ({ item }: SlashItemCommandContext) => {
130
- if (item.id !== DOCS_SAMPLE_COMMAND_ID) {
131
- return false;
132
- }
133
- queueMicrotask(() => runSampleCommand());
134
- return true;
135
- },
136
- [runSampleCommand],
137
- );
138
-
139
- const onSubmit = useCallback(
140
- (raw: string) => {
141
- const text = raw.trim();
142
- if (!text) return;
143
-
144
- if (text === `/${DOCS_SAMPLE_COMMAND_ID}`) {
145
- runSampleCommand();
146
- return;
147
- }
148
-
149
- setMessages(prev => [...prev, makeMessage(MessageRole.USER, text)]);
150
- setIsLoading(true);
151
-
152
- if (replyTimeoutRef.current != null) {
153
- clearTimeout(replyTimeoutRef.current);
154
- }
155
- replyTimeoutRef.current = setTimeout(() => {
156
- replyTimeoutRef.current = null;
157
- setMessages(prev => [
158
- ...prev,
159
- makeMessage(MessageRole.ASSISTANT, ASSISTANT_REPLY_TEXT),
160
- ]);
161
- setIsLoading(false);
162
- }, 900);
163
- },
164
- [runSampleCommand],
165
- );
166
-
167
92
  return (
168
93
  <>
169
94
  <AppPageHeader
170
95
  breadcrumbs={[{ label: 'Chat' }, { label: 'Chat slash commands' }]}
171
96
  title="Chat slash commands"
172
- 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. Long-running handlers can append a SYSTEM message with inProgress while work is in flight, then update it in place (e.g. via updateMessageById) to a final assistant message when done.`}
173
- actions={
174
- <DocsHeaderActions slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS} />
175
- }
97
+ subheader={`Slash palette uses TipTap Mention with "/" as the trigger. Pass slashCommandItems from the app each item inserts an inline mention chip by default. Optional className and color on SlashCommandItem style the chip in the composer.`}
98
+ actions={<DocsHeaderActions slashCommandItems={DOCS_SLASH_ITEMS} />}
176
99
  />
177
100
  <PageContentSection>
178
101
  <p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
@@ -180,13 +103,17 @@ export default function ChatSlashCommandsPage() {
180
103
  <Link className="underline underline-offset-2" to="/docs/chat">
181
104
  Chat
182
105
  </Link>{' '}
183
- shell below: scrolling history, empty state, disclaimer, composer.
184
- Type <kbd className="font-mono">/</kbd> at line start or after a
185
- space; pick <kbd className="font-mono">sample-command</kbd> to run the
186
- custom handler (shows an <kbd className="font-mono">inProgress</kbd>{' '}
187
- shimmer, then updates the same message to a success assistant reply),
188
- or send <kbd className="font-mono">/sample-command</kbd> with Enter.
106
+ shell below. Type <kbd className="font-mono">/</kbd> at line start or
107
+ after a space, then pick a command the composer inserts a styled
108
+ mention chip. Items can set <kbd className="font-mono">className</kbd>{' '}
109
+ and <kbd className="font-mono">color</kbd> on{' '}
110
+ <kbd className="font-mono">SlashCommandItem</kbd>.
189
111
  </p>
112
+ <style>{`
113
+ .docs-slash-mention-accent {
114
+ font-size: 150%;
115
+ }
116
+ `}</style>
190
117
  <ChatChrome
191
118
  showResizeHandle={false}
192
119
  resizeHandle={undefined}
@@ -207,13 +134,12 @@ export default function ChatSlashCommandsPage() {
207
134
  effectiveScopeId="docs-chat-slash-inline"
208
135
  onPromptSubmit={onSubmit}
209
136
  onChatDeleted={() => {}}
210
- slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS}
211
- onSlashItemCommand={onSlashItemCommand}
137
+ slashCommandItems={DOCS_SLASH_ITEMS}
212
138
  promptPlaceholder='Ask something or type "/" for demo commands…'
213
139
  emptyState={{
214
- title: 'Try a slash command',
140
+ title: 'Try slash commands',
215
141
  description:
216
- 'Pick sample-command from the palette or send /sample-command inProgress placeholder shimmers, then becomes "✅ Sample command complete." in the same slot.',
142
+ 'Pick a command from the palette to insert a mention chip with optional className and color.',
217
143
  }}
218
144
  />
219
145
  </PageContentSection>
@@ -11,9 +11,6 @@ import { DocsHeaderActions } from '../docsHeaderActions';
11
11
 
12
12
  const TOOLTIP_SIDES = ['left', 'top', 'bottom', 'right'] as const;
13
13
 
14
- const OVER_TRIGGER_TEXT =
15
- 'Actual Order volume for Baerlocher MB 301, a compound that includes stearin as a component. MB 301 is typically used in construction-grade applications.';
16
-
17
14
  export default function TooltipPage() {
18
15
  return (
19
16
  <>
@@ -37,34 +34,6 @@ export default function TooltipPage() {
37
34
  ))}
38
35
  </div>
39
36
  </PageContentSection>
40
- <PageContentSection>
41
- <p style={{ margin: '0 0 8px', fontWeight: 600 }}>Over trigger</p>
42
- <div
43
- style={{
44
- maxWidth: 500,
45
- border: '1px dashed var(--border)',
46
- padding: 8,
47
- }}
48
- >
49
- <Tooltip>
50
- <TooltipTrigger asChild>
51
- <div
52
- style={{
53
- overflow: 'hidden',
54
- textOverflow: 'ellipsis',
55
- whiteSpace: 'nowrap',
56
- fontWeight: 600,
57
- }}
58
- >
59
- {OVER_TRIGGER_TEXT}
60
- </div>
61
- </TooltipTrigger>
62
- <TooltipContent overTrigger maxWidth={400}>
63
- {OVER_TRIGGER_TEXT}
64
- </TooltipContent>
65
- </Tooltip>
66
- </div>
67
- </PageContentSection>
68
37
  </>
69
38
  );
70
39
  }
@@ -115,6 +115,12 @@ export const DOC_REGISTRY: DocEntry[] = [
115
115
  section: 'Chat',
116
116
  load: () => import('./pages/ChatSlashCommandsPage'),
117
117
  },
118
+ {
119
+ slug: 'chat-sheet',
120
+ title: 'Chat sheet (portal)',
121
+ section: 'Chat',
122
+ load: () => import('./pages/ChatSheetPage'),
123
+ },
118
124
  {
119
125
  slug: 'chat-user-csv-attachment',
120
126
  title: 'Chat user CSV attachment',
@@ -46,3 +46,7 @@
46
46
  .itemDesc
47
47
  font-size var(--text-xs)
48
48
  color var(--muted-foreground)
49
+
50
+ .mention
51
+ border-radius .3em
52
+ padding 0 .3em
@@ -5,6 +5,7 @@ interface CssExports {
5
5
  'itemDesc': string;
6
6
  'itemHighlighted': string;
7
7
  'itemLabel': string;
8
+ 'mention': string;
8
9
  'root': string;
9
10
  }
10
11
  export const cssExports: CssExports;