@sybilion/uilib 1.3.44 → 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 (43) 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 +5 -3
  5. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.styl.js +1 -1
  6. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptComposer.js +7 -3
  7. package/dist/esm/components/ui/Chat/ChatPrompt/chatPromptComposerInsert.js +67 -0
  8. package/dist/esm/components/ui/Logo/Logo.styl.js +1 -1
  9. package/dist/esm/components/ui/Logo/logo.svg.js +1 -1
  10. package/dist/esm/components/ui/Page/PageFooter/PageFooter.js +3 -2
  11. package/dist/esm/components/widgets/DriversComparisonChart/DriversComparisonChart.js +1 -0
  12. package/dist/esm/components/widgets/PerformanceChart/PerformanceTable.js +1 -0
  13. package/dist/esm/index.js +1 -0
  14. package/dist/esm/tiptap/slash-mention/SlashSuggestionList.styl.js +2 -2
  15. package/dist/esm/tiptap/slash-mention/createSlashMentionExtension.js +45 -28
  16. package/dist/esm/types/src/components/ui/Chat/Chat.d.ts +1 -2
  17. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +1 -1
  18. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +3 -1
  19. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.d.ts +3 -1
  20. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.types.d.ts +2 -0
  21. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/chatPromptComposerInsert.d.ts +35 -0
  22. package/dist/esm/types/src/components/ui/Chat/index.d.ts +3 -0
  23. package/dist/esm/types/src/components/ui/Page/PageFooter/PageFooter.d.ts +1 -2
  24. package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +7 -2
  25. package/package.json +1 -1
  26. package/src/components/ui/Chart/Chart.styl +2 -1
  27. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +8 -1
  28. package/src/components/ui/Chat/Chat.types.ts +1 -1
  29. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl +25 -0
  30. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +94 -71
  31. package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.tsx +35 -10
  32. package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.types.ts +11 -0
  33. package/src/components/ui/Chat/ChatPrompt/chatPromptComposerInsert.ts +122 -0
  34. package/src/components/ui/Chat/index.ts +9 -0
  35. package/src/components/ui/Logo/Logo.styl +1 -0
  36. package/src/components/ui/Logo/logo.svg +1 -1
  37. package/src/components/ui/Page/PageFooter/PageFooter.tsx +2 -3
  38. package/src/docs/DocsShell.tsx +1 -6
  39. package/src/docs/pages/ChatSlashCommandsPage.tsx +46 -120
  40. package/src/tiptap/slash-mention/SlashSuggestionList.styl +4 -0
  41. package/src/tiptap/slash-mention/SlashSuggestionList.styl.d.ts +1 -0
  42. package/src/tiptap/slash-mention/createSlashMentionExtension.ts +46 -27
  43. package/src/tiptap/slash-mention/types.ts +7 -2
@@ -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>
@@ -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;
@@ -10,6 +10,7 @@ import {
10
10
  SlashSuggestionList,
11
11
  type SlashSuggestionListHandle,
12
12
  } from './SlashSuggestionList';
13
+ import S from './SlashSuggestionList.styl';
13
14
  import { filterSlashItems } from './defaultChatSlashItems';
14
15
  import type {
15
16
  CreateSlashMentionExtensionOptions,
@@ -17,6 +18,30 @@ import type {
17
18
  SlashSuggestionPlacement,
18
19
  } from './types';
19
20
 
21
+ const SlashMention = Mention.extend({
22
+ addAttributes() {
23
+ return {
24
+ ...this.parent?.(),
25
+ className: {
26
+ default: null,
27
+ parseHTML: element => element.getAttribute('data-slash-class'),
28
+ renderHTML: attributes => {
29
+ if (!attributes.className) return {};
30
+ return { 'data-slash-class': attributes.className };
31
+ },
32
+ },
33
+ color: {
34
+ default: null,
35
+ parseHTML: element => element.getAttribute('data-slash-color'),
36
+ renderHTML: attributes => {
37
+ if (!attributes.color) return {};
38
+ return { 'data-slash-color': attributes.color };
39
+ },
40
+ },
41
+ };
42
+ },
43
+ });
44
+
20
45
  const SUGGESTION_GAP_PX = 4;
21
46
 
22
47
  function placeSlashSuggestionPopup(
@@ -124,28 +149,11 @@ export function slashMentionSuggestionRender(
124
149
  };
125
150
  }
126
151
 
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
- }
152
+ function slashMentionChipStyle(color: string | null): string | undefined {
153
+ if (!color) return undefined;
154
+ return `color: ${color}; background-color: color-mix(in srgb, ${color} 30%, transparent);`;
137
155
  }
