@urbicon-ui/blocks 6.34.0 → 6.36.0

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 (127) hide show
  1. package/README.md +11 -1
  2. package/dist/components/Calendar/CalendarHeader.svelte +32 -26
  3. package/dist/components/Calendar/CalendarMiniMonth.svelte +10 -10
  4. package/dist/components/Calendar/calendar.variants.js +19 -5
  5. package/dist/components/Chat/Chat/Chat.svelte +53 -0
  6. package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
  7. package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
  8. package/dist/components/Chat/Chat/chat.variants.js +15 -0
  9. package/dist/components/Chat/Chat/index.d.ts +47 -0
  10. package/dist/components/Chat/Chat/index.js +2 -0
  11. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
  12. package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
  13. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
  14. package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
  15. package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
  16. package/dist/components/Chat/ChatMessage/index.js +2 -0
  17. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
  18. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
  19. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
  20. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
  21. package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
  22. package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
  23. package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
  24. package/dist/components/Chat/ChatMessageList/index.js +3 -0
  25. package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
  26. package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
  27. package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
  28. package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
  29. package/dist/components/Chat/CitationChip/index.d.ts +69 -0
  30. package/dist/components/Chat/CitationChip/index.js +2 -0
  31. package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
  32. package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
  33. package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
  34. package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
  35. package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
  36. package/dist/components/Chat/CodeBlock/index.js +2 -0
  37. package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
  38. package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
  39. package/dist/components/Chat/PromptInput/index.d.ts +165 -0
  40. package/dist/components/Chat/PromptInput/index.js +2 -0
  41. package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
  42. package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
  43. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
  44. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
  45. package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
  46. package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
  47. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
  48. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
  49. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
  50. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
  51. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
  52. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
  53. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
  54. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
  55. package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
  56. package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
  57. package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
  58. package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
  59. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
  60. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
  61. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
  62. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
  63. package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
  64. package/dist/components/Chat/ToolCallCard/index.js +2 -0
  65. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
  66. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
  67. package/dist/components/Chat/chat.types.d.ts +82 -0
  68. package/dist/components/Chat/chat.types.js +7 -0
  69. package/dist/components/Chat/index.d.ts +22 -0
  70. package/dist/components/Chat/index.js +14 -0
  71. package/dist/components/Chat/markdown/blocks.d.ts +54 -0
  72. package/dist/components/Chat/markdown/blocks.js +462 -0
  73. package/dist/components/Chat/markdown/inline.d.ts +31 -0
  74. package/dist/components/Chat/markdown/inline.js +446 -0
  75. package/dist/components/Chat/markdown/repair.d.ts +23 -0
  76. package/dist/components/Chat/markdown/repair.js +337 -0
  77. package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
  78. package/dist/components/Chat/markdown/streaming-harness.js +62 -0
  79. package/dist/components/Chat/markdown/types.d.ts +139 -0
  80. package/dist/components/Chat/markdown/types.js +13 -0
  81. package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
  82. package/dist/components/Chat/markdown/url-policy.js +109 -0
  83. package/dist/components/FileUpload/FileUpload.svelte +52 -125
  84. package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
  85. package/dist/components/FileUpload/index.d.ts +2 -28
  86. package/dist/components/Planner/PlannerHeader.svelte +16 -13
  87. package/dist/components/Planner/planner.variants.js +14 -3
  88. package/dist/components/index.d.ts +1 -0
  89. package/dist/components/index.js +1 -0
  90. package/dist/i18n/index.d.ts +268 -382
  91. package/dist/i18n/index.js +83 -9
  92. package/dist/internal/core/CoreIconButton.svelte +66 -0
  93. package/dist/internal/core/CoreIconButton.svelte.d.ts +13 -0
  94. package/dist/internal/core/CoreSpinner.svelte +49 -0
  95. package/dist/internal/core/CoreSpinner.svelte.d.ts +8 -0
  96. package/dist/internal/core/spinner-geometry.d.ts +6 -0
  97. package/dist/internal/core/spinner-geometry.js +6 -0
  98. package/dist/mint/registry.d.ts +20 -2
  99. package/dist/mint/registry.js +20 -2
  100. package/dist/primitives/Badge/Badge.svelte +5 -5
  101. package/dist/primitives/Badge/badge.variants.js +17 -1
  102. package/dist/primitives/Button/Button.svelte +4 -2
  103. package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
  104. package/dist/primitives/Dialog/Dialog.svelte +7 -5
  105. package/dist/primitives/Dialog/dialog.variants.d.ts +7 -0
  106. package/dist/primitives/Dialog/dialog.variants.js +17 -0
  107. package/dist/primitives/Drawer/Drawer.svelte +7 -5
  108. package/dist/primitives/Drawer/drawer.variants.d.ts +8 -0
  109. package/dist/primitives/Drawer/drawer.variants.js +17 -0
  110. package/dist/primitives/Spinner/Spinner.svelte +3 -1
  111. package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
  112. package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
  113. package/dist/primitives/SplitPane/index.d.ts +88 -0
  114. package/dist/primitives/SplitPane/index.js +2 -0
  115. package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
  116. package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
  117. package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
  118. package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
  119. package/dist/primitives/Toast/Toaster.svelte +17 -2
  120. package/dist/primitives/index.d.ts +2 -0
  121. package/dist/primitives/index.js +1 -0
  122. package/dist/utils/file-intake.d.ts +78 -0
  123. package/dist/utils/file-intake.js +144 -0
  124. package/dist/utils/index.d.ts +1 -0
  125. package/dist/utils/index.js +1 -0
  126. package/dist/utils/variants.js +44 -10
  127. package/package.json +4 -6
