@urbicon-ui/blocks 6.35.0 → 6.37.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 (124) hide show
  1. package/dist/components/Chat/Chat/Chat.svelte +53 -0
  2. package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
  3. package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
  4. package/dist/components/Chat/Chat/chat.variants.js +15 -0
  5. package/dist/components/Chat/Chat/index.d.ts +47 -0
  6. package/dist/components/Chat/Chat/index.js +2 -0
  7. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
  8. package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
  9. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
  10. package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
  11. package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
  12. package/dist/components/Chat/ChatMessage/index.js +2 -0
  13. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
  14. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
  15. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
  16. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
  17. package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
  18. package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
  19. package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
  20. package/dist/components/Chat/ChatMessageList/index.js +3 -0
  21. package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
  22. package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
  23. package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
  24. package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
  25. package/dist/components/Chat/CitationChip/index.d.ts +69 -0
  26. package/dist/components/Chat/CitationChip/index.js +2 -0
  27. package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
  28. package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
  29. package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
  30. package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
  31. package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
  32. package/dist/components/Chat/CodeBlock/index.js +2 -0
  33. package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
  34. package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
  35. package/dist/components/Chat/PromptInput/index.d.ts +165 -0
  36. package/dist/components/Chat/PromptInput/index.js +2 -0
  37. package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
  38. package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
  39. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
  40. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
  41. package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
  42. package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
  43. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
  44. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
  45. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
  46. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
  47. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
  48. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
  49. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
  50. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
  51. package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
  52. package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
  53. package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
  54. package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
  55. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
  56. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
  57. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
  58. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
  59. package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
  60. package/dist/components/Chat/ToolCallCard/index.js +2 -0
  61. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
  62. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
  63. package/dist/components/Chat/chat.types.d.ts +82 -0
  64. package/dist/components/Chat/chat.types.js +7 -0
  65. package/dist/components/Chat/index.d.ts +22 -0
  66. package/dist/components/Chat/index.js +14 -0
  67. package/dist/components/Chat/markdown/blocks.d.ts +54 -0
  68. package/dist/components/Chat/markdown/blocks.js +462 -0
  69. package/dist/components/Chat/markdown/inline.d.ts +31 -0
  70. package/dist/components/Chat/markdown/inline.js +446 -0
  71. package/dist/components/Chat/markdown/repair.d.ts +23 -0
  72. package/dist/components/Chat/markdown/repair.js +337 -0
  73. package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
  74. package/dist/components/Chat/markdown/streaming-harness.js +62 -0
  75. package/dist/components/Chat/markdown/types.d.ts +139 -0
  76. package/dist/components/Chat/markdown/types.js +13 -0
  77. package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
  78. package/dist/components/Chat/markdown/url-policy.js +109 -0
  79. package/dist/components/FileUpload/FileUpload.svelte +35 -123
  80. package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
  81. package/dist/components/FileUpload/index.d.ts +2 -28
  82. package/dist/components/PinInput/PinInput.svelte +321 -0
  83. package/dist/components/PinInput/PinInput.svelte.d.ts +4 -0
  84. package/dist/components/PinInput/index.d.ts +98 -0
  85. package/dist/components/PinInput/index.js +2 -0
  86. package/dist/components/PinInput/pin-input.variants.d.ts +96 -0
  87. package/dist/components/PinInput/pin-input.variants.js +111 -0
  88. package/dist/components/QRCode/QRCode.svelte +107 -0
  89. package/dist/components/QRCode/QRCode.svelte.d.ts +4 -0
  90. package/dist/components/QRCode/index.d.ts +74 -0
  91. package/dist/components/QRCode/index.js +3 -0
  92. package/dist/components/QRCode/qr-code.variants.d.ts +25 -0
  93. package/dist/components/QRCode/qr-code.variants.js +24 -0
  94. package/dist/components/QRCode/qr-encode.d.ts +22 -0
  95. package/dist/components/QRCode/qr-encode.js +528 -0
  96. package/dist/components/TimeInput/TimeInput.svelte +521 -0
  97. package/dist/components/TimeInput/TimeInput.svelte.d.ts +4 -0
  98. package/dist/components/TimeInput/index.d.ts +93 -0
  99. package/dist/components/TimeInput/index.js +2 -0
  100. package/dist/components/TimeInput/time-input.variants.d.ts +131 -0
  101. package/dist/components/TimeInput/time-input.variants.js +120 -0
  102. package/dist/components/index.d.ts +7 -0
  103. package/dist/components/index.js +4 -0
  104. package/dist/i18n/index.d.ts +18 -0
  105. package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
  106. package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
  107. package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
  108. package/dist/primitives/SplitPane/index.d.ts +88 -0
  109. package/dist/primitives/SplitPane/index.js +2 -0
  110. package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
  111. package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
  112. package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
  113. package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
  114. package/dist/primitives/index.d.ts +2 -0
  115. package/dist/primitives/index.js +1 -0
  116. package/dist/translations/de.d.ts +6 -0
  117. package/dist/translations/de.js +6 -0
  118. package/dist/translations/en.d.ts +6 -0
  119. package/dist/translations/en.js +6 -0
  120. package/dist/utils/file-intake.d.ts +78 -0
  121. package/dist/utils/file-intake.js +144 -0
  122. package/dist/utils/index.d.ts +1 -0
  123. package/dist/utils/index.js +1 -0
  124. package/package.json +3 -3