138
156
 
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
- }
149
157
  function insertDefaultMention(
150
158
  editor: Editor,
151
159
  range: { from: number; to: number },
@@ -166,6 +174,8 @@ function insertDefaultMention(
166
174
  id: props.id,
167
175
  label: props.label,
168
176
  mentionSuggestionChar: slashChar,
177
+ className: props.className ?? null,
178
+ color: props.color ?? null,
169
179
  },
170
180
  },
171
181
  { type: 'text', text: ' ' },
@@ -212,27 +222,38 @@ export function createSlashMentionExtension({
212
222
  current: null,
213
223
  };
214
224
 
215
- return Mention.configure({
225
+ return SlashMention.configure({
216
226
  renderText({ node }) {
217
227
  return `/${node.attrs.id as string}`;
218
228
  },
219
- renderHTML({ options, node, suggestion }) {
220
- const suggestionChar = suggestion?.char ?? slashChar;
229
+ renderHTML({ options, node }) {
221
230
  const id =
222
231
  typeof node.attrs.id === 'string'
223
232
  ? node.attrs.id
224
233
  : String(node.attrs.id ?? '');
234
+ const label =
235
+ typeof node.attrs.label === 'string' && node.attrs.label.trim() !== ''
236
+ ? node.attrs.label
237
+ : id;
238
+ const color =
239
+ typeof node.attrs.color === 'string' ? node.attrs.color : null;
240
+ const className = [S.mention, node.attrs.className]
241
+ .filter(Boolean)
242
+ .join(' ');
243
+ const chipStyle = slashMentionChipStyle(color);
244
+
225
245
  return [
226
246
  'span',
227
247
  mergeAttributes(
228
248
  {
229
249
  'data-type': 'mention',
230
250
  'data-slash-command': id,
231
- class: 'slash-mention',
251
+ class: className,
252
+ ...(chipStyle ? { style: chipStyle } : {}),
232
253
  },
233
254
  options.HTMLAttributes,
234
255
  ),
235
- `${suggestionChar}${id}`,
256
+ label,
236
257
  ];
237
258
  },
238
259
  suggestion: {
@@ -245,8 +266,6 @@ export function createSlashMentionExtension({
245
266
  command: ({ editor, range, props }) => {
246
267
  const item = props as SlashCommandItem;
247
268
  if (onItemCommand?.({ editor, range, item }) === true) {
248
- clearSlashTriggerEditor(editor, range);
249
- queueMicrotask(() => collapseEditorSelectionEnd(editor));
250
269
  return null;
251
270
  }
252
271
  insertDefaultMention(editor, range, item, slashChar);
@@ -4,6 +4,10 @@ export type SlashCommandItem = {
4
4
  id: string;
5
5
  label: string;
6
6
  description?: string;
7
+ /** Extra class on the inserted mention chip (merged with `slash-mention`). */
8
+ className?: string;
9
+ /** CSS color for chip text; background is 30% of this color mixed with transparent. */
10
+ color?: string;
7
11
  };
8
12
 
9
13
  export type SlashItemCommandContext = {
@@ -14,8 +18,9 @@ export type SlashItemCommandContext = {
14
18
  };
15
19
 
16
20
  /**
17
- * If provided, run when a slash item is picked. Return true to skip mention insert
18
- * (extension clears the trigger text from the composer).
21
+ * If provided, run when a slash item is picked. Return true to skip default mention insert
22
+ * and handle the composer yourself (e.g. `createChatPromptComposerHandle(editor).insertAtCaret`
23
+ * with `{ replaceRange: range }` to replace `/query` with a custom chip).
19
24
  */
20
25
  export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
21
26