@@ -0,0 +1,342 @@
1
+ <script lang="ts">
2
+ import { getBlocksConfig, resolveSlotClasses } from '../../../provider';
3
+ import { Alert, Avatar, Button, Skeleton, Tooltip } from '../../../primitives';
4
+ import { resolveIcon } from '../../../icons';
5
+ import SparklesIconDefault from '../../../icons/SparklesIcon.svelte';
6
+ import UserIconDefault from '../../../icons/UserIcon.svelte';
7
+ import InfoIconDefault from '../../../icons/InfoCircleIcon.svelte';
8
+ import CopyIconDefault from '../../../icons/CopyIcon.svelte';
9
+ import CheckIconDefault from '../../../icons/CheckIcon.svelte';
10
+ import RefreshIconDefault from '../../../icons/RefreshIcon.svelte';
11
+ import FileIconDefault from '../../../icons/FileIcon.svelte';
12
+ import { formatFileSize } from '../../../utils/file-intake';
13
+ import StreamingMarkdown from '../StreamingMarkdown/StreamingMarkdown.svelte';
14
+ import CitationChip from '../CitationChip/CitationChip.svelte';
15
+ import ReasoningDisclosure from '../ReasoningDisclosure/ReasoningDisclosure.svelte';
16
+ import ToolCallCard from '../ToolCallCard/ToolCallCard.svelte';
17
+ import type { CitationSource } from '../CitationChip';
18
+ import { checkLinkUrl } from '../markdown/url-policy.js';
19
+ import type { ChatMessagePart, ChatRole } from '../chat.types';
20
+ import { chatMessageVariants, type ChatMessageVariants } from './chat-message.variants';
21
+ import type { ChatMessageProps } from './index';
22
+
23
+ type AttachmentPart = Extract<ChatMessagePart, { type: 'attachment' }>;
24
+
25
+ let {
26
+ message,
27
+ urlPolicy,
28
+ onRegenerate,
29
+ onRetry,
30
+ avatar,
31
+ actions,
32
+ metadata,
33
+ partRenderers,
34
+ layout = 'bubble',
35
+ density = 'comfortable',
36
+ roleLabels,
37
+ copyLabel = 'Copy message',
38
+ copiedLabel = 'Copied',
39
+ regenerateLabel = 'Regenerate',
40
+ retryLabel = 'Retry',
41
+ errorLabel = 'Something went wrong',
42
+ abortedLabel = 'Generation stopped',
43
+ class: className,
44
+ unstyled: unstyledProp = false,
45
+ slotClasses: slotClassesProp = {},
46
+ preset,
47
+ ...restProps
48
+ }: ChatMessageProps = $props();
49
+
50
+ const blocksConfig = getBlocksConfig();
51
+ const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
52
+
53
+ const SparklesIcon = resolveIcon('sparkles', SparklesIconDefault);
54
+ const UserIcon = resolveIcon('user', UserIconDefault);
55
+ const InfoIcon = resolveIcon('info', InfoIconDefault);
56
+ const CopyIcon = resolveIcon('copy', CopyIconDefault);
57
+ const CheckIcon = resolveIcon('check', CheckIconDefault);
58
+ const RefreshIcon = resolveIcon('refresh', RefreshIconDefault);
59
+ const FileIcon = resolveIcon('file', FileIconDefault);
60
+
61
+ const variantProps: ChatMessageVariants = $derived({ layout, density, role: message.role });
62
+ const styles = $derived(chatMessageVariants(variantProps));
63
+ const slotClasses = $derived(
64
+ resolveSlotClasses(blocksConfig, 'ChatMessage', preset, variantProps, slotClassesProp)
65
+ );
66
+
67
+ function cls(name: keyof typeof slotClasses, extra?: string | (string | undefined)[]) {
68
+ if (unstyled) {
69
+ const own = slotClasses?.[name];
70
+ return [own, ...(Array.isArray(extra) ? extra : [extra])].filter(Boolean).join(' ');
71
+ }
72
+ const slotFns = styles as Record<string, (args: { class?: unknown }) => string>;
73
+ return slotFns[name]({
74
+ class: [slotClasses?.[name], ...(Array.isArray(extra) ? extra : [extra])]
75
+ });
76
+ }
77
+
78
+ const status = $derived(message.status ?? 'complete');
79
+ const lastIndex = $derived(message.parts.length - 1);
80
+
81
+ // Sources are collected out of the flow into a single numbered footer row and
82
+ // also feed StreamingMarkdown so `[id]` markers resolve to chips. Deduped by
83
+ // id (first occurrence wins): models routinely cite one source repeatedly,
84
+ // and a duplicate id would crash the keyed footer each (review finding).
85
+ const sources = $derived.by<CitationSource[]>(() => {
86
+ const byId = new Map<string, CitationSource>();
87
+ for (const p of message.parts) {
88
+ if (p.type === 'source' && !byId.has(p.id)) {
89
+ byId.set(p.id, { id: p.id, title: p.title, url: p.url, snippet: p.snippet });
90
+ }
91
+ }
92
+ return [...byId.values()];
93
+ });
94
+
95
+ // Concatenated text parts — what the copy action writes to the clipboard.
96
+ const copyText = $derived(
97
+ message.parts.flatMap((p) => (p.type === 'text' ? [p.text] : [])).join('\n\n')
98
+ );
99
+
100
+ // Zero-parts streaming placeholder. An empty *trailing* text part already gets
101
+ // StreamingMarkdown's cursor, so only the genuinely empty message needs this.
102
+ const showPlaceholder = $derived(message.status === 'streaming' && message.parts.length === 0);
103
+
104
+ const DEFAULT_ROLE_LABELS: Record<ChatRole, string> = {
105
+ user: 'You',
106
+ assistant: 'Assistant',
107
+ system: 'System'
108
+ };
109
+ const roleLabel = $derived(roleLabels?.[message.role] ?? DEFAULT_ROLE_LABELS[message.role]);
110
+
111
+ const createdAtIso = $derived(message.createdAt?.toISOString());
112
+ const createdAtTime = $derived(
113
+ message.createdAt?.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
114
+ );
115
+
116
+ // ── Copy interaction ─────────────────────────────────────────────────────
117
+ let copied = $state(false);
118
+ let resetTimer: ReturnType<typeof setTimeout> | undefined;
119
+
120
+ async function copyMessage() {
121
+ try {
122
+ await navigator.clipboard.writeText(copyText);
123
+ copied = true;
124
+ if (resetTimer) clearTimeout(resetTimer);
125
+ resetTimer = setTimeout(() => {
126
+ copied = false;
127
+ resetTimer = undefined;
128
+ }, 2000);
129
+ } catch (err) {
130
+ // Never confirm a copy that failed (e.g. denied clipboard permission).
131
+ console.error('ChatMessage: failed to copy message', err);
132
+ }
133
+ }
134
+
135
+ $effect(() => () => {
136
+ if (resetTimer) clearTimeout(resetTimer);
137
+ });
138
+ </script>
139
+
140
+ {#snippet defaultAvatar(role: ChatRole)}
141
+ <Avatar size="sm" class={cls('avatar')}>
142
+ {#if role === 'assistant'}
143
+ <SparklesIcon size={16} />
144
+ {:else if role === 'user'}
145
+ <UserIcon size={16} />
146
+ {:else}
147
+ <InfoIcon size={16} />
148
+ {/if}
149
+ </Avatar>
150
+ {/snippet}
151
+
152
+ {#snippet attachmentChip(part: AttachmentPart)}
153
+ {@const linkCheck =
154
+ part.url != null ? checkLinkUrl(part.url, urlPolicy) : ({ ok: false } as const)}
155
+ {#if linkCheck.ok}
156
+ <a
157
+ class={cls('attachment')}
158
+ href={linkCheck.href}
159
+ target="_blank"
160
+ rel="noopener noreferrer"
161
+ download
162
+ >
163
+ <FileIcon size={16} class={cls('attachmentIcon')} />
164
+ <span class={cls('attachmentName')}>{part.name}</span>
165
+ {#if part.size != null}<span class={cls('attachmentSize')}>{formatFileSize(part.size)}</span
166
+ >{/if}
167
+ </a>
168
+ {:else}
169
+ <span class={cls('attachment')}>
170
+ <FileIcon size={16} class={cls('attachmentIcon')} />
171
+ <span class={cls('attachmentName')}>{part.name}</span>
172
+ {#if part.size != null}<span class={cls('attachmentSize')}>{formatFileSize(part.size)}</span
173
+ >{/if}
174
+ </span>
175
+ {/if}
176
+ {/snippet}
177
+
178
+ {#snippet body()}
179
+ <div class={cls('partsFlow')}>
180
+ {#each message.parts as part, index (index)}
181
+ <!--
182
+ Parts are append-only during streaming: array position IS identity, so
183
+ the index key is stable here (the documented exception to the
184
+ no-index-as-key rule).
185
+ -->
186
+ {#if part.type === 'text'}
187
+ {#if partRenderers?.text}
188
+ {@render partRenderers.text(part)}
189
+ {:else}
190
+ <StreamingMarkdown
191
+ content={part.text}
192
+ streaming={message.status === 'streaming' && index === lastIndex}
193
+ sources={sources.length ? sources : undefined}
194
+ {urlPolicy}
195
+ size="sm"
196
+ />
197
+ {/if}
198
+ {:else if part.type === 'reasoning'}
199
+ {#if partRenderers?.reasoning}
200
+ {@render partRenderers.reasoning(part)}
201
+ {:else}
202
+ <!-- Streaming while it is the trailing part: once the answer text
203
+ starts flowing behind it, the reasoning is settled. -->
204
+ <ReasoningDisclosure
205
+ reasoning={part}
206
+ streaming={status === 'streaming' && index === lastIndex}
207
+ {urlPolicy}
208
+ />
209
+ {/if}
210
+ {:else if part.type === 'tool-call'}
211
+ {#if partRenderers?.['tool-call']}
212
+ {@render partRenderers['tool-call'](part)}
213
+ {:else}
214
+ <ToolCallCard toolCall={part} />
215
+ {/if}
216
+ {:else if part.type === 'attachment'}
217
+ {#if partRenderers?.attachment}
218
+ {@render partRenderers.attachment(part)}
219
+ {:else}
220
+ {@render attachmentChip(part)}
221
+ {/if}
222
+ {:else if part.type === 'a2ui'}
223
+ <!-- default: render nothing. A2UIView lands in P4 via partRenderers. -->
224
+ {#if partRenderers?.a2ui}{@render partRenderers.a2ui(part)}{/if}
225
+ {/if}
226
+ <!-- part.type === 'source' is intentionally skipped: sources render in the
227
+ citation footer, never inline. -->
228
+ {/each}
229
+
230
+ {#if showPlaceholder}
231
+ <div class={cls('placeholder')}>
232
+ <Skeleton variant="text" width="40%" />
233
+ </div>
234
+ {/if}
235
+ </div>
236
+ {/snippet}
237
+
238
+ {#snippet retryAction()}
239
+ <Button size="sm" onclick={onRetry}>
240
+ <RefreshIcon size={14} />
241
+ {retryLabel}
242
+ </Button>
243
+ {/snippet}
244
+
245
+ {#snippet defaultActions()}
246
+ <div class={cls('actions')}>
247
+ <!-- No copy for text-less messages (tool-call/reasoning-only): copying an
248
+ empty string and confirming "Copied" would be a lie (review finding). -->
249
+ {#if copyText}
250
+ <Tooltip label={copyLabel}>
251
+ <button
252
+ type="button"
253
+ class={cls('actionButton')}
254
+ onclick={copyMessage}
255
+ aria-label={copied ? copiedLabel : copyLabel}
256
+ >
257
+ {#if copied}
258
+ <CheckIcon size={16} />
259
+ {:else}
260
+ <CopyIcon size={16} />
261
+ {/if}
262
+ </button>
263
+ </Tooltip>
264
+ {/if}
265
+ {#if onRegenerate}
266
+ <Tooltip label={regenerateLabel}>
267
+ <button
268
+ type="button"
269
+ class={cls('actionButton')}
270
+ onclick={onRegenerate}
271
+ aria-label={regenerateLabel}
272
+ >
273
+ <RefreshIcon size={16} />
274
+ </button>
275
+ </Tooltip>
276
+ {/if}
277
+ </div>
278
+ {/snippet}
279
+
280
+ <div class={cls('root', className)} data-role={message.role} data-status={status} {...restProps}>
281
+ {#if layout === 'plain'}
282
+ <div class={cls('header')}>
283
+ {#if avatar}
284
+ {@render avatar({ role: message.role })}
285
+ {:else}
286
+ {@render defaultAvatar(message.role)}
287
+ {/if}
288
+ <span class={cls('roleName')}>{roleLabel}</span>
289
+ </div>
290
+ <div class={cls('bubble')}>
291
+ {@render body()}
292
+ </div>
293
+ {:else}
294
+ <div class={cls('container')}>
295
+ {#if message.role !== 'user'}
296
+ {#if avatar}
297
+ {@render avatar({ role: message.role })}
298
+ {:else}
299
+ {@render defaultAvatar(message.role)}
300
+ {/if}
301
+ {/if}
302
+ <div class={cls('bubble')}>
303
+ {@render body()}
304
+ </div>
305
+ </div>
306
+ {/if}
307
+
308
+ {#if sources.length}
309
+ <div class={cls('sourcesFooter')}>
310
+ {#each sources as source, i (source.id)}
311
+ <CitationChip {source} index={i + 1} {urlPolicy} />
312
+ {/each}
313
+ </div>
314
+ {/if}
315
+
316
+ {#if status === 'error' || status === 'aborted'}
317
+ <div class={cls('statusAlert')}>
318
+ <Alert
319
+ intent={status === 'error' ? 'danger' : 'warning'}
320
+ title={status === 'error' ? errorLabel : abortedLabel}
321
+ actions={onRetry ? retryAction : undefined}
322
+ />
323
+ </div>
324
+ {/if}
325
+
326
+ <div class={cls('footer')}>
327
+ {#if actions}
328
+ {@render actions({ message })}
329
+ {:else}
330
+ {@render defaultActions()}
331
+ {/if}
332
+ {#if metadata}
333
+ {@render metadata({ message })}
334
+ {:else if createdAtTime}
335
+ <time class={cls('metadata')} datetime={createdAtIso}>{createdAtTime}</time>
336
+ {/if}
337
+ </div>
338
+
339
+ <!-- Copy confirmation for screen readers — a live status region, present
340
+ before `copied` flips so it always announces the change. -->
341
+ <span class="sr-only" role="status">{copied ? copiedLabel : ''}</span>
342
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { ChatMessageProps } from './index.js';
2
+ declare const ChatMessage: import("svelte").Component<ChatMessageProps, {}, "">;
3
+ type ChatMessage = ReturnType<typeof ChatMessage>;
4
+ export default ChatMessage;
@@ -0,0 +1,138 @@
1
+ import { type SlotNames, type VariantProps } from '../../../utils/variants.js';
2
+ export declare const chatMessageVariants: ((props?: {
3
+ layout?: "bubble" | "plain" | undefined;
4
+ role?: "user" | "system" | "assistant" | undefined;
5
+ density?: "comfortable" | "compact" | undefined;
6
+ } | undefined) => {
7
+ root: (props?: ({
8
+ layout?: "bubble" | "plain" | undefined;
9
+ role?: "user" | "system" | "assistant" | undefined;
10
+ density?: "comfortable" | "compact" | undefined;
11
+ } & {
12
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
13
+ }) | undefined) => string;
14
+ container: (props?: ({
15
+ layout?: "bubble" | "plain" | undefined;
16
+ role?: "user" | "system" | "assistant" | undefined;
17
+ density?: "comfortable" | "compact" | undefined;
18
+ } & {
19
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
20
+ }) | undefined) => string;
21
+ header: (props?: ({
22
+ layout?: "bubble" | "plain" | undefined;
23
+ role?: "user" | "system" | "assistant" | undefined;
24
+ density?: "comfortable" | "compact" | undefined;
25
+ } & {
26
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
27
+ }) | undefined) => string;
28
+ roleName: (props?: ({
29
+ layout?: "bubble" | "plain" | undefined;
30
+ role?: "user" | "system" | "assistant" | undefined;
31
+ density?: "comfortable" | "compact" | undefined;
32
+ } & {
33
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
34
+ }) | undefined) => string;
35
+ avatar: (props?: ({
36
+ layout?: "bubble" | "plain" | undefined;
37
+ role?: "user" | "system" | "assistant" | undefined;
38
+ density?: "comfortable" | "compact" | undefined;
39
+ } & {
40
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
41
+ }) | undefined) => string;
42
+ bubble: (props?: ({
43
+ layout?: "bubble" | "plain" | undefined;
44
+ role?: "user" | "system" | "assistant" | undefined;
45
+ density?: "comfortable" | "compact" | undefined;
46
+ } & {
47
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
48
+ }) | undefined) => string;
49
+ partsFlow: (props?: ({
50
+ layout?: "bubble" | "plain" | undefined;
51
+ role?: "user" | "system" | "assistant" | undefined;
52
+ density?: "comfortable" | "compact" | undefined;
53
+ } & {
54
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
55
+ }) | undefined) => string;
56
+ attachment: (props?: ({
57
+ layout?: "bubble" | "plain" | undefined;
58
+ role?: "user" | "system" | "assistant" | undefined;
59
+ density?: "comfortable" | "compact" | undefined;
60
+ } & {
61
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
62
+ }) | undefined) => string;
63
+ attachmentIcon: (props?: ({
64
+ layout?: "bubble" | "plain" | undefined;
65
+ role?: "user" | "system" | "assistant" | undefined;
66
+ density?: "comfortable" | "compact" | undefined;
67
+ } & {
68
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
69
+ }) | undefined) => string;
70
+ attachmentName: (props?: ({
71
+ layout?: "bubble" | "plain" | undefined;
72
+ role?: "user" | "system" | "assistant" | undefined;
73
+ density?: "comfortable" | "compact" | undefined;
74
+ } & {
75
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
76
+ }) | undefined) => string;
77
+ attachmentSize: (props?: ({
78
+ layout?: "bubble" | "plain" | undefined;
79
+ role?: "user" | "system" | "assistant" | undefined;
80
+ density?: "comfortable" | "compact" | undefined;
81
+ } & {
82
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
83
+ }) | undefined) => string;
84
+ sourcesFooter: (props?: ({
85
+ layout?: "bubble" | "plain" | undefined;
86
+ role?: "user" | "system" | "assistant" | undefined;
87
+ density?: "comfortable" | "compact" | undefined;
88
+ } & {
89
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
90
+ }) | undefined) => string;
91
+ placeholder: (props?: ({
92
+ layout?: "bubble" | "plain" | undefined;
93
+ role?: "user" | "system" | "assistant" | undefined;
94
+ density?: "comfortable" | "compact" | undefined;
95
+ } & {
96
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
97
+ }) | undefined) => string;
98
+ statusAlert: (props?: ({
99
+ layout?: "bubble" | "plain" | undefined;
100
+ role?: "user" | "system" | "assistant" | undefined;
101
+ density?: "comfortable" | "compact" | undefined;
102
+ } & {
103
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
104
+ }) | undefined) => string;
105
+ footer: (props?: ({
106
+ layout?: "bubble" | "plain" | undefined;
107
+ role?: "user" | "system" | "assistant" | undefined;
108
+ density?: "comfortable" | "compact" | undefined;
109
+ } & {
110
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
111
+ }) | undefined) => string;
112
+ actions: (props?: ({
113
+ layout?: "bubble" | "plain" | undefined;
114
+ role?: "user" | "system" | "assistant" | undefined;
115
+ density?: "comfortable" | "compact" | undefined;
116
+ } & {
117
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
118
+ }) | undefined) => string;
119
+ actionButton: (props?: ({
120
+ layout?: "bubble" | "plain" | undefined;
121
+ role?: "user" | "system" | "assistant" | undefined;
122
+ density?: "comfortable" | "compact" | undefined;
123
+ } & {
124
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
125
+ }) | undefined) => string;
126
+ metadata: (props?: ({
127
+ layout?: "bubble" | "plain" | undefined;
128
+ role?: "user" | "system" | "assistant" | undefined;
129
+ density?: "comfortable" | "compact" | undefined;
130
+ } & {
131
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
132
+ }) | undefined) => string;
133
+ }) & {
134
+ readonly config: import("../../../utils/variants.js").TVConfig;
135
+ };
136
+ export type ChatMessageVariants = VariantProps<typeof chatMessageVariants>;
137
+ /** Slot names derived from the tv() config — single source of truth for slotClasses. */
138
+ export type ChatMessageSlots = SlotNames<typeof chatMessageVariants>;
@@ -0,0 +1,118 @@
1
+ import { tv } from '../../../utils/variants.js';
2
+ export const chatMessageVariants = tv({
3
+ slots: {
4
+ // Group scope drives the hover/focus reveal of the action bar. `min-w-0`
5
+ // so long code/markdown scrolls inside the message, never the page.
6
+ root: ['group/message flex w-full min-w-0 flex-col'],
7
+ // Bubble layout: avatar beside the bubble. Plain layout ignores this and
8
+ // uses `header` + `content` instead.
9
+ container: ['flex min-w-0 items-start gap-3'],
10
+ // Plain-layout header row: avatar + role name above the full-width content.
11
+ header: ['flex min-w-0 items-center gap-2'],
12
+ roleName: ['text-sm font-medium text-text-secondary select-none'],
13
+ avatar: ['shrink-0'],
14
+ // The content bubble (bubble layout) / the full-width column (plain layout).
15
+ bubble: ['min-w-0 max-w-full'],
16
+ // Vertical stack of the rendered parts.
17
+ partsFlow: ['flex min-w-0 flex-col'],
18
+ // reasoning and tool-call parts render through ReasoningDisclosure /
19
+ // ToolCallCard (own slotClasses there); override via partRenderers.
20
+ // attachments
21
+ attachment: [
22
+ 'inline-flex max-w-full items-center gap-2 rounded-modify border border-border-subtle bg-surface-base px-2.5 py-1.5 text-sm no-underline',
23
+ 'transition-[color,background-color] duration-[var(--blocks-duration-fast)] ease-out'
24
+ ],
25
+ attachmentIcon: ['shrink-0 text-text-tertiary'],
26
+ attachmentName: ['min-w-0 truncate text-text-primary'],
27
+ attachmentSize: ['shrink-0 text-xs text-text-tertiary'],
28
+ // sources footer
29
+ sourcesFooter: ['flex flex-wrap items-center gap-1.5 pt-1'],
30
+ // streaming placeholder (zero parts)
31
+ placeholder: ['py-1'],
32
+ // status alert
33
+ statusAlert: ['mt-1'],
34
+ // footer row holding actions + metadata
35
+ footer: ['flex items-center gap-2'],
36
+ actions: [
37
+ 'flex items-center gap-1 opacity-0',
38
+ 'transition-opacity duration-[var(--blocks-duration-fast)] ease-out',
39
+ 'group-hover/message:opacity-100 group-focus-within/message:opacity-100'
40
+ ],
41
+ actionButton: [
42
+ 'inline-flex items-center justify-center rounded-modify p-1.5',
43
+ 'text-text-tertiary cursor-pointer',
44
+ 'transition-[color,background-color] duration-[var(--blocks-duration-fast)] ease-out',
45
+ 'hover:bg-surface-hover hover:text-text-primary',
46
+ 'focus-visible:outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1'
47
+ ],
48
+ metadata: ['text-xs text-text-tertiary']
49
+ },
50
+ variants: {
51
+ layout: {
52
+ bubble: {},
53
+ // Document-like: full-width content under an avatar + role header row.
54
+ plain: {
55
+ bubble: 'w-full',
56
+ partsFlow: 'gap-3'
57
+ }
58
+ },
59
+ role: {
60
+ user: {},
61
+ assistant: {},
62
+ system: {}
63
+ },
64
+ density: {
65
+ comfortable: {
66
+ root: 'gap-1.5',
67
+ partsFlow: 'gap-2'
68
+ },
69
+ compact: {
70
+ root: 'gap-1',
71
+ partsFlow: 'gap-1.5'
72
+ }
73
+ }
74
+ },
75
+ compoundVariants: [
76
+ // ── bubble layout: role tint + alignment ────────────────────────────────
77
+ {
78
+ layout: 'bubble',
79
+ role: 'user',
80
+ class: {
81
+ // Reverse so the (avatar-less) bubble packs to the right edge.
82
+ container: 'flex-row-reverse',
83
+ bubble: 'max-w-[85%] rounded-contain bg-primary-subtle'
84
+ }
85
+ },
86
+ {
87
+ layout: 'bubble',
88
+ role: 'assistant',
89
+ class: {
90
+ bubble: 'max-w-[85%] rounded-contain bg-surface-elevated'
91
+ }
92
+ },
93
+ {
94
+ layout: 'bubble',
95
+ role: 'system',
96
+ class: {
97
+ container: 'justify-center',
98
+ bubble: 'max-w-[90%] rounded-contain border border-border-subtle bg-surface-base text-sm text-text-secondary'
99
+ }
100
+ },
101
+ // ── bubble layout: density padding ──────────────────────────────────────
102
+ {
103
+ layout: 'bubble',
104
+ density: 'comfortable',
105
+ class: { bubble: 'px-3.5 py-2.5' }
106
+ },
107
+ {
108
+ layout: 'bubble',
109
+ density: 'compact',
110
+ class: { bubble: 'px-3 py-2' }
111
+ }
112
+ ],
113
+ defaultVariants: {
114
+ layout: 'bubble',
115
+ role: 'assistant',
116
+ density: 'comfortable'
117
+ }
118
+ });