@happyvertical/smrt-chat 0.30.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 (169) hide show
  1. package/AGENTS.md +35 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +163 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/chunks/ChatService-Dpzc1Pa5.js +2044 -0
  8. package/dist/chunks/ChatService-Dpzc1Pa5.js.map +1 -0
  9. package/dist/collections/AgentSessionCollection.d.ts +57 -0
  10. package/dist/collections/AgentSessionCollection.d.ts.map +1 -0
  11. package/dist/collections/ChatMessageCollection.d.ts +79 -0
  12. package/dist/collections/ChatMessageCollection.d.ts.map +1 -0
  13. package/dist/collections/ChatParticipantCollection.d.ts +26 -0
  14. package/dist/collections/ChatParticipantCollection.d.ts.map +1 -0
  15. package/dist/collections/ChatReactionCollection.d.ts +23 -0
  16. package/dist/collections/ChatReactionCollection.d.ts.map +1 -0
  17. package/dist/collections/ChatRoomCollection.d.ts +43 -0
  18. package/dist/collections/ChatRoomCollection.d.ts.map +1 -0
  19. package/dist/collections/ChatThreadCollection.d.ts +9 -0
  20. package/dist/collections/ChatThreadCollection.d.ts.map +1 -0
  21. package/dist/index.d.ts +5 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +18 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/internal/agent-runtime.d.ts +16 -0
  26. package/dist/internal/agent-runtime.d.ts.map +1 -0
  27. package/dist/internal/agent-runtime.js +5 -0
  28. package/dist/internal/agent-runtime.js.map +1 -0
  29. package/dist/manifest.json +2811 -0
  30. package/dist/models/AgentSession.d.ts +70 -0
  31. package/dist/models/AgentSession.d.ts.map +1 -0
  32. package/dist/models/ChatMessage.d.ts +55 -0
  33. package/dist/models/ChatMessage.d.ts.map +1 -0
  34. package/dist/models/ChatParticipant.d.ts +32 -0
  35. package/dist/models/ChatParticipant.d.ts.map +1 -0
  36. package/dist/models/ChatReaction.d.ts +19 -0
  37. package/dist/models/ChatReaction.d.ts.map +1 -0
  38. package/dist/models/ChatRoom.d.ts +44 -0
  39. package/dist/models/ChatRoom.d.ts.map +1 -0
  40. package/dist/models/ChatThread.d.ts +24 -0
  41. package/dist/models/ChatThread.d.ts.map +1 -0
  42. package/dist/models/index.d.ts +7 -0
  43. package/dist/models/index.d.ts.map +1 -0
  44. package/dist/playground.d.ts +2 -0
  45. package/dist/playground.d.ts.map +1 -0
  46. package/dist/playground.js +166 -0
  47. package/dist/playground.js.map +1 -0
  48. package/dist/services/ChatService.d.ts +390 -0
  49. package/dist/services/ChatService.d.ts.map +1 -0
  50. package/dist/services/index.d.ts +2 -0
  51. package/dist/services/index.d.ts.map +1 -0
  52. package/dist/smrt-knowledge.json +1507 -0
  53. package/dist/svelte/components/agent/AgentChat.svelte +542 -0
  54. package/dist/svelte/components/agent/AgentChat.svelte.d.ts +21 -0
  55. package/dist/svelte/components/agent/AgentChat.svelte.d.ts.map +1 -0
  56. package/dist/svelte/components/agent/AgentSelector.svelte +175 -0
  57. package/dist/svelte/components/agent/AgentSelector.svelte.d.ts +11 -0
  58. package/dist/svelte/components/agent/AgentSelector.svelte.d.ts.map +1 -0
  59. package/dist/svelte/components/agent/AgentSessionPanel.svelte +322 -0
  60. package/dist/svelte/components/agent/AgentSessionPanel.svelte.d.ts +15 -0
  61. package/dist/svelte/components/agent/AgentSessionPanel.svelte.d.ts.map +1 -0
  62. package/dist/svelte/components/agent/ToolCallDisplay.svelte +335 -0
  63. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts +9 -0
  64. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts.map +1 -0
  65. package/dist/svelte/components/agent/message-blocks.d.ts +12 -0
  66. package/dist/svelte/components/agent/message-blocks.d.ts.map +1 -0
  67. package/dist/svelte/components/agent/message-blocks.js +41 -0
  68. package/dist/svelte/components/agent/message-blocks.test.js +31 -0
  69. package/dist/svelte/components/dialogs/RoomCreateDialog.svelte +403 -0
  70. package/dist/svelte/components/dialogs/RoomCreateDialog.svelte.d.ts +16 -0
  71. package/dist/svelte/components/dialogs/RoomCreateDialog.svelte.d.ts.map +1 -0
  72. package/dist/svelte/components/dialogs/SearchMessages.svelte +457 -0
  73. package/dist/svelte/components/dialogs/SearchMessages.svelte.d.ts +17 -0
  74. package/dist/svelte/components/dialogs/SearchMessages.svelte.d.ts.map +1 -0
  75. package/dist/svelte/components/layout/ChatLayout.svelte +150 -0
  76. package/dist/svelte/components/layout/ChatLayout.svelte.d.ts +18 -0
  77. package/dist/svelte/components/layout/ChatLayout.svelte.d.ts.map +1 -0
  78. package/dist/svelte/components/layout/MemberList.svelte +389 -0
  79. package/dist/svelte/components/layout/MemberList.svelte.d.ts +11 -0
  80. package/dist/svelte/components/layout/MemberList.svelte.d.ts.map +1 -0
  81. package/dist/svelte/components/layout/RoomHeader.svelte +241 -0
  82. package/dist/svelte/components/layout/RoomHeader.svelte.d.ts +15 -0
  83. package/dist/svelte/components/layout/RoomHeader.svelte.d.ts.map +1 -0
  84. package/dist/svelte/components/layout/RoomList.svelte +471 -0
  85. package/dist/svelte/components/layout/RoomList.svelte.d.ts +15 -0
  86. package/dist/svelte/components/layout/RoomList.svelte.d.ts.map +1 -0
  87. package/dist/svelte/components/messages/MessageInput.svelte +232 -0
  88. package/dist/svelte/components/messages/MessageInput.svelte.d.ts +20 -0
  89. package/dist/svelte/components/messages/MessageInput.svelte.d.ts.map +1 -0
  90. package/dist/svelte/components/messages/MessageItem.svelte +431 -0
  91. package/dist/svelte/components/messages/MessageItem.svelte.d.ts +19 -0
  92. package/dist/svelte/components/messages/MessageItem.svelte.d.ts.map +1 -0
  93. package/dist/svelte/components/messages/MessageList.svelte +129 -0
  94. package/dist/svelte/components/messages/MessageList.svelte.d.ts +17 -0
  95. package/dist/svelte/components/messages/MessageList.svelte.d.ts.map +1 -0
  96. package/dist/svelte/components/messages/ThreadPanel.svelte +156 -0
  97. package/dist/svelte/components/messages/ThreadPanel.svelte.d.ts +17 -0
  98. package/dist/svelte/components/messages/ThreadPanel.svelte.d.ts.map +1 -0
  99. package/dist/svelte/components/messages/__tests__/MessageInput.test.js +38 -0
  100. package/dist/svelte/components/shared/Avatar.svelte +30 -0
  101. package/dist/svelte/components/shared/Avatar.svelte.d.ts +14 -0
  102. package/dist/svelte/components/shared/Avatar.svelte.d.ts.map +1 -0
  103. package/dist/svelte/components/shared/FileUpload.svelte +382 -0
  104. package/dist/svelte/components/shared/FileUpload.svelte.d.ts +14 -0
  105. package/dist/svelte/components/shared/FileUpload.svelte.d.ts.map +1 -0
  106. package/dist/svelte/components/shared/LinkPreview.svelte +108 -0
  107. package/dist/svelte/components/shared/LinkPreview.svelte.d.ts +18 -0
  108. package/dist/svelte/components/shared/LinkPreview.svelte.d.ts.map +1 -0
  109. package/dist/svelte/components/shared/MentionAutocomplete.svelte +168 -0
  110. package/dist/svelte/components/shared/MentionAutocomplete.svelte.d.ts +18 -0
  111. package/dist/svelte/components/shared/MentionAutocomplete.svelte.d.ts.map +1 -0
  112. package/dist/svelte/components/shared/MessageBubble.svelte +81 -0
  113. package/dist/svelte/components/shared/MessageBubble.svelte.d.ts +16 -0
  114. package/dist/svelte/components/shared/MessageBubble.svelte.d.ts.map +1 -0
  115. package/dist/svelte/components/shared/ReactionPicker.svelte +103 -0
  116. package/dist/svelte/components/shared/ReactionPicker.svelte.d.ts +10 -0
  117. package/dist/svelte/components/shared/ReactionPicker.svelte.d.ts.map +1 -0
  118. package/dist/svelte/components/shared/ReadReceipts.svelte +127 -0
  119. package/dist/svelte/components/shared/ReadReceipts.svelte.d.ts +13 -0
  120. package/dist/svelte/components/shared/ReadReceipts.svelte.d.ts.map +1 -0
  121. package/dist/svelte/components/shared/TypingIndicator.svelte +90 -0
  122. package/dist/svelte/components/shared/TypingIndicator.svelte.d.ts +12 -0
  123. package/dist/svelte/components/shared/TypingIndicator.svelte.d.ts.map +1 -0
  124. package/dist/svelte/components/shared/UserPresence.svelte +65 -0
  125. package/dist/svelte/components/shared/UserPresence.svelte.d.ts +13 -0
  126. package/dist/svelte/components/shared/UserPresence.svelte.d.ts.map +1 -0
  127. package/dist/svelte/components/shared/__tests__/Avatar.test.js +20 -0
  128. package/dist/svelte/components/shared/__tests__/LinkPreview.test.js +29 -0
  129. package/dist/svelte/components/shared/__tests__/MessageBubble.test.js +21 -0
  130. package/dist/svelte/components/shared/__tests__/ReactionPicker.test.js +35 -0
  131. package/dist/svelte/components/shared/__tests__/ReadReceipts.test.js +28 -0
  132. package/dist/svelte/components/shared/__tests__/TypingIndicator.test.js +27 -0
  133. package/dist/svelte/components/shared/__tests__/UserPresence.test.js +23 -0
  134. package/dist/svelte/components/tabs/ChatTab.svelte +240 -0
  135. package/dist/svelte/components/tabs/ChatTab.svelte.d.ts +21 -0
  136. package/dist/svelte/components/tabs/ChatTab.svelte.d.ts.map +1 -0
  137. package/dist/svelte/components/tabs/ChatTabList.svelte +158 -0
  138. package/dist/svelte/components/tabs/ChatTabList.svelte.d.ts +13 -0
  139. package/dist/svelte/components/tabs/ChatTabList.svelte.d.ts.map +1 -0
  140. package/dist/svelte/components/tabs/ChatTabs.svelte +88 -0
  141. package/dist/svelte/components/tabs/ChatTabs.svelte.d.ts +21 -0
  142. package/dist/svelte/components/tabs/ChatTabs.svelte.d.ts.map +1 -0
  143. package/dist/svelte/components/tabs/MiniChat.svelte +253 -0
  144. package/dist/svelte/components/tabs/MiniChat.svelte.d.ts +15 -0
  145. package/dist/svelte/components/tabs/MiniChat.svelte.d.ts.map +1 -0
  146. package/dist/svelte/i18n.d.ts +51 -0
  147. package/dist/svelte/i18n.d.ts.map +1 -0
  148. package/dist/svelte/i18n.js +72 -0
  149. package/dist/svelte/i18n.messages.d.ts +50 -0
  150. package/dist/svelte/i18n.messages.d.ts.map +1 -0
  151. package/dist/svelte/i18n.messages.js +69 -0
  152. package/dist/svelte/index.d.ts +48 -0
  153. package/dist/svelte/index.d.ts.map +1 -0
  154. package/dist/svelte/index.js +117 -0
  155. package/dist/svelte/playground.d.ts +171 -0
  156. package/dist/svelte/playground.d.ts.map +1 -0
  157. package/dist/svelte/playground.js +161 -0
  158. package/dist/svelte/types.d.ts +116 -0
  159. package/dist/svelte/types.d.ts.map +1 -0
  160. package/dist/svelte/types.js +1 -0
  161. package/dist/types.d.ts +99 -0
  162. package/dist/types.d.ts.map +1 -0
  163. package/dist/types.js +2 -0
  164. package/dist/types.js.map +1 -0
  165. package/dist/ui.d.ts +4 -0
  166. package/dist/ui.d.ts.map +1 -0
  167. package/dist/ui.js +92 -0
  168. package/dist/ui.js.map +1 -0
  169. package/package.json +95 -0