@@ -0,0 +1,301 @@
1
+ <script lang="ts">
2
+ import { getBlocksConfig, resolveSlotClasses } from '../../../provider';
3
+ import { resolveIcon } from '../../../icons';
4
+ import ArrowDownIconDefault from '../../../icons/ArrowDownIcon.svelte';
5
+ import MessageSquareIconDefault from '../../../icons/MessageSquareIcon.svelte';
6
+ import { Badge } from '../../../primitives/Badge';
7
+ import { EmptyState } from '../../EmptyState';
8
+ import type { ChatMessageData, ChatMessageStatus } from '../chat.types';
9
+ import ChatMessage from '../ChatMessage/ChatMessage.svelte';
10
+ import { chatMessageListVariants } from './chat-message-list.variants';
11
+ import {
12
+ appendedCount,
13
+ classifyTransition,
14
+ type ListIdSnapshot,
15
+ type ListTransition,
16
+ resolveScrollIntent
17
+ } from './chat-scroll';
18
+ import type { ChatMessageListProps } from './index';
19
+
20
+ const ArrowDownIcon = resolveIcon('arrowDown', ArrowDownIconDefault);
21
+ const MessageSquareIcon = resolveIcon('messageSquare', MessageSquareIconDefault);
22
+
23
+ let {
24
+ messages,
25
+ message: messageSnippet,
26
+ empty,
27
+ urlPolicy,
28
+ partRenderers,
29
+ layout,
30
+ density,
31
+ onRegenerate,
32
+ onRetry,
33
+ onStickChange,
34
+ listLabel = 'Conversation',
35
+ scrollToBottomLabel = 'Scroll to bottom',
36
+ newMessagesLabel = 'New messages',
37
+ generatingLabel = 'Generating response…',
38
+ errorLabel = 'Something went wrong',
39
+ abortedLabel = 'Generation stopped',
40
+ emptyTitle = 'No messages yet',
41
+ emptyDescription,
42
+ class: className = '',
43
+ unstyled: unstyledProp = false,
44
+ slotClasses: slotClassesProp = {},
45
+ preset,
46
+ ...restProps
47
+ }: ChatMessageListProps = $props();
48
+
49
+ const blocksConfig = getBlocksConfig();
50
+ const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
51
+
52
+ const styles = $derived(chatMessageListVariants({}));
53
+ const slotClasses = $derived(
54
+ resolveSlotClasses(blocksConfig, 'ChatMessageList', preset, {}, slotClassesProp)
55
+ );
56
+
57
+ function slot(name: keyof typeof styles, extra?: string | false): string {
58
+ const overrides = [slotClasses?.[name], extra].filter(Boolean).join(' ');
59
+ if (unstyled) return overrides;
60
+ return styles[name]({ class: overrides });
61
+ }
62
+
63
+ // ── Stick state ──────────────────────────────────────────────────────────
64
+ // `stuck` = the list follows growing content. See chat-scroll.ts for the
65
+ // direction/proximity model that classifies scroll events without any
66
+ // "programmatic scroll" suppression flag.
67
+ let viewportRef = $state<HTMLDivElement>();
68
+ let contentRef = $state<HTMLDivElement>();
69
+ let stuck = $state(true);
70
+ let newCount = $state(0);
71
+ let lastScrollTop = 0;
72
+
73
+ const showJumpButton = $derived(!stuck && messages.length > 0);
74
+
75
+ function scrollNow(behavior: ScrollBehavior = 'instant') {
76
+ const el = viewportRef;
77
+ if (!el) return;
78
+ el.scrollTo({ top: el.scrollHeight, behavior });
79
+ }
80
+
81
+ /**
82
+ * Scroll to the newest message and resume following. Exposed on the
83
+ * instance (`bind:this`) for consumer-driven jumps.
84
+ */
85
+ export function scrollToBottom(behavior: ScrollBehavior = 'instant') {
86
+ setStuck(true);
87
+ scrollNow(behavior);
88
+ }
89
+
90
+ function setStuck(next: boolean) {
91
+ if (stuck === next) return;
92
+ stuck = next;
93
+ if (next) newCount = 0;
94
+ onStickChange?.(next);
95
+ }
96
+
97
+ function handleScroll() {
98
+ const el = viewportRef;
99
+ if (!el) return;
100
+ const snapshot = {
101
+ scrollTop: el.scrollTop,
102
+ scrollHeight: el.scrollHeight,
103
+ clientHeight: el.clientHeight
104
+ };
105
+ const intent = resolveScrollIntent(snapshot, lastScrollTop);
106
+ lastScrollTop = snapshot.scrollTop;
107
+ if (intent === 'stick') setStuck(true);
108
+ else if (intent === 'unstick') setStuck(false);
109
+ }
110
+
111
+ // Follow growing content (streaming tokens) and viewport resizes while
112
+ // stuck. Content growth below the fold never fires scroll events, so the
113
+ // engine observes sizes instead.
114
+ $effect(() => {
115
+ const viewport = viewportRef;
116
+ const content = contentRef;
117
+ if (!viewport || typeof ResizeObserver === 'undefined') return;
118
+ const ro = new ResizeObserver(() => {
119
+ if (stuck) scrollNow();
120
+ });
121
+ ro.observe(viewport);
122
+ if (content) ro.observe(content);
123
+ return () => ro.disconnect();
124
+ });
125
+
126
+ // ── List transitions (append / prepend / replace) ────────────────────────
127
+ // The pre-effect classifies the change and snapshots the scroll metrics
128
+ // BEFORE the DOM updates; the post-effect applies the matching correction
129
+ // after. Plain (non-reactive) carriers hand the verdict across the pair.
130
+ let prevIds: ListIdSnapshot = { firstId: undefined, lastId: undefined, length: 0 };
131
+ let pendingTransition: ListTransition = 'none';
132
+ let pendingPrependHeight = 0;
133
+ let pendingPrependTop = 0;
134
+ let pendingAppended = 0;
135
+
136
+ $effect.pre(() => {
137
+ const next: ListIdSnapshot = {
138
+ firstId: messages[0]?.id,
139
+ lastId: messages[messages.length - 1]?.id,
140
+ length: messages.length
141
+ };
142
+ const transition = classifyTransition(prevIds, next);
143
+ pendingTransition = transition;
144
+ if (transition === 'append') pendingAppended = appendedCount(prevIds.length, next.length);
145
+ if (transition === 'prepend' && viewportRef) {
146
+ pendingPrependHeight = viewportRef.scrollHeight;
147
+ pendingPrependTop = viewportRef.scrollTop;
148
+ }
149
+ prevIds = next;
150
+ });
151
+
152
+ $effect(() => {
153
+ // Subscribe to the SAME signals as the pre-effect (length + boundary ids),
154
+ // not just the array reference: an in-place `messages.push(m)` — the
155
+ // idiomatic Svelte-5 stream append — changes length but not the reference,
156
+ // and `void messages` alone would miss it (review finding, P2 wave).
157
+ void messages.length;
158
+ void messages[0]?.id;
159
+ void messages[messages.length - 1]?.id;
160
+ const transition = pendingTransition;
161
+ pendingTransition = 'none';
162
+
163
+ if (transition === 'prepend' && viewportRef) {
164
+ // Keep the previously visible message visually still: shift scrollTop
165
+ // by exactly the height the prepended history added above it.
166
+ const delta = viewportRef.scrollHeight - pendingPrependHeight;
167
+ viewportRef.scrollTop = pendingPrependTop + delta;
168
+ lastScrollTop = viewportRef.scrollTop;
169
+ } else if (transition === 'initial' || transition === 'replace') {
170
+ // A conversation opens (or resets) at its newest message.
171
+ setStuck(true);
172
+ scrollNow();
173
+ } else if (transition === 'append') {
174
+ if (stuck) scrollNow();
175
+ else newCount += pendingAppended;
176
+ }
177
+ });
178
+
179
+ // ── Screen-reader strategy ───────────────────────────────────────────────
180
+ // The visible log is aria-live="off" — announcing every streamed token is
181
+ // spam. A visually hidden status region announces the start of generation
182
+ // and, when a reply settles, its full text once (a deliberate trade-off:
183
+ // the answer is what the user asked for; a short cue would force a manual
184
+ // hunt through the log). Transitions settling in the SAME tick collapse to
185
+ // the last one — accepted: parallel assistant streams are out of scope.
186
+ let announcement = $state('');
187
+ // Deliberately a plain Map, NOT SvelteMap: it is only read/written inside
188
+ // the effect below — reactivity here would re-trigger that very effect.
189
+ const announcedStatus = new Map<string, ChatMessageStatus | undefined>();
190
+ // First effect run only REGISTERS the mounted history — announcing it would
191
+ // read the newest old message aloud on every conversation open.
192
+ let announceSyncDone = false;
193
+
194
+ function messageText(msg: ChatMessageData): string {
195
+ return msg.parts.flatMap((p) => (p.type === 'text' ? [p.text] : [])).join('\n\n');
196
+ }
197
+
198
+ function settledAnnouncement(msg: ChatMessageData): string {
199
+ if (msg.status === 'error') return errorLabel;
200
+ if (msg.status === 'aborted') return abortedLabel;
201
+ return messageText(msg);
202
+ }
203
+
204
+ $effect(() => {
205
+ const seen = new Set<string>();
206
+ const announceable = announceSyncDone;
207
+ for (const msg of messages) {
208
+ seen.add(msg.id);
209
+ if (msg.role !== 'assistant') continue;
210
+ const prev = announcedStatus.get(msg.id);
211
+ const known = announcedStatus.has(msg.id);
212
+ if (msg.status === 'streaming' && prev !== 'streaming') {
213
+ if (announceable || !known) announcement = generatingLabel;
214
+ } else if (known && prev === 'streaming' && msg.status !== 'streaming') {
215
+ // Settle: text when there is any, the error/aborted label otherwise —
216
+ // never leave a stale "generating" in the region (review finding).
217
+ announcement = settledAnnouncement(msg) || '';
218
+ } else if (announceable && !known && msg.status !== 'streaming') {
219
+ // A reply that arrives already settled (fast/cached models stream
220
+ // nothing) must still be announced (review finding).
221
+ announcement = settledAnnouncement(msg) || announcement;
222
+ }
223
+ announcedStatus.set(msg.id, msg.status);
224
+ }
225
+ for (const id of announcedStatus.keys()) {
226
+ if (!seen.has(id)) announcedStatus.delete(id);
227
+ }
228
+ announceSyncDone = true;
229
+ });
230
+ </script>
231
+
232
+ <div
233
+ {...restProps}
234
+ class={unstyled
235
+ ? [slotClasses?.root, className].filter(Boolean).join(' ')
236
+ : styles.root({ class: [slotClasses?.root, className] })}
237
+ data-stuck={stuck || undefined}
238
+ >
239
+ <!-- Focusable scroll container (axe: scrollable-region-focusable) -->
240
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
241
+ <div
242
+ bind:this={viewportRef}
243
+ class={slot('viewport')}
244
+ role="region"
245
+ aria-label={listLabel}
246
+ tabindex="0"
247
+ onscroll={handleScroll}
248
+ >
249
+ {#if messages.length === 0}
250
+ <div class={slot('empty')}>
251
+ {#if empty}
252
+ {@render empty()}
253
+ {:else}
254
+ <EmptyState icon={MessageSquareIcon} title={emptyTitle} description={emptyDescription} />
255
+ {/if}
256
+ </div>
257
+ {:else}
258
+ <div bind:this={contentRef} class={slot('content')} role="log" aria-live="off">
259
+ {#each messages as msg, i (msg.id)}
260
+ {@const isLast = i === messages.length - 1}
261
+ {#if messageSnippet}
262
+ {@render messageSnippet({ message: msg, index: i, isLast })}
263
+ {:else}
264
+ <ChatMessage
265
+ message={msg}
266
+ {urlPolicy}
267
+ {partRenderers}
268
+ {layout}
269
+ {density}
270
+ onRegenerate={isLast && msg.role === 'assistant' && onRegenerate
271
+ ? () => onRegenerate(msg)
272
+ : undefined}
273
+ onRetry={onRetry ? () => onRetry(msg) : undefined}
274
+ />
275
+ {/if}
276
+ {/each}
277
+ </div>
278
+ {/if}
279
+ </div>
280
+
281
+ {#if showJumpButton}
282
+ <button
283
+ type="button"
284
+ class={slot('newButton')}
285
+ aria-label={newCount > 0 ? `${newCount} ${newMessagesLabel}` : scrollToBottomLabel}
286
+ onclick={() => {
287
+ scrollToBottom();
288
+ // Re-sticking unmounts this button — hand focus to the viewport so a
289
+ // keyboard activation does not drop focus to <body> (WCAG 2.4.3).
290
+ viewportRef?.focus();
291
+ }}
292
+ >
293
+ <ArrowDownIcon size={16} />
294
+ {#if newCount > 0}
295
+ <Badge intent="primary" size="xs" counter>{newCount}</Badge>
296
+ {/if}
297
+ </button>
298
+ {/if}
299
+
300
+ <div class="sr-only" role="status">{announcement}</div>
301
+ </div>
@@ -0,0 +1,6 @@
1
+ import type { ChatMessageListProps } from './index.js';
2
+ declare const ChatMessageList: import("svelte").Component<ChatMessageListProps, {
3
+ scrollToBottom: (behavior?: ScrollBehavior) => void;
4
+ }, "">;
5
+ type ChatMessageList = ReturnType<typeof ChatMessageList>;
6
+ export default ChatMessageList;
@@ -0,0 +1,23 @@
1
+ import { type SlotNames, type VariantProps } from '../../../utils/variants.js';
2
+ export declare const chatMessageListVariants: ((props?: {} | undefined) => {
3
+ root: (props?: ({} & {
4
+ 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;
5
+ }) | undefined) => string;
6
+ viewport: (props?: ({} & {
7
+ 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;
8
+ }) | undefined) => string;
9
+ content: (props?: ({} & {
10
+ 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;
11
+ }) | undefined) => string;
12
+ empty: (props?: ({} & {
13
+ 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;
14
+ }) | undefined) => string;
15
+ newButton: (props?: ({} & {
16
+ 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;
17
+ }) | undefined) => string;
18
+ }) & {
19
+ readonly config: import("../../../utils/variants.js").TVConfig;
20
+ };
21
+ export type ChatMessageListVariants = VariantProps<typeof chatMessageListVariants>;
22
+ /** Slot names derived from the tv() config — single source of truth for slotClasses. */
23
+ export type ChatMessageListSlots = SlotNames<typeof chatMessageListVariants>;
@@ -0,0 +1,32 @@
1
+ import { tv } from '../../../utils/variants.js';
2
+ export const chatMessageListVariants = tv({
3
+ slots: {
4
+ // Positioning context for the floating new-messages button; fills the
5
+ // parent (Chat's body region) and delegates all scrolling to the viewport.
6
+ root: ['relative flex h-full min-h-0 flex-col'],
7
+ // The one scrolling element. Browser scroll anchoring is disabled — the
8
+ // engine does its own deterministic prepend anchoring (Safari has no
9
+ // overflow-anchor support, so we correct manually everywhere).
10
+ viewport: [
11
+ 'min-h-0 flex-1 overflow-y-auto overscroll-contain',
12
+ '[overflow-anchor:none]',
13
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset'
14
+ ],
15
+ // The log element whose children are the messages.
16
+ content: ['flex flex-col gap-4 p-4'],
17
+ empty: ['flex h-full items-center justify-center p-6'],
18
+ // Floating pill centered over the bottom edge of the viewport.
19
+ newButton: [
20
+ 'absolute bottom-3 left-1/2 -translate-x-1/2',
21
+ 'inline-flex items-center gap-1.5',
22
+ 'rounded-full border border-border-default bg-surface-overlay',
23
+ 'px-3 py-1.5 text-sm text-text-primary',
24
+ 'shadow-[var(--blocks-shadow-md)]',
25
+ 'cursor-pointer select-none',
26
+ 'transition-[background-color,color] duration-[var(--blocks-duration-fast)] ease-out',
27
+ 'hover:bg-surface-hover',
28
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2'
29
+ ]
30
+ },
31
+ variants: {}
32
+ });
@@ -0,0 +1,45 @@
1
+ /** Scroll metrics of the viewport at one point in time. */
2
+ export interface ScrollSnapshot {
3
+ scrollTop: number;
4
+ scrollHeight: number;
5
+ clientHeight: number;
6
+ }
7
+ /**
8
+ * Distance from the bottom edge within which the user counts as "at the
9
+ * bottom": entering it re-sticks, and tiny upward wiggles inside it (trackpad
10
+ * inertia, macOS bounce) never unstick.
11
+ */
12
+ export declare const RESTICK_PX = 24;
13
+ export declare function distanceFromBottom(snapshot: ScrollSnapshot): number;
14
+ /**
15
+ * Classify one scroll event. `lastScrollTop` is the previous event's position.
16
+ * Order matters: the bottom zone wins over direction, so bounce-back at the
17
+ * bottom edge stays stuck.
18
+ */
19
+ export declare function resolveScrollIntent(snapshot: ScrollSnapshot, lastScrollTop: number): 'stick' | 'unstick' | 'none';
20
+ /**
21
+ * How the message list changed between two renders, judged purely from ids —
22
+ * content growth inside an existing message (streaming) is deliberately NOT a
23
+ * transition (the follow behaviour reacts to it via ResizeObserver instead).
24
+ *
25
+ * - `append` — new messages at the end (feeds the new-messages counter).
26
+ * - `prepend` — older messages loaded at the top (needs the scroll anchor fix).
27
+ * - `truncate` — trailing messages removed (stable head, shorter list): NOT an
28
+ * arrival, so it must never bump the counter (review finding, P2 wave).
29
+ * - `replace` — both ends changed or the list was reset (jump to bottom).
30
+ * - `initial` — first non-empty render (open at the bottom).
31
+ * Simultaneous append+prepend degrades to `replace` — rare (history load
32
+ * during streaming) and safe: worst case is one jump to the bottom.
33
+ */
34
+ export type ListTransition = 'none' | 'initial' | 'append' | 'prepend' | 'truncate' | 'replace';
35
+ export interface ListIdSnapshot {
36
+ firstId: string | undefined;
37
+ lastId: string | undefined;
38
+ length: number;
39
+ }
40
+ export declare function classifyTransition(prev: ListIdSnapshot, next: ListIdSnapshot): ListTransition;
41
+ /**
42
+ * New messages added by an `append` transition. A same-length append (the tail
43
+ * message was swapped for a new id) still counts as one new message.
44
+ */
45
+ export declare function appendedCount(prevLength: number, nextLength: number): number;
@@ -0,0 +1,52 @@
1
+ // Pure decision logic for the ChatMessageList scroll engine — kept free of
2
+ // DOM/Svelte so the invariants are node-testable.
3
+ //
4
+ // Core idea: programmatic follow-scrolling only ever moves DOWN, so any UPWARD
5
+ // scroll movement is user intent and breaks the stick. Re-sticking happens by
6
+ // proximity: whoever scrolls back into the bottom zone follows again. This
7
+ // needs no "suppress programmatic scroll" flag and no event-source sniffing.
8
+ /**
9
+ * Distance from the bottom edge within which the user counts as "at the
10
+ * bottom": entering it re-sticks, and tiny upward wiggles inside it (trackpad
11
+ * inertia, macOS bounce) never unstick.
12
+ */
13
+ export const RESTICK_PX = 24;
14
+ /** Minimum upward movement (px) that counts as deliberate user scrolling. */
15
+ const UPWARD_INTENT_PX = 1;
16
+ export function distanceFromBottom(snapshot) {
17
+ return Math.max(0, snapshot.scrollHeight - snapshot.scrollTop - snapshot.clientHeight);
18
+ }
19
+ /**
20
+ * Classify one scroll event. `lastScrollTop` is the previous event's position.
21
+ * Order matters: the bottom zone wins over direction, so bounce-back at the
22
+ * bottom edge stays stuck.
23
+ */
24
+ export function resolveScrollIntent(snapshot, lastScrollTop) {
25
+ if (distanceFromBottom(snapshot) <= RESTICK_PX)
26
+ return 'stick';
27
+ if (snapshot.scrollTop < lastScrollTop - UPWARD_INTENT_PX)
28
+ return 'unstick';
29
+ return 'none';
30
+ }
31
+ export function classifyTransition(prev, next) {
32
+ if (next.length === 0)
33
+ return prev.length === 0 ? 'none' : 'replace';
34
+ if (prev.length === 0)
35
+ return 'initial';
36
+ const firstStable = prev.firstId === next.firstId;
37
+ const lastStable = prev.lastId === next.lastId;
38
+ if (firstStable && lastStable && next.length === prev.length)
39
+ return 'none';
40
+ if (firstStable)
41
+ return next.length < prev.length ? 'truncate' : 'append';
42
+ if (lastStable)
43
+ return 'prepend';
44
+ return 'replace';
45
+ }
46
+ /**
47
+ * New messages added by an `append` transition. A same-length append (the tail
48
+ * message was swapped for a new id) still counts as one new message.
49
+ */
50
+ export function appendedCount(prevLength, nextLength) {
51
+ return Math.max(1, nextLength - prevLength);
52
+ }
@@ -0,0 +1,93 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { ChatMessageProps } from '../ChatMessage/index.js';
4
+ import type { ChatMessageData } from '../chat.types.js';
5
+ import type { MarkdownUrlPolicy } from '../markdown/types.js';
6
+ import type { ChatMessageListSlots } from './chat-message-list.variants.js';
7
+ /** Render context handed to the per-message override snippet. */
8
+ export interface ChatMessageListItemContext {
9
+ message: ChatMessageData;
10
+ index: number;
11
+ isLast: boolean;
12
+ }
13
+ /**
14
+ * @description Scrollable conversation log with a stick-to-bottom engine: follows
15
+ * streaming content while the reader is at the bottom, breaks off on upward scroll,
16
+ * and offers a floating jump-back button with a new-message counter. Anchors the
17
+ * scroll position when older history is prepended. Announces generation start and
18
+ * the completed answer once to screen readers instead of every token. Renders
19
+ * ChatMessage per entry by default; override per message via the `message` snippet.
20
+ *
21
+ * @tag ai
22
+ * @tag display
23
+ * @related ChatMessage
24
+ * @related Chat
25
+ * @related PromptInput
26
+ * @stability experimental
27
+ *
28
+ * @example
29
+ * ```svelte
30
+ * <ChatMessageList
31
+ * {messages}
32
+ * onRegenerate={(m) => regenerate(m.id)}
33
+ * onStickChange={(stuck) => (following = stuck)}
34
+ * />
35
+ * ```
36
+ */
37
+ export interface ChatMessageListProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
38
+ /**
39
+ * The conversation, oldest first. The component never mutates it. Note:
40
+ * rest attributes (incl. a raw `onscroll`) land on the non-scrolling root —
41
+ * observe follow-state via `onStickChange` instead.
42
+ */
43
+ messages: ChatMessageData[];
44
+ /** Per-message renderer override. Default: `<ChatMessage>` wired with the props below. */
45
+ message?: Snippet<[ChatMessageListItemContext]>;
46
+ /** Empty-state content. Default: an EmptyState with `emptyTitle`/`emptyDescription`. */
47
+ empty?: Snippet;
48
+ /** URL policy handed to every default ChatMessage (links, citations, attachments). */
49
+ urlPolicy?: MarkdownUrlPolicy;
50
+ /** Part-renderer overrides handed to every default ChatMessage. */
51
+ partRenderers?: ChatMessageProps['partRenderers'];
52
+ /** Message layout handed to every default ChatMessage. */
53
+ layout?: ChatMessageProps['layout'];
54
+ /** Message density handed to every default ChatMessage. */
55
+ density?: ChatMessageProps['density'];
56
+ /** Regenerate handler — wired only to the last message when it is an assistant message. */
57
+ onRegenerate?: (message: ChatMessageData) => void;
58
+ /** Retry handler for messages in `error`/`aborted` status. */
59
+ onRetry?: (message: ChatMessageData) => void;
60
+ /** Fires when the list starts (`true`) or stops (`false`) following new content. */
61
+ onStickChange?: (stuck: boolean) => void;
62
+ /** Accessible name of the scrollable conversation region. */
63
+ listLabel?: string;
64
+ /** Accessible label of the jump button when there are no new messages. */
65
+ scrollToBottomLabel?: string;
66
+ /** Label suffix of the jump button while new messages are pending (prefixed with the count). */
67
+ newMessagesLabel?: string;
68
+ /** Screen-reader announcement when an assistant message starts streaming. */
69
+ generatingLabel?: string;
70
+ /** Screen-reader announcement when a stream ends in `error` without text. */
71
+ errorLabel?: string;
72
+ /** Screen-reader announcement when a stream ends in `aborted` without text. */
73
+ abortedLabel?: string;
74
+ /** Empty-state heading. */
75
+ emptyTitle?: string;
76
+ /** Empty-state description. */
77
+ emptyDescription?: string;
78
+ /** Extra classes merged onto the root element. */
79
+ class?: string;
80
+ /** Remove all default tv classes. */
81
+ unstyled?: boolean;
82
+ /** Per-slot class overrides. Slots: root | viewport | content | empty | newButton */
83
+ slotClasses?: Partial<Record<ChatMessageListSlots, string>>;
84
+ /**
85
+ * Apply a named preset registered via `<BlocksProvider presets={{ ChatMessageList: {...} }}>`.
86
+ * Prefer this over `class` overrides when the requested look falls outside the
87
+ * semantic intent palette.
88
+ */
89
+ preset?: string;
90
+ }
91
+ export { default as ChatMessageList } from './ChatMessageList.svelte';
92
+ export { type ChatMessageListVariants, chatMessageListVariants } from './chat-message-list.variants.js';
93
+ export { appendedCount, classifyTransition, distanceFromBottom, type ListIdSnapshot, type ListTransition, resolveScrollIntent, type ScrollSnapshot } from './chat-scroll.js';
@@ -0,0 +1,3 @@
1
+ export { default as ChatMessageList } from './ChatMessageList.svelte';
2
+ export { chatMessageListVariants } from './chat-message-list.variants.js';
3
+ export { appendedCount, classifyTransition, distanceFromBottom, resolveScrollIntent } from './chat-scroll.js';