@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,432 @@
1
+ <script lang="ts">
2
+ import { onDestroy, tick } from 'svelte';
3
+ import { useBlocksI18n } from '../../..';
4
+ import { getBlocksConfig, resolveSlotClasses } from '../../../provider';
5
+ import { resolveIcon } from '../../../icons';
6
+ import SendIconDefault from '../../../icons/SendIcon.svelte';
7
+ import SquareIconDefault from '../../../icons/SquareIcon.svelte';
8
+ import PaperclipIconDefault from '../../../icons/PaperclipIcon.svelte';
9
+ import CloseIconDefault from '../../../icons/CloseIcon.svelte';
10
+ import FileIconDefault from '../../../icons/FileIcon.svelte';
11
+ import {
12
+ partitionIntake,
13
+ revokeIntakePreviews,
14
+ formatFileSize,
15
+ type FileIntakeEntry,
16
+ type FileIntakeConstraints,
17
+ type FileIntakeMessages
18
+ } from '../../../utils/file-intake';
19
+ import { promptInputVariants, type PromptInputVariants } from './prompt-input.variants';
20
+ import type { PromptInputProps } from './index';
21
+
22
+ let {
23
+ value = $bindable(''),
24
+ attachments = $bindable<FileIntakeEntry[]>([]),
25
+ onValueChange,
26
+ onSubmit,
27
+ onStop,
28
+ onAttachmentReject,
29
+ submitOn = 'enter',
30
+ clearOnSubmit = true,
31
+ placeholder,
32
+ disabled = false,
33
+ busy = false,
34
+ autofocus = false,
35
+ minRows = 1,
36
+ maxRows = 8,
37
+ label = 'Message',
38
+ sendLabel = 'Send',
39
+ stopLabel = 'Stop',
40
+ attachLabel = 'Attach file',
41
+ removeAttachmentLabel = (name: string) => `Remove ${name}`,
42
+ allowAttachments = false,
43
+ preventDocumentDrop = true,
44
+ accept,
45
+ maxFiles,
46
+ maxFileSize,
47
+ validate,
48
+ size = 'md',
49
+ leading,
50
+ trailing,
51
+ hint,
52
+ class: className,
53
+ unstyled: unstyledProp = false,
54
+ slotClasses: slotClassesProp = {},
55
+ preset,
56
+ ...restProps
57
+ }: PromptInputProps = $props();
58
+
59
+ const bt = useBlocksI18n();
60
+
61
+ const blocksConfig = getBlocksConfig();
62
+ const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
63
+
64
+ const SendIcon = resolveIcon('send', SendIconDefault);
65
+ const SquareIcon = resolveIcon('square', SquareIconDefault);
66
+ const PaperclipIcon = resolveIcon('paperclip', PaperclipIconDefault);
67
+ const CloseIcon = resolveIcon('close', CloseIconDefault);
68
+ const FileIcon = resolveIcon('file', FileIconDefault);
69
+
70
+ const variantProps: PromptInputVariants = $derived({ size });
71
+ const styles = $derived(promptInputVariants(variantProps));
72
+ const slotClasses = $derived(
73
+ resolveSlotClasses(blocksConfig, 'PromptInput', preset, variantProps, slotClassesProp)
74
+ );
75
+
76
+ function cls(name: keyof typeof slotClasses, extra?: string | (string | undefined)[]) {
77
+ const extras = Array.isArray(extra) ? extra : [extra];
78
+ if (unstyled) {
79
+ return [slotClasses?.[name], ...extras].filter(Boolean).join(' ');
80
+ }
81
+ const slotFns = styles as Record<string, (args: { class?: unknown }) => string>;
82
+ return slotFns[name]({ class: [slotClasses?.[name], ...extras] });
83
+ }
84
+
85
+ // ── IDs & a11y ───────────────────────────────────────────────────────────────
86
+ const propsId = $props.id();
87
+ const fieldId = `prompt-input-${propsId}`;
88
+ const errorId = `${fieldId}-error`;
89
+
90
+ // aria-keyshortcuts reflects the active submit binding so AT users know the
91
+ // gesture (Enter vs Cmd/Ctrl+Enter) that sends the message.
92
+ const keyshortcuts = $derived(submitOn === 'enter' ? 'Enter' : 'Meta+Enter Control+Enter');
93
+
94
+ // ── Auto-resize (measured pattern, mirrors Textarea.svelte) ────────────────────
95
+ let textareaRef = $state<HTMLTextAreaElement>();
96
+ const lineHeight = $derived(size === 'sm' ? 20 : 24);
97
+
98
+ function adjustHeight() {
99
+ if (!textareaRef) return;
100
+ textareaRef.style.height = 'auto';
101
+ const minHeight = minRows * lineHeight;
102
+ const maxHeight = maxRows * lineHeight;
103
+ const scrollHeight = textareaRef.scrollHeight;
104
+ textareaRef.style.height = `${Math.min(Math.max(scrollHeight, minHeight), maxHeight)}px`;
105
+ textareaRef.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden';
106
+ }
107
+
108
+ // Re-measure on every value change (typing and programmatic clear alike).
109
+ $effect(() => {
110
+ void value;
111
+ adjustHeight();
112
+ });
113
+
114
+ // Focus once when requested — a bare native `autofocus` attribute is
115
+ // unreliable for a chromeless textarea mounted inside a bordered surface.
116
+ $effect(() => {
117
+ if (autofocus && textareaRef) textareaRef.focus();
118
+ });
119
+
120
+ // ── Value ──────────────────────────────────────────────────────────────────
121
+ function handleInput(event: Event & { currentTarget: HTMLTextAreaElement }) {
122
+ value = event.currentTarget.value;
123
+ onValueChange?.(value);
124
+ adjustHeight();
125
+ }
126
+
127
+ const canSubmit = $derived((value.trim().length > 0 || attachments.length > 0) && !disabled);
128
+
129
+ function submit() {
130
+ if (disabled || busy) return;
131
+ const text = value.trim();
132
+ if (!text && attachments.length === 0) return;
133
+ // Hand out a shallow copy so a consumer that keeps the array by reference
134
+ // (e.g. clearOnSubmit=false) is not aliased to our still-mutating state.
135
+ onSubmit({ text, attachments: [...attachments] });
136
+ // Clear any stale inline rejection on EVERY successful submit, regardless
137
+ // of clearOnSubmit — the error described the previous intake, not this send.
138
+ error = '';
139
+ if (clearOnSubmit) {
140
+ value = '';
141
+ onValueChange?.('');
142
+ // Ownership of the accepted entries transfers to the consumer — the
143
+ // submitted files (and their preview object-URLs) live on in the
144
+ // consumer's message list, so we clear the reference WITHOUT revoking.
145
+ attachments = [];
146
+ }
147
+ }
148
+
149
+ function handleKeydown(event: KeyboardEvent) {
150
+ if (event.key !== 'Enter') return;
151
+ // IME guard — never submit while a composition is in progress.
152
+ if (event.isComposing || event.keyCode === 229) return;
153
+
154
+ const withMod = event.metaKey || event.ctrlKey;
155
+ if (submitOn === 'enter') {
156
+ if (event.shiftKey) return; // Shift+Enter inserts a newline.
157
+ // While busy, DON'T preventDefault — let Enter insert a newline so the
158
+ // user can keep composing during a streaming response (mod-enter parity).
159
+ // Swallowing the keystroke here would drop it entirely.
160
+ if (busy) return;
161
+ // Empty-idle Enter still preventDefaults: submit() no-ops, and we
162
+ // deliberately suppress the leading newline (chat convention).
163
+ event.preventDefault();
164
+ submit();
165
+ } else {
166
+ if (!withMod) return; // Plain Enter inserts a newline in mod-enter mode.
167
+ event.preventDefault();
168
+ submit();
169
+ }
170
+ }
171
+
172
+ // ── Attachments ──────────────────────────────────────────────────────────────
173
+ let error = $state('');
174
+ let dragging = $state(false);
175
+ let fileInputEl = $state<HTMLInputElement>();
176
+ let rootEl = $state<HTMLDivElement>();
177
+
178
+ const acceptString = $derived(Array.isArray(accept) ? accept.join(',') : (accept ?? ''));
179
+
180
+ // Error texts reuse FileUpload's i18n keys exactly — no new keys for a second
181
+ // intake surface. `bt` is reactive, so building this per-intake keeps locale
182
+ // changes correct.
183
+ const messages: FileIntakeMessages = {
184
+ invalidType: (type) => bt('fileUpload.invalidType', { type }),
185
+ tooLarge: (formattedSize) => bt('fileUpload.tooLarge', { size: formattedSize }),
186
+ tooSmall: (formattedSize) => bt('fileUpload.tooSmall', { size: formattedSize }),
187
+ exists: () => bt('fileUpload.exists'),
188
+ tooMany: (count) => bt('fileUpload.tooMany', { count: String(count) })
189
+ };
190
+
191
+ function addFiles(incoming: File[]) {
192
+ if (!allowAttachments || disabled || incoming.length === 0) return;
193
+ const constraints: FileIntakeConstraints = { accept, maxFiles, maxFileSize, validate };
194
+ const { accepted, rejected } = partitionIntake(
195
+ incoming,
196
+ attachments,
197
+ constraints,
198
+ messages,
199
+ 'attachment'
200
+ );
201
+
202
+ if (accepted.length > 0) {
203
+ attachments = [...attachments, ...accepted];
204
+ error = ''; // A successful add clears the previous rejection line.
205
+ }
206
+ if (rejected.length > 0) {
207
+ error = rejected[0].errors[0]?.message ?? '';
208
+ onAttachmentReject?.(rejected);
209
+ }
210
+ }
211
+
212
+ async function removeAttachment(entry: FileIntakeEntry, index: number) {
213
+ // Explicit removal — the entry never reached the consumer, so we own and
214
+ // revoke its preview object-URL here.
215
+ revokeIntakePreviews([entry]);
216
+ attachments = attachments.filter((a) => a.id !== entry.id);
217
+ // Move focus deterministically after the chip leaves the DOM so keyboard
218
+ // users don't get dropped to <body>: the chip now at this index (the one
219
+ // that was next), else the last remaining chip (the previous one), else the
220
+ // textarea when the strip is empty.
221
+ await tick();
222
+ const buttons = rootEl?.querySelectorAll<HTMLButtonElement>('[data-attachment-remove]');
223
+ if (buttons && buttons.length > 0) {
224
+ buttons[Math.min(index, buttons.length - 1)]?.focus();
225
+ } else {
226
+ textareaRef?.focus();
227
+ }
228
+ }
229
+
230
+ function openFilePicker() {
231
+ if (!disabled) fileInputEl?.click();
232
+ }
233
+
234
+ function handleFileInputChange(event: Event & { currentTarget: HTMLInputElement }) {
235
+ const selected = Array.from(event.currentTarget.files ?? []);
236
+ if (selected.length > 0) addFiles(selected);
237
+ event.currentTarget.value = '';
238
+ }
239
+
240
+ function handlePaste(event: ClipboardEvent) {
241
+ if (!allowAttachments || disabled) return;
242
+ const pasted = Array.from(event.clipboardData?.files ?? []);
243
+ if (pasted.length > 0) {
244
+ event.preventDefault();
245
+ addFiles(pasted);
246
+ }
247
+ }
248
+
249
+ function handleDragOver(event: DragEvent) {
250
+ if (!allowAttachments || disabled) return;
251
+ event.preventDefault();
252
+ dragging = true;
253
+ }
254
+
255
+ function handleDragLeave(event: DragEvent) {
256
+ // Only clear when leaving the composer itself, not when moving between
257
+ // its children (which fire dragleave against the child).
258
+ if (event.currentTarget === event.target) dragging = false;
259
+ }
260
+
261
+ function handleDrop(event: DragEvent) {
262
+ if (!allowAttachments || disabled) return;
263
+ dragging = false;
264
+ const dropped = Array.from(event.dataTransfer?.files ?? []);
265
+ // Only claim the drop when it carries files. A text-selection drop must
266
+ // fall through to the native textarea (inserts the dragged text), so we
267
+ // never preventDefault for it.
268
+ if (dropped.length === 0) return;
269
+ event.preventDefault();
270
+ addFiles(dropped);
271
+ }
272
+
273
+ // Prevent a file misdropped OUTSIDE the composer from navigating the page
274
+ // away (browser default for a dropped file). Effective only while the
275
+ // attachment surface is enabled. Mirrors FileUpload's document guard.
276
+ $effect(() => {
277
+ if (!allowAttachments || !preventDocumentDrop) return;
278
+ function preventDrop(event: DragEvent) {
279
+ // Only guard drops OUTSIDE the composer. Inside, the composer's own
280
+ // handlers act (file drops), and a text-selection drop must reach the
281
+ // native textarea — so we must not preventDefault the bubbled event here.
282
+ if (rootEl && event.target instanceof Node && rootEl.contains(event.target)) return;
283
+ event.preventDefault();
284
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'none';
285
+ }
286
+ document.addEventListener('dragover', preventDrop);
287
+ document.addEventListener('drop', preventDrop);
288
+ return () => {
289
+ document.removeEventListener('dragover', preventDrop);
290
+ document.removeEventListener('drop', preventDrop);
291
+ };
292
+ });
293
+
294
+ // Reset the drag highlight if a drag ends anywhere (dropped outside, or the
295
+ // pointer left the window) — otherwise the ring stays stuck after such a drag.
296
+ $effect(() => {
297
+ if (!allowAttachments) return;
298
+ function clearDrag() {
299
+ dragging = false;
300
+ }
301
+ window.addEventListener('dragend', clearDrag);
302
+ return () => window.removeEventListener('dragend', clearDrag);
303
+ });
304
+
305
+ // Non-submitted attachments own their previews — revoke on teardown. Submitted
306
+ // entries were already cleared from `attachments` (without revoking), so this
307
+ // only frees what the user left behind.
308
+ onDestroy(() => {
309
+ revokeIntakePreviews(attachments);
310
+ });
311
+
312
+ const iconSize = $derived(size === 'sm' ? 16 : 18);
313
+ </script>
314
+
315
+ <div
316
+ bind:this={rootEl}
317
+ {...restProps}
318
+ class={cls('root', className)}
319
+ data-dragging={allowAttachments && dragging ? 'true' : undefined}
320
+ ondragover={allowAttachments ? handleDragOver : undefined}
321
+ ondragleave={allowAttachments ? handleDragLeave : undefined}
322
+ ondrop={allowAttachments ? handleDrop : undefined}
323
+ >
324
+ {#if allowAttachments}
325
+ <input
326
+ bind:this={fileInputEl}
327
+ type="file"
328
+ accept={acceptString || undefined}
329
+ multiple={maxFiles !== 1}
330
+ {disabled}
331
+ class="sr-only"
332
+ tabindex={-1}
333
+ aria-hidden="true"
334
+ onchange={handleFileInputChange}
335
+ />
336
+ {/if}
337
+
338
+ {#if allowAttachments && attachments.length > 0}
339
+ <ul class={cls('attachmentsStrip')} aria-label={attachLabel}>
340
+ {#each attachments as entry, i (entry.id)}
341
+ <li class={cls('attachmentChip')}>
342
+ <!-- Decorative: the adjacent attachmentName span carries the file
343
+ name — a filled alt would read it twice (review finding). -->
344
+ <span class={cls('attachmentThumb')} aria-hidden="true">
345
+ {#if entry.preview}
346
+ <img src={entry.preview} alt="" />
347
+ {:else}
348
+ <FileIcon size={16} />
349
+ {/if}
350
+ </span>
351
+ <span class={cls('attachmentName')} title={entry.file.name}>{entry.file.name}</span>
352
+ <span class={cls('attachmentSize')}>{formatFileSize(entry.file.size)}</span>
353
+ <button
354
+ type="button"
355
+ class={cls('attachmentRemove')}
356
+ data-attachment-remove
357
+ aria-label={removeAttachmentLabel(entry.file.name)}
358
+ onclick={() => removeAttachment(entry, i)}
359
+ >
360
+ <CloseIcon size={14} />
361
+ </button>
362
+ </li>
363
+ {/each}
364
+ </ul>
365
+ {/if}
366
+
367
+ <textarea
368
+ bind:this={textareaRef}
369
+ id={fieldId}
370
+ class={cls('textarea')}
371
+ {value}
372
+ {placeholder}
373
+ {disabled}
374
+ rows={minRows}
375
+ aria-label={label}
376
+ aria-keyshortcuts={keyshortcuts}
377
+ aria-describedby={error ? errorId : undefined}
378
+ oninput={handleInput}
379
+ onkeydown={handleKeydown}
380
+ onpaste={handlePaste}></textarea>
381
+
382
+ <div class={cls('actions')}>
383
+ <div class={cls('leading')}>
384
+ {#if allowAttachments}
385
+ <button
386
+ type="button"
387
+ class={cls('attachButton')}
388
+ aria-label={attachLabel}
389
+ {disabled}
390
+ onclick={openFilePicker}
391
+ >
392
+ <PaperclipIcon size={iconSize} />
393
+ </button>
394
+ {/if}
395
+ {@render leading?.()}
396
+ </div>
397
+
398
+ <div class={cls('trailing')}>
399
+ {@render trailing?.()}
400
+ {#if busy}
401
+ <button
402
+ type="button"
403
+ class={cls('stopButton')}
404
+ aria-label={stopLabel}
405
+ onclick={() => onStop?.()}
406
+ >
407
+ <SquareIcon size={iconSize} />
408
+ </button>
409
+ {:else}
410
+ <button
411
+ type="button"
412
+ class={cls('sendButton')}
413
+ aria-label={sendLabel}
414
+ disabled={!canSubmit}
415
+ onclick={submit}
416
+ >
417
+ <SendIcon size={iconSize} />
418
+ </button>
419
+ {/if}
420
+ </div>
421
+ </div>
422
+
423
+ <!-- Live rejection line — the region is PERMANENTLY in the DOM (present
424
+ before its text, so AT reliably announces the change; see ChatMessage's
425
+ copy-status span). Empty state collapses to sr-only: still in the
426
+ accessibility tree, but reserving no layout. -->
427
+ <div id={errorId} class={error ? cls('error') : 'sr-only'} role="status">{error}</div>
428
+
429
+ {#if hint}
430
+ <div class={cls('hint')}>{@render hint()}</div>
431
+ {/if}
432
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { PromptInputProps } from './index.js';
2
+ declare const PromptInput: import("svelte").Component<PromptInputProps, {}, "value" | "attachments">;
3
+ type PromptInput = ReturnType<typeof PromptInput>;
4
+ export default PromptInput;
@@ -0,0 +1,165 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { FileIntakeEntry, FileIntakeRejection } from '../../../utils/file-intake.js';
4
+ import type { PromptInputSlots, PromptInputVariants } from './prompt-input.variants.js';
5
+ /**
6
+ * @description The chat composer: an auto-growing textarea in a single bordered
7
+ * surface with a send button that flips to a stop button while a response is
8
+ * streaming. `onSubmit({ text, attachments })` fires with trimmed text when there
9
+ * is content (or at least one attachment); `onStop` fires from the stop button.
10
+ * Attachment intake (paperclip picker, clipboard paste, drag-and-drop, chip strip)
11
+ * reuses the shared `file-intake` core — the same accept/size/count validation and
12
+ * preview-URL lifecycle as FileUpload — and is opt-in via `allowAttachments`.
13
+ *
14
+ * @tag ai
15
+ * @tag form
16
+ * @related Textarea
17
+ * @related FileUpload
18
+ * @related Chat
19
+ * @stability experimental
20
+ *
21
+ * @example
22
+ * ```svelte
23
+ * <PromptInput
24
+ * bind:value={draft}
25
+ * busy={isStreaming}
26
+ * onSubmit={({ text }) => send(text)}
27
+ * onStop={abort}
28
+ * />
29
+ * ```
30
+ *
31
+ * @example With attachments and Cmd/Ctrl+Enter to send
32
+ * ```svelte
33
+ * <PromptInput
34
+ * bind:value={draft}
35
+ * bind:attachments={files}
36
+ * allowAttachments
37
+ * accept="image/*"
38
+ * maxFiles={4}
39
+ * submitOn="mod-enter"
40
+ * onSubmit={({ text, attachments }) => send(text, attachments)}
41
+ * />
42
+ * ```
43
+ */
44
+ export interface PromptInputProps extends PromptInputVariants, Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'onsubmit'> {
45
+ /** The composer's text (bindable). */
46
+ value?: string;
47
+ /** Called whenever the text changes (including the clear after submit). */
48
+ onValueChange?: (value: string) => void;
49
+ /**
50
+ * The accepted attachments (bindable). Populated by the picker / paste / drop
51
+ * when `allowAttachments` is set. On submit these are handed to `onSubmit` and
52
+ * — with `clearOnSubmit` — cleared from here **without** revoking their preview
53
+ * object-URLs, because ownership transfers to the consumer's message list.
54
+ *
55
+ * Removing chips through the built-in UI revokes their preview URLs for you.
56
+ * Clearing or splicing this array **externally** (e.g. reassigning
57
+ * `bind:attachments`) bypasses that cleanup — such mutations must revoke the
58
+ * dropped entries' previews themselves via `revokeIntakePreviews` (exported
59
+ * from `$lib/utils/file-intake`) to avoid leaking object-URLs.
60
+ */
61
+ attachments?: FileIntakeEntry[];
62
+ /**
63
+ * Fired on send with the trimmed text and the current attachments. Only fires
64
+ * when there is text or at least one attachment, and never while `busy` or
65
+ * `disabled`.
66
+ */
67
+ onSubmit: (payload: {
68
+ text: string;
69
+ attachments: FileIntakeEntry[];
70
+ }) => void;
71
+ /** Fired by the stop button (shown in place of send while `busy`). */
72
+ onStop?: () => void;
73
+ /**
74
+ * Called with the rejected files when an add is refused (bad type, too large,
75
+ * over `maxFiles`, duplicate, or custom `validate`). The first rejection's
76
+ * message also surfaces inline; it clears on the next successful add.
77
+ */
78
+ onAttachmentReject?: (rejections: FileIntakeRejection[]) => void;
79
+ /**
80
+ * Which key gesture sends the message.
81
+ * - `enter` (default) — Enter sends, Shift+Enter inserts a newline.
82
+ * - `mod-enter` — Cmd/Ctrl+Enter sends, Enter inserts a newline.
83
+ *
84
+ * Submission is always suppressed mid-IME-composition.
85
+ * @default 'enter'
86
+ */
87
+ submitOn?: 'enter' | 'mod-enter';
88
+ /**
89
+ * Clear the text and attachments after a successful submit. Preview URLs of
90
+ * the submitted attachments are intentionally **not** revoked — see
91
+ * `attachments`.
92
+ * @default true
93
+ */
94
+ clearOnSubmit?: boolean;
95
+ /** Placeholder text for the textarea. */
96
+ placeholder?: string;
97
+ /** Disables the whole composer (textarea + buttons). @default false */
98
+ disabled?: boolean;
99
+ /**
100
+ * A response is in flight: the send button is replaced by a stop button and
101
+ * Enter no longer submits.
102
+ * @default false
103
+ */
104
+ busy?: boolean;
105
+ /** Focus the textarea on mount. @default false */
106
+ autofocus?: boolean;
107
+ /** Minimum visible rows of the auto-growing textarea. @default 1 */
108
+ minRows?: number;
109
+ /** Maximum visible rows before the textarea scrolls internally. @default 8 */
110
+ maxRows?: number;
111
+ /** Accessible name for the textarea (rendered as `aria-label`). @default 'Message' */
112
+ label?: string;
113
+ /** Accessible label for the send button. @default 'Send' */
114
+ sendLabel?: string;
115
+ /** Accessible label for the stop button. @default 'Stop' */
116
+ stopLabel?: string;
117
+ /** Accessible label for the attach button. @default 'Attach file' */
118
+ attachLabel?: string;
119
+ /** Accessible label factory for a chip's remove button. @default (name) => `Remove ${name}` */
120
+ removeAttachmentLabel?: (name: string) => string;
121
+ /**
122
+ * Enable the attachment surface: paperclip picker, clipboard-image paste,
123
+ * drag-and-drop, and the chip strip above the textarea.
124
+ * @default false
125
+ */
126
+ allowAttachments?: boolean;
127
+ /**
128
+ * Prevent browser navigation when a file is dropped outside the composer.
129
+ * Only takes effect while `allowAttachments` is set. @default true
130
+ */
131
+ preventDocumentDrop?: boolean;
132
+ /** Accepted MIME types / extensions (e.g. `'image/*'`, `['.pdf', 'image/png']`). */
133
+ accept?: string | string[];
134
+ /** Maximum number of attachments across the whole list. */
135
+ maxFiles?: number;
136
+ /** Maximum attachment size in bytes. */
137
+ maxFileSize?: number;
138
+ /** Custom per-file validation. Return an errors array or `null`. */
139
+ validate?: (file: File) => FileIntakeEntry['errors'] | null;
140
+ /** Content in the leading (left) action zone, after the attach button. */
141
+ leading?: Snippet;
142
+ /** Content in the trailing (right) action zone, before the send/stop button. */
143
+ trailing?: Snippet;
144
+ /** Helper line rendered under the composer (e.g. "Enter to send"). */
145
+ hint?: Snippet;
146
+ /** Extra classes merged onto the root element. */
147
+ class?: string;
148
+ /** Remove all default tv() classes. */
149
+ unstyled?: boolean;
150
+ /**
151
+ * Per-slot class overrides. Slots: root | attachmentsStrip | attachmentChip |
152
+ * attachmentThumb | attachmentName | attachmentSize | attachmentRemove |
153
+ * textarea | actions | leading | trailing | attachButton | sendButton |
154
+ * stopButton | error | hint
155
+ */
156
+ slotClasses?: Partial<Record<PromptInputSlots, string>>;
157
+ /**
158
+ * Apply a named preset registered via `<BlocksProvider presets={{ PromptInput: {...} }}>`.
159
+ * Prefer this over `class` overrides when the requested look falls outside the
160
+ * semantic intent palette.
161
+ */
162
+ preset?: string;
163
+ }
164
+ export { default as PromptInput } from './PromptInput.svelte';
165
+ export { type PromptInputVariants, promptInputVariants } from './prompt-input.variants.js';
@@ -0,0 +1,2 @@
1
+ export { default as PromptInput } from './PromptInput.svelte';
2
+ export { promptInputVariants } from './prompt-input.variants.js';