@@ -0,0 +1,382 @@
1
+ <script lang="ts">
2
+ /**
3
+ * FileUpload - Attachment upload UI
4
+ * Button with drag-and-drop zone. Shows file preview before upload.
5
+ * Supports file type filtering and max size constraints.
6
+ */
7
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
8
+ import { M } from '../../i18n.js';
9
+
10
+ const { t } = useI18n();
11
+
12
+ export interface Props {
13
+ /** Callback when files are selected */
14
+ onupload: (files: FileList) => void;
15
+ /** Accepted file types (e.g., "image/*,.pdf") */
16
+ accept?: string;
17
+ /** Maximum file size in bytes */
18
+ maxSize?: number;
19
+ /** Whether the upload is disabled */
20
+ disabled?: boolean;
21
+ }
22
+
23
+ const {
24
+ onupload,
25
+ accept,
26
+ maxSize = 25 * 1024 * 1024,
27
+ disabled = false,
28
+ }: Props = $props();
29
+
30
+ let isDragOver = $state(false);
31
+ let pendingFiles = $state<File[]>([]);
32
+ let sizeError = $state('');
33
+
34
+ function validateAndStage(fileList: FileList | null) {
35
+ if (!fileList || fileList.length === 0) return;
36
+ sizeError = '';
37
+
38
+ const valid: File[] = [];
39
+ const oversized: string[] = [];
40
+
41
+ for (const file of Array.from(fileList)) {
42
+ if (file.size > maxSize) {
43
+ oversized.push(file.name);
44
+ } else {
45
+ valid.push(file);
46
+ }
47
+ }
48
+
49
+ if (oversized.length > 0) {
50
+ sizeError = `${oversized.join(', ')} exceed${oversized.length === 1 ? 's' : ''} the ${formatSize(maxSize)} limit`;
51
+ }
52
+
53
+ pendingFiles = [...pendingFiles, ...valid];
54
+ }
55
+
56
+ function handleDrop(event: DragEvent) {
57
+ event.preventDefault();
58
+ isDragOver = false;
59
+ if (disabled) return;
60
+ validateAndStage(event.dataTransfer?.files ?? null);
61
+ }
62
+
63
+ function handleDragOver(event: DragEvent) {
64
+ event.preventDefault();
65
+ if (!disabled) isDragOver = true;
66
+ }
67
+
68
+ function handleDragLeave() {
69
+ isDragOver = false;
70
+ }
71
+
72
+ function handleInputChange(event: Event) {
73
+ const input = event.target as HTMLInputElement;
74
+ validateAndStage(input.files);
75
+ input.value = '';
76
+ }
77
+
78
+ function removePending(index: number) {
79
+ pendingFiles = pendingFiles.filter((_, i) => i !== index);
80
+ if (pendingFiles.length === 0) sizeError = '';
81
+ }
82
+
83
+ function confirmUpload() {
84
+ if (pendingFiles.length === 0) return;
85
+ const dt = new DataTransfer();
86
+ for (const file of pendingFiles) {
87
+ dt.items.add(file);
88
+ }
89
+ onupload(dt.files);
90
+ pendingFiles = [];
91
+ sizeError = '';
92
+ }
93
+
94
+ function formatSize(bytes: number): string {
95
+ if (bytes < 1024) return `${bytes} B`;
96
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
97
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
98
+ }
99
+
100
+ function getFileIcon(type: string): string {
101
+ if (type.startsWith('image/')) return 'img';
102
+ if (type.startsWith('video/')) return 'vid';
103
+ if (type.startsWith('audio/')) return 'aud';
104
+ if (type === 'application/pdf') return 'pdf';
105
+ return 'file';
106
+ }
107
+ </script>
108
+
109
+ <div class="file-upload" class:file-upload--disabled={disabled}>
110
+ {#if pendingFiles.length > 0}
111
+ <div class="file-upload__preview" aria-label={t(M['chat.file_upload.preview'])}>
112
+ {#each pendingFiles as file, i (file.name + i)}
113
+ <div class="file-upload__file">
114
+ <span class="file-upload__file-icon">{getFileIcon(file.type)}</span>
115
+ <div class="file-upload__file-info">
116
+ <span class="file-upload__file-name">{file.name}</span>
117
+ <span class="file-upload__file-size">{formatSize(file.size)}</span>
118
+ </div>
119
+ <button
120
+ class="file-upload__file-remove"
121
+ type="button"
122
+ onclick={() => removePending(i)}
123
+ aria-label={t(M['chat.file_upload.remove'], { name: file.name })}
124
+ >
125
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
126
+ <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
127
+ </svg>
128
+ </button>
129
+ </div>
130
+ {/each}
131
+
132
+ <div class="file-upload__preview-actions">
133
+ <button
134
+ class="file-upload__confirm-btn"
135
+ type="button"
136
+ onclick={confirmUpload}
137
+ >
138
+ {t(M['chat.file_upload.upload_files'], { count: pendingFiles.length, plural: pendingFiles.length !== 1 ? 's' : '' })}
139
+ </button>
140
+ <button
141
+ class="file-upload__clear-btn"
142
+ type="button"
143
+ onclick={() => { pendingFiles = []; sizeError = ''; }}
144
+ >
145
+ Clear
146
+ </button>
147
+ </div>
148
+ </div>
149
+ {/if}
150
+
151
+ {#if sizeError}
152
+ <div class="file-upload__error" role="alert">
153
+ {sizeError}
154
+ </div>
155
+ {/if}
156
+
157
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
158
+ <div
159
+ class="file-upload__drop-zone"
160
+ class:file-upload__drop-zone--active={isDragOver}
161
+ class:file-upload__drop-zone--disabled={disabled}
162
+ ondrop={handleDrop}
163
+ ondragover={handleDragOver}
164
+ ondragleave={handleDragLeave}
165
+ >
166
+ <label class="file-upload__label">
167
+ <input
168
+ type="file"
169
+ class="file-upload__input"
170
+ {accept}
171
+ multiple
172
+ {disabled}
173
+ onchange={handleInputChange}
174
+ />
175
+ <svg class="file-upload__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
176
+ <path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" />
177
+ </svg>
178
+ <span class="file-upload__text">
179
+ {#if disabled}
180
+ {t(M['chat.file_upload.disabled'])}
181
+ {:else}
182
+ {t(M['chat.file_upload.drop_files'])}
183
+ {/if}
184
+ </span>
185
+ <span class="file-upload__hint">{t(M['chat.file_upload.max_size'], { size: formatSize(maxSize) })}</span>
186
+ </label>
187
+ </div>
188
+ </div>
189
+
190
+ <style>
191
+ .file-upload {
192
+ display: flex;
193
+ flex-direction: column;
194
+ gap: var(--smrt-spacing-2, 8px);
195
+ }
196
+
197
+ .file-upload--disabled {
198
+ opacity: 0.5;
199
+ pointer-events: none;
200
+ }
201
+
202
+ .file-upload__drop-zone {
203
+ border: 2px dashed var(--smrt-color-outline-variant, #c4c6d0);
204
+ border-radius: var(--smrt-radius-large, 12px);
205
+ padding: var(--smrt-spacing-5, 20px);
206
+ text-align: center;
207
+ transition:
208
+ border-color var(--smrt-duration-short2, 150ms),
209
+ background var(--smrt-duration-short2, 150ms);
210
+ }
211
+
212
+ .file-upload__drop-zone--active {
213
+ border-color: var(--smrt-color-primary, #005ac1);
214
+ background: var(--smrt-color-primary-container, #d6e3ff);
215
+ }
216
+
217
+ .file-upload__drop-zone--disabled {
218
+ cursor: not-allowed;
219
+ }
220
+
221
+ .file-upload__label {
222
+ display: flex;
223
+ flex-direction: column;
224
+ align-items: center;
225
+ gap: var(--smrt-spacing-2, 8px);
226
+ cursor: pointer;
227
+ }
228
+
229
+ .file-upload__drop-zone--disabled .file-upload__label {
230
+ cursor: not-allowed;
231
+ }
232
+
233
+ .file-upload__input {
234
+ display: none;
235
+ }
236
+
237
+ .file-upload__icon {
238
+ color: var(--smrt-color-outline, #74777f);
239
+ }
240
+
241
+ .file-upload__text {
242
+ font: var(--smrt-typography-body-medium-font, 0.875rem/1.4 sans-serif);
243
+ color: var(--smrt-color-on-surface, #1a1c1e);
244
+ }
245
+
246
+ .file-upload__hint {
247
+ font: var(--smrt-typography-label-small-font, 500 0.6875rem/1 sans-serif);
248
+ color: var(--smrt-color-outline, #74777f);
249
+ }
250
+
251
+ .file-upload__preview {
252
+ display: flex;
253
+ flex-direction: column;
254
+ gap: var(--smrt-spacing-1, 4px);
255
+ padding: var(--smrt-spacing-2, 8px);
256
+ background: var(--smrt-color-surface-container-low, #f7f7fb);
257
+ border-radius: var(--smrt-radius-medium, 8px);
258
+ border: 1px solid var(--smrt-color-outline-variant, #c4c6d0);
259
+ }
260
+
261
+ .file-upload__file {
262
+ display: flex;
263
+ align-items: center;
264
+ gap: var(--smrt-spacing-2, 8px);
265
+ padding: var(--smrt-spacing-2, 8px);
266
+ border-radius: var(--smrt-radius-small, 4px);
267
+ background: var(--smrt-color-surface, #fefbff);
268
+ }
269
+
270
+ .file-upload__file-icon {
271
+ display: inline-flex;
272
+ align-items: center;
273
+ justify-content: center;
274
+ width: 28px;
275
+ height: 28px;
276
+ border-radius: var(--smrt-radius-small, 4px);
277
+ background: var(--smrt-color-surface-variant, #e1e2ec);
278
+ font: var(--smrt-typography-label-small-font, 500 0.6875rem/1 sans-serif);
279
+ color: var(--smrt-color-on-surface-variant, #43474e);
280
+ text-transform: uppercase;
281
+ flex-shrink: 0;
282
+ }
283
+
284
+ .file-upload__file-info {
285
+ flex: 1;
286
+ display: flex;
287
+ flex-direction: column;
288
+ gap: var(--smrt-spacing-1, 4px);
289
+ min-width: 0;
290
+ }
291
+
292
+ .file-upload__file-name {
293
+ font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
294
+ color: var(--smrt-color-on-surface, #1a1c1e);
295
+ white-space: nowrap;
296
+ overflow: hidden;
297
+ text-overflow: ellipsis;
298
+ }
299
+
300
+ .file-upload__file-size {
301
+ font: var(--smrt-typography-label-small-font, 500 0.6875rem/1 sans-serif);
302
+ color: var(--smrt-color-outline, #74777f);
303
+ }
304
+
305
+ .file-upload__file-remove {
306
+ display: inline-flex;
307
+ align-items: center;
308
+ justify-content: center;
309
+ width: 24px;
310
+ height: 24px;
311
+ border: none;
312
+ background: none;
313
+ color: var(--smrt-color-on-surface-variant, #43474e);
314
+ cursor: pointer;
315
+ border-radius: var(--smrt-radius-full, 9999px);
316
+ flex-shrink: 0;
317
+ transition: background var(--smrt-duration-short2, 150ms);
318
+ }
319
+
320
+ .file-upload__file-remove:hover {
321
+ background: var(--smrt-color-error-container, #ffdad6);
322
+ color: var(--smrt-color-error, #ba1a1a);
323
+ }
324
+
325
+ .file-upload__preview-actions {
326
+ display: flex;
327
+ gap: var(--smrt-spacing-2, 8px);
328
+ padding-top: var(--smrt-spacing-1, 4px);
329
+ }
330
+
331
+ .file-upload__confirm-btn {
332
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-4, 16px);
333
+ border: none;
334
+ border-radius: var(--smrt-radius-full, 9999px);
335
+ background: var(--smrt-color-primary, #005ac1);
336
+ color: var(--smrt-color-on-primary, #ffffff);
337
+ font: var(--smrt-typography-label-large-font, 500 0.875rem/1.25 sans-serif);
338
+ cursor: pointer;
339
+ transition: opacity var(--smrt-duration-short2, 150ms);
340
+ }
341
+
342
+ .file-upload__confirm-btn:hover {
343
+ opacity: 0.85;
344
+ }
345
+
346
+ .file-upload__confirm-btn:focus-visible {
347
+ outline: 2px solid var(--smrt-color-primary, #005ac1);
348
+ outline-offset: 2px;
349
+ }
350
+
351
+ .file-upload__clear-btn {
352
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-4, 16px);
353
+ border: 1px solid var(--smrt-color-outline, #74777f);
354
+ border-radius: var(--smrt-radius-full, 9999px);
355
+ background: transparent;
356
+ color: var(--smrt-color-on-surface-variant, #43474e);
357
+ font: var(--smrt-typography-label-large-font, 500 0.875rem/1.25 sans-serif);
358
+ cursor: pointer;
359
+ transition: background var(--smrt-duration-short2, 150ms);
360
+ }
361
+
362
+ .file-upload__clear-btn:hover {
363
+ background: var(--smrt-color-surface-variant, #e1e2ec);
364
+ }
365
+
366
+ .file-upload__error {
367
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
368
+ border-radius: var(--smrt-radius-small, 4px);
369
+ background: var(--smrt-color-error-container, #ffdad6);
370
+ color: var(--smrt-color-on-error-container, #410002);
371
+ font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
372
+ }
373
+
374
+ @media (prefers-reduced-motion: reduce) {
375
+ .file-upload__drop-zone,
376
+ .file-upload__file-remove,
377
+ .file-upload__confirm-btn,
378
+ .file-upload__clear-btn {
379
+ transition: none;
380
+ }
381
+ }
382
+ </style>
@@ -0,0 +1,14 @@
1
+ export interface Props {
2
+ /** Callback when files are selected */
3
+ onupload: (files: FileList) => void;
4
+ /** Accepted file types (e.g., "image/*,.pdf") */
5
+ accept?: string;
6
+ /** Maximum file size in bytes */
7
+ maxSize?: number;
8
+ /** Whether the upload is disabled */
9
+ disabled?: boolean;
10
+ }
11
+ declare const FileUpload: import("svelte").Component<Props, {}, "">;
12
+ type FileUpload = ReturnType<typeof FileUpload>;
13
+ export default FileUpload;
14
+ //# sourceMappingURL=FileUpload.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileUpload.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/shared/FileUpload.svelte.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,KAAK;IACpB,uCAAuC;IACvC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;IACpC,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAyJD,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LinkPreview - URL preview card
4
+ * Compact card with thumbnail, title, and description.
5
+ */
6
+
7
+ export interface Props {
8
+ /** The URL being previewed */
9
+ url: string;
10
+ /** Title of the linked page */
11
+ title?: string;
12
+ /** Description of the linked page */
13
+ description?: string;
14
+ /** Preview thumbnail image URL */
15
+ imageUrl?: string;
16
+ }
17
+
18
+ const { url, title, description, imageUrl }: Props = $props();
19
+
20
+ const hostname = $derived.by(() => {
21
+ try {
22
+ return new URL(url).hostname;
23
+ } catch {
24
+ return url;
25
+ }
26
+ });
27
+ </script>
28
+
29
+ <a class="link-preview" href={url} target="_blank" rel="noopener noreferrer">
30
+ {#if imageUrl}
31
+ <div class="link-preview__image">
32
+ <img src={imageUrl} alt={title ?? ''} />
33
+ </div>
34
+ {/if}
35
+ <div class="link-preview__body">
36
+ {#if title}
37
+ <span class="link-preview__title">{title}</span>
38
+ {/if}
39
+ {#if description}
40
+ <span class="link-preview__description">{description}</span>
41
+ {/if}
42
+ <span class="link-preview__domain">{hostname}</span>
43
+ </div>
44
+ </a>
45
+
46
+ <style>
47
+ .link-preview {
48
+ display: flex;
49
+ border: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
50
+ border-radius: var(--smrt-radius-medium, 0.5rem);
51
+ overflow: hidden;
52
+ text-decoration: none;
53
+ color: inherit;
54
+ max-width: 360px;
55
+ transition: box-shadow var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
56
+ }
57
+
58
+ .link-preview:hover {
59
+ box-shadow: var(--smrt-elevation-1, 0 1px 3px rgba(0, 0, 0, 0.12));
60
+ }
61
+
62
+ .link-preview__image {
63
+ width: 80px;
64
+ min-height: 60px;
65
+ flex-shrink: 0;
66
+ overflow: hidden;
67
+ background: var(--smrt-color-surface-container, #f3f4f6);
68
+ }
69
+
70
+ .link-preview__image img {
71
+ width: 100%;
72
+ height: 100%;
73
+ object-fit: cover;
74
+ }
75
+
76
+ .link-preview__body {
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: var(--smrt-spacing-1, 0.25rem);
80
+ padding: var(--smrt-spacing-2, 0.375rem) var(--smrt-spacing-3, 0.75rem);
81
+ min-width: 0;
82
+ overflow: hidden;
83
+ }
84
+
85
+ .link-preview__title {
86
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
87
+ font-weight: var(--smrt-typography-weight-medium, 500);
88
+ color: var(--smrt-color-on-surface, #1b1b1f);
89
+ white-space: nowrap;
90
+ overflow: hidden;
91
+ text-overflow: ellipsis;
92
+ }
93
+
94
+ .link-preview__description {
95
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
96
+ color: var(--smrt-color-on-surface-variant, #43474e);
97
+ display: -webkit-box;
98
+ -webkit-line-clamp: 2;
99
+ line-clamp: 2;
100
+ -webkit-box-orient: vertical;
101
+ overflow: hidden;
102
+ }
103
+
104
+ .link-preview__domain {
105
+ font-size: var(--smrt-typography-label-small-size, 0.6875rem);
106
+ color: var(--smrt-color-outline, #74777f);
107
+ }
108
+ </style>
@@ -0,0 +1,18 @@
1
+ /**
2
+ * LinkPreview - URL preview card
3
+ * Compact card with thumbnail, title, and description.
4
+ */
5
+ export interface Props {
6
+ /** The URL being previewed */
7
+ url: string;
8
+ /** Title of the linked page */
9
+ title?: string;
10
+ /** Description of the linked page */
11
+ description?: string;
12
+ /** Preview thumbnail image URL */
13
+ imageUrl?: string;
14
+ }
15
+ declare const LinkPreview: import("svelte").Component<Props, {}, "">;
16
+ type LinkPreview = ReturnType<typeof LinkPreview>;
17
+ export default LinkPreview;
18
+ //# sourceMappingURL=LinkPreview.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LinkPreview.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/shared/LinkPreview.svelte.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,MAAM,WAAW,KAAK;IACpB,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAoCD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,168 @@
1
+ <script lang="ts">
2
+ /**
3
+ * MentionAutocomplete - @mention suggestions popup
4
+ * Floating dropdown positioned near cursor. Shows matching profile names
5
+ * with optional avatars. Keyboard navigable.
6
+ */
7
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
8
+ import { M } from '../../i18n.js';
9
+ import Avatar from './Avatar.svelte';
10
+
11
+ const { t } = useI18n();
12
+
13
+ export interface Props {
14
+ /** Current search query (text after @) */
15
+ query: string;
16
+ /** Matching suggestions */
17
+ suggestions: Array<{ id: string; name: string; avatarUrl?: string }>;
18
+ /** Select a suggestion */
19
+ onselect: (id: string) => void;
20
+ /** Whether the popup is visible */
21
+ isVisible: boolean;
22
+ }
23
+
24
+ const { query, suggestions, onselect, isVisible }: Props = $props();
25
+
26
+ let activeIndex = $state(0);
27
+
28
+ // Reset active index when suggestions change
29
+ $effect(() => {
30
+ if (suggestions.length) {
31
+ activeIndex = 0;
32
+ }
33
+ });
34
+
35
+ function handleKeydown(event: KeyboardEvent) {
36
+ if (!isVisible || suggestions.length === 0) return;
37
+
38
+ switch (event.key) {
39
+ case 'ArrowDown':
40
+ event.preventDefault();
41
+ activeIndex = (activeIndex + 1) % suggestions.length;
42
+ break;
43
+ case 'ArrowUp':
44
+ event.preventDefault();
45
+ activeIndex = (activeIndex - 1 + suggestions.length) % suggestions.length;
46
+ break;
47
+ case 'Enter':
48
+ case 'Tab':
49
+ event.preventDefault();
50
+ onselect(suggestions[activeIndex].id);
51
+ break;
52
+ case 'Escape':
53
+ break;
54
+ }
55
+ }
56
+
57
+ function highlightMatch(
58
+ name: string,
59
+ q: string,
60
+ ): { before: string; match: string; after: string } {
61
+ if (!q) return { before: name, match: '', after: '' };
62
+ const lower = name.toLowerCase();
63
+ const idx = lower.indexOf(q.toLowerCase());
64
+ if (idx === -1) return { before: name, match: '', after: '' };
65
+ return {
66
+ before: name.slice(0, idx),
67
+ match: name.slice(idx, idx + q.length),
68
+ after: name.slice(idx + q.length),
69
+ };
70
+ }
71
+ </script>
72
+
73
+ <svelte:window onkeydown={isVisible ? handleKeydown : undefined} />
74
+
75
+ {#if isVisible && suggestions.length > 0}
76
+ <div
77
+ class="mention-autocomplete"
78
+ role="listbox"
79
+ aria-label={t(M['chat.mention_autocomplete.suggestions'])}
80
+ >
81
+ {#each suggestions as suggestion, i (suggestion.id)}
82
+ {@const parts = highlightMatch(suggestion.name, query)}
83
+ <button
84
+ class="mention-autocomplete__item"
85
+ class:mention-autocomplete__item--active={i === activeIndex}
86
+ type="button"
87
+ role="option"
88
+ aria-selected={i === activeIndex}
89
+ onclick={() => onselect(suggestion.id)}
90
+ onpointerenter={() => activeIndex = i}
91
+ >
92
+ <Avatar
93
+ name={suggestion.name}
94
+ avatarUrl={suggestion.avatarUrl}
95
+ size="sm"
96
+ />
97
+ <span class="mention-autocomplete__name">
98
+ {parts.before}<mark class="mention-autocomplete__highlight">{parts.match}</mark>{parts.after}
99
+ </span>
100
+ </button>
101
+ {/each}
102
+ </div>
103
+ {/if}
104
+
105
+ <style>
106
+ .mention-autocomplete {
107
+ position: absolute;
108
+ bottom: 100%;
109
+ left: 0;
110
+ margin-bottom: var(--smrt-spacing-1, 4px);
111
+ min-width: 200px;
112
+ max-width: 320px;
113
+ max-height: 240px;
114
+ overflow-y: auto;
115
+ background: var(--smrt-color-surface, #fefbff);
116
+ border: 1px solid var(--smrt-color-outline-variant, #c4c6d0);
117
+ border-radius: var(--smrt-radius-medium, 8px);
118
+ box-shadow: var(--smrt-elevation-2, 0 2px 8px rgba(0, 0, 0, 0.15));
119
+ z-index: var(--smrt-z-index-dropdown, 1000);
120
+ padding: var(--smrt-spacing-1, 4px);
121
+ display: flex;
122
+ flex-direction: column;
123
+ }
124
+
125
+ .mention-autocomplete__item {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: var(--smrt-spacing-2, 8px);
129
+ width: 100%;
130
+ padding: var(--smrt-spacing-2, 8px);
131
+ border: none;
132
+ background: transparent;
133
+ cursor: pointer;
134
+ text-align: left;
135
+ color: var(--smrt-color-on-surface, #1a1c1e);
136
+ border-radius: var(--smrt-radius-small, 4px);
137
+ transition: background var(--smrt-duration-short2, 150ms);
138
+ }
139
+
140
+ .mention-autocomplete__item:hover,
141
+ .mention-autocomplete__item--active {
142
+ background: var(--smrt-color-surface-container-high, #e6e6ea);
143
+ }
144
+
145
+ .mention-autocomplete__item:focus-visible {
146
+ outline: 2px solid var(--smrt-color-primary, #005ac1);
147
+ outline-offset: -2px;
148
+ }
149
+
150
+ .mention-autocomplete__name {
151
+ font: var(--smrt-typography-body-medium-font, 0.875rem/1.25 sans-serif);
152
+ white-space: nowrap;
153
+ overflow: hidden;
154
+ text-overflow: ellipsis;
155
+ }
156
+
157
+ .mention-autocomplete__highlight {
158
+ background: transparent;
159
+ color: var(--smrt-color-primary, #005ac1);
160
+ font-weight: var(--smrt-typography-weight-semibold, 600);
161
+ }
162
+
163
+ @media (prefers-reduced-motion: reduce) {
164
+ .mention-autocomplete__item {
165
+ transition: none;
166
+ }
167
+ }
168
+ </style>