@juspay/svelte-ui-components 2.126.0 → 2.127.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.
- package/dist/AttachmentChipRow/AttachmentChipRow.svelte +354 -0
- package/dist/AttachmentChipRow/AttachmentChipRow.svelte.d.ts +4 -0
- package/dist/AttachmentChipRow/properties.d.ts +47 -0
- package/dist/AttachmentChipRow/properties.js +1 -0
- package/dist/ChatBubble/ChatBubble.svelte.d.ts +1 -1
- package/dist/ChatComposer/ChatComposer.svelte +96 -10
- package/dist/ChatComposer/properties.d.ts +76 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte +118 -11
- package/dist/ChatSuggestions/properties.d.ts +42 -0
- package/dist/HITL/HITL.svelte +576 -0
- package/dist/HITL/HITL.svelte.d.ts +4 -0
- package/dist/HITL/properties.d.ts +71 -0
- package/dist/HITL/properties.js +1 -0
- package/dist/HITL/timers.d.ts +6 -0
- package/dist/HITL/timers.js +7 -0
- package/dist/Pill/Pill.svelte +6 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +217 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte.d.ts +4 -0
- package/dist/ThinkingIndicator/properties.d.ts +30 -0
- package/dist/ThinkingIndicator/properties.js +1 -0
- package/dist/Tooltip/tooltip-action.js +5 -0
- package/dist/TypewriterText/TypewriterText.svelte +110 -0
- package/dist/TypewriterText/TypewriterText.svelte.d.ts +4 -0
- package/dist/TypewriterText/properties.d.ts +21 -0
- package/dist/TypewriterText/properties.js +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../Button/Button.svelte';
|
|
3
|
+
import Img from '../Img/Img.svelte';
|
|
4
|
+
import Scroller from '../Scroller/Scroller.svelte';
|
|
5
|
+
import { tooltip } from '../Tooltip/tooltip-action';
|
|
6
|
+
import type { AttachmentChipRowProperties } from './properties';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The pending-attachment strip above a chat composer: image thumbnails, video
|
|
10
|
+
* poster tiles (with a play badge) and file tiles, each with a floating remove
|
|
11
|
+
* button, scrolling horizontally when they overflow. Renders NOTHING while
|
|
12
|
+
* there are no attachments.
|
|
13
|
+
*/
|
|
14
|
+
let {
|
|
15
|
+
images = [],
|
|
16
|
+
files = [],
|
|
17
|
+
videos = [],
|
|
18
|
+
onRemoveImage,
|
|
19
|
+
onRemoveFile,
|
|
20
|
+
onRemoveVideo,
|
|
21
|
+
onOpenImage,
|
|
22
|
+
onOpenVideo,
|
|
23
|
+
onOpenFile,
|
|
24
|
+
imageTooltip,
|
|
25
|
+
videoTooltip,
|
|
26
|
+
removeIcon,
|
|
27
|
+
fileIcon,
|
|
28
|
+
testId,
|
|
29
|
+
classes
|
|
30
|
+
}: AttachmentChipRowProperties = $props();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
{#if images.length > 0 || videos.length > 0 || files.length > 0}
|
|
34
|
+
<Scroller
|
|
35
|
+
direction="horizontal"
|
|
36
|
+
showArrows={false}
|
|
37
|
+
hideScrollbar={false}
|
|
38
|
+
showGradient={false}
|
|
39
|
+
classes={`attachment-chip-row ${classes ?? ''}`}
|
|
40
|
+
{testId}
|
|
41
|
+
>
|
|
42
|
+
{#each images as image (image.id)}
|
|
43
|
+
<div
|
|
44
|
+
class="chip"
|
|
45
|
+
data-pw={typeof testId === 'string' ? `${testId}-image-${image.id}` : null}
|
|
46
|
+
testID={typeof testId === 'string' ? `${testId}-image-${image.id}` : null}
|
|
47
|
+
>
|
|
48
|
+
<svelte:element
|
|
49
|
+
this={typeof onOpenImage === 'function' ? 'button' : 'div'}
|
|
50
|
+
type={typeof onOpenImage === 'function' ? 'button' : null}
|
|
51
|
+
role={typeof onOpenImage === 'function' ? 'button' : null}
|
|
52
|
+
aria-label={typeof onOpenImage === 'function'
|
|
53
|
+
? `Open ${image.filename ?? 'image'}`
|
|
54
|
+
: null}
|
|
55
|
+
class="thumb"
|
|
56
|
+
class:openable={typeof onOpenImage === 'function'}
|
|
57
|
+
onclick={typeof onOpenImage === 'function' ? () => onOpenImage(image) : null}
|
|
58
|
+
use:tooltip={{
|
|
59
|
+
text: typeof imageTooltip === 'function' ? imageTooltip(image) : '',
|
|
60
|
+
position: 'top'
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<Img src={image.thumbnailData} alt={image.filename ?? 'Uploaded image'} fallback="" />
|
|
64
|
+
</svelte:element>
|
|
65
|
+
{#if typeof onRemoveImage === 'function'}
|
|
66
|
+
<div class="remove-wrap">
|
|
67
|
+
<Button
|
|
68
|
+
variant="secondary"
|
|
69
|
+
iconOnly={true}
|
|
70
|
+
ariaLabel="Remove image"
|
|
71
|
+
testId={testId && `${testId}-remove-image-${image.id}`}
|
|
72
|
+
onclick={() => onRemoveImage(image.id)}
|
|
73
|
+
>
|
|
74
|
+
{#snippet icon()}
|
|
75
|
+
{#if removeIcon}
|
|
76
|
+
{@render removeIcon()}
|
|
77
|
+
{:else}
|
|
78
|
+
<svg class="cross" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
79
|
+
<path
|
|
80
|
+
d="M4 4l8 8m0-8l-8 8"
|
|
81
|
+
stroke="currentColor"
|
|
82
|
+
stroke-width="1.5"
|
|
83
|
+
stroke-linecap="round"
|
|
84
|
+
/>
|
|
85
|
+
</svg>
|
|
86
|
+
{/if}
|
|
87
|
+
{/snippet}
|
|
88
|
+
</Button>
|
|
89
|
+
</div>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
92
|
+
{/each}
|
|
93
|
+
{#each videos as video (video.id)}
|
|
94
|
+
<div
|
|
95
|
+
class="chip"
|
|
96
|
+
data-pw={typeof testId === 'string' ? `${testId}-video-${video.id}` : null}
|
|
97
|
+
testID={typeof testId === 'string' ? `${testId}-video-${video.id}` : null}
|
|
98
|
+
>
|
|
99
|
+
<svelte:element
|
|
100
|
+
this={typeof onOpenVideo === 'function' ? 'button' : 'div'}
|
|
101
|
+
type={typeof onOpenVideo === 'function' ? 'button' : null}
|
|
102
|
+
role={typeof onOpenVideo === 'function' ? 'button' : null}
|
|
103
|
+
aria-label={typeof onOpenVideo === 'function'
|
|
104
|
+
? `Play ${video.filename ?? 'video'}`
|
|
105
|
+
: null}
|
|
106
|
+
class="thumb video-tile"
|
|
107
|
+
class:openable={typeof onOpenVideo === 'function'}
|
|
108
|
+
onclick={typeof onOpenVideo === 'function' ? () => onOpenVideo(video) : null}
|
|
109
|
+
use:tooltip={{
|
|
110
|
+
text: typeof videoTooltip === 'function' ? videoTooltip(video) : '',
|
|
111
|
+
position: 'top'
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{#if typeof video.thumbnailData === 'string' && video.thumbnailData.length > 0}
|
|
115
|
+
<Img src={video.thumbnailData} alt={video.filename ?? 'Video attachment'} fallback="" />
|
|
116
|
+
{/if}
|
|
117
|
+
<span class="play-badge" aria-hidden="true">
|
|
118
|
+
<svg class="play-glyph" viewBox="0 0 16 16" fill="none">
|
|
119
|
+
<path d="M6 4.5v7l5.5-3.5z" fill="currentColor" />
|
|
120
|
+
</svg>
|
|
121
|
+
</span>
|
|
122
|
+
</svelte:element>
|
|
123
|
+
{#if typeof onRemoveVideo === 'function'}
|
|
124
|
+
<div class="remove-wrap">
|
|
125
|
+
<Button
|
|
126
|
+
variant="secondary"
|
|
127
|
+
iconOnly={true}
|
|
128
|
+
ariaLabel="Remove video"
|
|
129
|
+
testId={testId && `${testId}-remove-video-${video.id}`}
|
|
130
|
+
onclick={() => onRemoveVideo(video.id)}
|
|
131
|
+
>
|
|
132
|
+
{#snippet icon()}
|
|
133
|
+
{#if removeIcon}
|
|
134
|
+
{@render removeIcon()}
|
|
135
|
+
{:else}
|
|
136
|
+
<svg class="cross" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
137
|
+
<path
|
|
138
|
+
d="M4 4l8 8m0-8l-8 8"
|
|
139
|
+
stroke="currentColor"
|
|
140
|
+
stroke-width="1.5"
|
|
141
|
+
stroke-linecap="round"
|
|
142
|
+
/>
|
|
143
|
+
</svg>
|
|
144
|
+
{/if}
|
|
145
|
+
{/snippet}
|
|
146
|
+
</Button>
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
</div>
|
|
150
|
+
{/each}
|
|
151
|
+
{#each files as file (file.id)}
|
|
152
|
+
<div
|
|
153
|
+
class="chip"
|
|
154
|
+
data-pw={typeof testId === 'string' ? `${testId}-file-${file.id}` : null}
|
|
155
|
+
testID={typeof testId === 'string' ? `${testId}-file-${file.id}` : null}
|
|
156
|
+
>
|
|
157
|
+
<svelte:element
|
|
158
|
+
this={typeof onOpenFile === 'function' ? 'button' : 'div'}
|
|
159
|
+
type={typeof onOpenFile === 'function' ? 'button' : null}
|
|
160
|
+
role={typeof onOpenFile === 'function' ? 'button' : null}
|
|
161
|
+
aria-label={typeof onOpenFile === 'function' ? `Open ${file.filename}` : null}
|
|
162
|
+
class="thumb file-tile"
|
|
163
|
+
class:openable={typeof onOpenFile === 'function'}
|
|
164
|
+
onclick={typeof onOpenFile === 'function' ? () => onOpenFile(file) : null}
|
|
165
|
+
>
|
|
166
|
+
{#if fileIcon}
|
|
167
|
+
{@render fileIcon()}
|
|
168
|
+
{:else}
|
|
169
|
+
<svg class="file-glyph" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
170
|
+
<path
|
|
171
|
+
d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"
|
|
172
|
+
fill="currentColor"
|
|
173
|
+
/>
|
|
174
|
+
<path
|
|
175
|
+
d="M14 2v6h6"
|
|
176
|
+
stroke="var(--attachment-chip-row-file-glyph-fold-color, #ffffff)"
|
|
177
|
+
stroke-width="1.5"
|
|
178
|
+
stroke-linejoin="round"
|
|
179
|
+
/>
|
|
180
|
+
</svg>
|
|
181
|
+
{/if}
|
|
182
|
+
<div class="file-info">
|
|
183
|
+
<span class="file-name" use:tooltip={{ text: file.filename, position: 'top' }}>
|
|
184
|
+
{file.filename}
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
</svelte:element>
|
|
188
|
+
{#if typeof onRemoveFile === 'function'}
|
|
189
|
+
<div class="remove-wrap">
|
|
190
|
+
<Button
|
|
191
|
+
variant="secondary"
|
|
192
|
+
iconOnly={true}
|
|
193
|
+
ariaLabel="Remove file"
|
|
194
|
+
testId={testId && `${testId}-remove-file-${file.id}`}
|
|
195
|
+
onclick={() => onRemoveFile(file.id)}
|
|
196
|
+
>
|
|
197
|
+
{#snippet icon()}
|
|
198
|
+
{#if removeIcon}
|
|
199
|
+
{@render removeIcon()}
|
|
200
|
+
{:else}
|
|
201
|
+
<svg class="cross" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
202
|
+
<path
|
|
203
|
+
d="M4 4l8 8m0-8l-8 8"
|
|
204
|
+
stroke="currentColor"
|
|
205
|
+
stroke-width="1.5"
|
|
206
|
+
stroke-linecap="round"
|
|
207
|
+
/>
|
|
208
|
+
</svg>
|
|
209
|
+
{/if}
|
|
210
|
+
{/snippet}
|
|
211
|
+
</Button>
|
|
212
|
+
</div>
|
|
213
|
+
{/if}
|
|
214
|
+
</div>
|
|
215
|
+
{/each}
|
|
216
|
+
</Scroller>
|
|
217
|
+
{/if}
|
|
218
|
+
|
|
219
|
+
<style>
|
|
220
|
+
/* Keyed on a component-namespaced class passed to the Scroller root — the Scroller is
|
|
221
|
+
this component's own root, so there is no scoped ancestor to hang the rule on. */
|
|
222
|
+
:global(.attachment-chip-row) {
|
|
223
|
+
--scroller-gap: var(--attachment-chip-row-gap, 0.5rem);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.chip {
|
|
227
|
+
position: relative;
|
|
228
|
+
flex-shrink: 0;
|
|
229
|
+
|
|
230
|
+
/* Top/right padding is the room the floating remove button straddles into — it
|
|
231
|
+
keeps the button inside the chip's own box, so a clipping ancestor (the
|
|
232
|
+
Scroller viewport, a composer strip) can never cut it off. */
|
|
233
|
+
padding-top: var(--attachment-chip-row-chip-padding-top, 0.25rem);
|
|
234
|
+
padding-right: var(--attachment-chip-row-chip-padding-right, 0.25rem);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.thumb {
|
|
238
|
+
display: block;
|
|
239
|
+
width: var(--attachment-chip-row-chip-size, 4.5rem);
|
|
240
|
+
height: var(--attachment-chip-row-chip-size, 4.5rem);
|
|
241
|
+
padding: 0;
|
|
242
|
+
border: 0;
|
|
243
|
+
border-radius: var(--attachment-chip-row-chip-border-radius, 0.5rem);
|
|
244
|
+
background-color: var(--attachment-chip-row-chip-background, #ffffff);
|
|
245
|
+
overflow: hidden;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
button.thumb {
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
font: inherit;
|
|
251
|
+
color: inherit;
|
|
252
|
+
text-align: inherit;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.thumb :global(img) {
|
|
256
|
+
width: var(--attachment-chip-row-thumb-img-width, 100%);
|
|
257
|
+
height: var(--attachment-chip-row-thumb-img-height, 100%);
|
|
258
|
+
object-fit: var(--attachment-chip-row-thumb-img-fit, cover);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.remove-wrap {
|
|
262
|
+
position: absolute;
|
|
263
|
+
top: var(--attachment-chip-row-remove-offset, 0);
|
|
264
|
+
right: var(--attachment-chip-row-remove-offset, 0);
|
|
265
|
+
--button-width: var(--attachment-chip-row-remove-size, 1.25rem);
|
|
266
|
+
--button-height: var(--attachment-chip-row-remove-size, 1.25rem);
|
|
267
|
+
--button-min-width: 0;
|
|
268
|
+
--button-padding: 0;
|
|
269
|
+
--button-border-radius: var(--attachment-chip-row-remove-border-radius, 999px);
|
|
270
|
+
|
|
271
|
+
/* Self-contained colors, readable on both themes: a dark disc with a white
|
|
272
|
+
glyph and a white ring — never inherited from the ambient --button-* scope,
|
|
273
|
+
which flips with the host theme and can go dark-on-dark. */
|
|
274
|
+
--button-color: var(--attachment-chip-row-remove-background, #18181b);
|
|
275
|
+
--button-text-color: var(--attachment-chip-row-remove-color, #ffffff);
|
|
276
|
+
--button-hover-color: var(--attachment-chip-row-remove-hover-background, #3f3f46);
|
|
277
|
+
--button-border: var(--attachment-chip-row-remove-ring, 1.5px solid #ffffff);
|
|
278
|
+
--button-hover-border: var(--attachment-chip-row-remove-ring, 1.5px solid #ffffff);
|
|
279
|
+
--button-box-shadow: var(--attachment-chip-row-remove-box-shadow, 0 1px 3px rgb(0 0 0 / 30%));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.cross {
|
|
283
|
+
width: var(--attachment-chip-row-remove-glyph-size, 0.625rem);
|
|
284
|
+
height: var(--attachment-chip-row-remove-glyph-size, 0.625rem);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.video-tile {
|
|
288
|
+
position: relative;
|
|
289
|
+
background-color: var(--attachment-chip-row-video-tile-background, #18181b);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.play-badge {
|
|
293
|
+
position: absolute;
|
|
294
|
+
inset: 0;
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
justify-content: center;
|
|
298
|
+
pointer-events: none;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.play-glyph {
|
|
302
|
+
width: var(--attachment-chip-row-play-glyph-size, 1.25rem);
|
|
303
|
+
height: var(--attachment-chip-row-play-glyph-size, 1.25rem);
|
|
304
|
+
color: var(--attachment-chip-row-play-glyph-color, #ffffff);
|
|
305
|
+
filter: var(--attachment-chip-row-play-glyph-shadow, drop-shadow(0 1px 2px rgb(0 0 0 / 45%)));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.file-tile {
|
|
309
|
+
display: flex;
|
|
310
|
+
flex-direction: column;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: center;
|
|
313
|
+
gap: var(--attachment-chip-row-file-tile-gap, 0.25rem);
|
|
314
|
+
padding: var(--attachment-chip-row-file-tile-padding, 0.25rem 2px);
|
|
315
|
+
background-color: var(--attachment-chip-row-file-tile-background, #ededed);
|
|
316
|
+
border: var(--attachment-chip-row-file-tile-border, 1px solid #e1e1e1);
|
|
317
|
+
box-shadow: var(--attachment-chip-row-file-tile-box-shadow, 0 2px 0.25rem rgb(0 0 0 / 10%));
|
|
318
|
+
box-sizing: border-box;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.file-glyph {
|
|
322
|
+
width: var(--attachment-chip-row-file-glyph-size, 1.5rem);
|
|
323
|
+
height: var(--attachment-chip-row-file-glyph-size, 1.5rem);
|
|
324
|
+
color: var(--attachment-chip-row-file-glyph-color, #858585);
|
|
325
|
+
flex-shrink: 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.file-info {
|
|
329
|
+
width: 100%;
|
|
330
|
+
text-align: center;
|
|
331
|
+
min-width: 0;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.file-name {
|
|
335
|
+
display: block;
|
|
336
|
+
margin: 0;
|
|
337
|
+
white-space: nowrap;
|
|
338
|
+
overflow: hidden;
|
|
339
|
+
text-overflow: ellipsis;
|
|
340
|
+
max-width: 100%;
|
|
341
|
+
font-size: var(--attachment-chip-row-file-name-font-size, 0.75rem);
|
|
342
|
+
font-weight: var(--attachment-chip-row-file-name-font-weight, 400);
|
|
343
|
+
letter-spacing: var(--attachment-chip-row-file-name-letter-spacing, normal);
|
|
344
|
+
line-height: var(--attachment-chip-row-file-name-line-height, 1.25rem);
|
|
345
|
+
color: var(--attachment-chip-row-file-name-color, #52525b);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@media (width <= 767px) {
|
|
349
|
+
.remove-wrap {
|
|
350
|
+
--button-width: var(--attachment-chip-row-remove-size, 1.125rem);
|
|
351
|
+
--button-height: var(--attachment-chip-row-remove-size, 1.125rem);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type AttachmentChipImage = {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Image src for the thumbnail — a data URI or URL. */
|
|
5
|
+
thumbnailData: string;
|
|
6
|
+
filename?: string | null;
|
|
7
|
+
};
|
|
8
|
+
export type AttachmentChipFile = {
|
|
9
|
+
id: string;
|
|
10
|
+
filename: string;
|
|
11
|
+
};
|
|
12
|
+
export type AttachmentChipVideo = {
|
|
13
|
+
id: string;
|
|
14
|
+
/** Poster/thumbnail src for the tile — a data URI or URL. Omit for a plain dark tile. */
|
|
15
|
+
thumbnailData?: string | null;
|
|
16
|
+
filename?: string | null;
|
|
17
|
+
};
|
|
18
|
+
export type AttachmentChipRowProperties = OptionalAttachmentChipRowProperties & MandatoryAttachmentChipRowProperties;
|
|
19
|
+
export type MandatoryAttachmentChipRowProperties = Record<never, never>;
|
|
20
|
+
export type OptionalAttachmentChipRowProperties = {
|
|
21
|
+
/** Omit to render the row read-only — chips lose their remove buttons. */
|
|
22
|
+
onRemoveImage?: (id: string) => void;
|
|
23
|
+
onRemoveFile?: (id: string) => void;
|
|
24
|
+
onRemoveVideo?: (id: string) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Open/preview callbacks — when provided, the chip's tile becomes a real button
|
|
27
|
+
* and clicking it fires with the attachment (e.g. open a lightbox, play the
|
|
28
|
+
* video). Omitted, the tile stays non-interactive.
|
|
29
|
+
*/
|
|
30
|
+
onOpenImage?: (image: AttachmentChipImage) => void;
|
|
31
|
+
onOpenVideo?: (video: AttachmentChipVideo) => void;
|
|
32
|
+
onOpenFile?: (file: AttachmentChipFile) => void;
|
|
33
|
+
images?: AttachmentChipImage[];
|
|
34
|
+
files?: AttachmentChipFile[];
|
|
35
|
+
/** Video attachments — a poster tile with a play badge (or a plain dark tile without a poster). */
|
|
36
|
+
videos?: AttachmentChipVideo[];
|
|
37
|
+
/** Tooltip text for an image chip. Nothing is shown when omitted or empty. */
|
|
38
|
+
imageTooltip?: (image: AttachmentChipImage) => string;
|
|
39
|
+
/** Tooltip text for a video chip. Nothing is shown when omitted or empty. */
|
|
40
|
+
videoTooltip?: (video: AttachmentChipVideo) => string;
|
|
41
|
+
/** Glyph inside the remove button. Falls back to a built-in cross. */
|
|
42
|
+
removeIcon?: Snippet;
|
|
43
|
+
/** Glyph on a file chip. Falls back to a built-in document. */
|
|
44
|
+
fileIcon?: Snippet;
|
|
45
|
+
testId?: string;
|
|
46
|
+
classes?: string;
|
|
47
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ChatBubbleProperties } from './properties';
|
|
2
|
-
declare const ChatBubble: import("svelte").Component<ChatBubbleProperties, {}, "open" | "
|
|
2
|
+
declare const ChatBubble: import("svelte").Component<ChatBubbleProperties, {}, "open" | "expanded" | "dragX" | "dragY" | "panelWidth" | "panelHeight" | "expandedPanelWidth" | "expandedPanelHeight">;
|
|
3
3
|
type ChatBubble = ReturnType<typeof ChatBubble>;
|
|
4
4
|
export default ChatBubble;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Button from '../Button/Button.svelte';
|
|
3
3
|
import Pill from '../Pill/Pill.svelte';
|
|
4
|
+
import AttachmentChipRow from '../AttachmentChipRow/AttachmentChipRow.svelte';
|
|
4
5
|
import sendSvg from '../assets/send.svg?raw';
|
|
5
6
|
import stopSvg from '../assets/stop.svg?raw';
|
|
6
7
|
import micSvg from '../assets/mic.svg?raw';
|
|
@@ -17,6 +18,21 @@
|
|
|
17
18
|
streaming = false,
|
|
18
19
|
recording = false,
|
|
19
20
|
attachments = $bindable([]),
|
|
21
|
+
attachmentsPreview,
|
|
22
|
+
richImages = [],
|
|
23
|
+
richFiles = [],
|
|
24
|
+
richVideos = [],
|
|
25
|
+
richImageTooltip,
|
|
26
|
+
richVideoTooltip,
|
|
27
|
+
richRemoveIcon,
|
|
28
|
+
richFileIcon,
|
|
29
|
+
onremoverichimage,
|
|
30
|
+
onremoverichfile,
|
|
31
|
+
onremoverichvideo,
|
|
32
|
+
onopenrichimage,
|
|
33
|
+
onopenrichvideo,
|
|
34
|
+
onopenrichfile,
|
|
35
|
+
sendable = null,
|
|
20
36
|
accept = '',
|
|
21
37
|
multiple = false,
|
|
22
38
|
sendLabel = 'Send message',
|
|
@@ -27,22 +43,42 @@
|
|
|
27
43
|
stopIcon,
|
|
28
44
|
voiceIcon,
|
|
29
45
|
attachIcon,
|
|
46
|
+
actionIcon,
|
|
47
|
+
actionLabel = 'Voice conversation',
|
|
30
48
|
leading,
|
|
31
49
|
onsubmit,
|
|
32
50
|
oninput,
|
|
33
51
|
onkeydown,
|
|
52
|
+
onpaste,
|
|
34
53
|
onstop,
|
|
35
54
|
onvoice,
|
|
36
55
|
onattach,
|
|
56
|
+
onattachclick,
|
|
57
|
+
onaction,
|
|
37
58
|
testId,
|
|
59
|
+
inputTestId,
|
|
60
|
+
inputAriaLabel,
|
|
61
|
+
sendTestId,
|
|
62
|
+
sendSlotTestId,
|
|
63
|
+
stopTestId,
|
|
64
|
+
voiceTestId,
|
|
65
|
+
attachTestId,
|
|
66
|
+
actionTestId,
|
|
38
67
|
classes
|
|
39
68
|
}: ChatComposerProperties = $props();
|
|
40
69
|
|
|
41
70
|
let fileInput: HTMLInputElement | null = $state(null);
|
|
42
71
|
|
|
43
72
|
let showVoice = $derived(typeof onvoice === 'function');
|
|
44
|
-
let showAttach = $derived(typeof onattach === 'function');
|
|
45
|
-
let
|
|
73
|
+
let showAttach = $derived(typeof onattach === 'function' || typeof onattachclick === 'function');
|
|
74
|
+
let hasRichAttachments = $derived(
|
|
75
|
+
richImages.length > 0 || richVideos.length > 0 || richFiles.length > 0
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
let canSend = $derived(
|
|
79
|
+
!disabled &&
|
|
80
|
+
(sendable ?? (value.trim().length > 0 || attachments.length > 0 || hasRichAttachments))
|
|
81
|
+
);
|
|
46
82
|
|
|
47
83
|
function submit(): void {
|
|
48
84
|
if (!canSend) {
|
|
@@ -100,7 +136,28 @@
|
|
|
100
136
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
101
137
|
testID={typeof testId === 'string' ? testId : null}
|
|
102
138
|
>
|
|
103
|
-
{#if
|
|
139
|
+
{#if typeof attachmentsPreview === 'function'}
|
|
140
|
+
{@render attachmentsPreview()}
|
|
141
|
+
{:else if hasRichAttachments}
|
|
142
|
+
<div class="attachments-rich">
|
|
143
|
+
<AttachmentChipRow
|
|
144
|
+
images={richImages}
|
|
145
|
+
videos={richVideos}
|
|
146
|
+
files={richFiles}
|
|
147
|
+
onRemoveImage={onremoverichimage}
|
|
148
|
+
onRemoveVideo={onremoverichvideo}
|
|
149
|
+
onRemoveFile={onremoverichfile}
|
|
150
|
+
onOpenImage={onopenrichimage}
|
|
151
|
+
onOpenVideo={onopenrichvideo}
|
|
152
|
+
onOpenFile={onopenrichfile}
|
|
153
|
+
imageTooltip={richImageTooltip}
|
|
154
|
+
videoTooltip={richVideoTooltip}
|
|
155
|
+
removeIcon={richRemoveIcon}
|
|
156
|
+
fileIcon={richFileIcon}
|
|
157
|
+
testId={testId && `${testId}-rich-attachments`}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
{:else if attachments.length > 0}
|
|
104
161
|
<div class="attachments">
|
|
105
162
|
{#each attachments as file, index (file.name + index)}
|
|
106
163
|
<Pill text={file.name} dismissible ondismiss={() => removeAttachment(index)} />
|
|
@@ -111,7 +168,18 @@
|
|
|
111
168
|
<div class="input-row">
|
|
112
169
|
{#if showAttach}
|
|
113
170
|
<div class="control attach">
|
|
114
|
-
<Button
|
|
171
|
+
<Button
|
|
172
|
+
onclick={() => {
|
|
173
|
+
if (typeof onattachclick === 'function') {
|
|
174
|
+
onattachclick();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
fileInput?.click();
|
|
178
|
+
}}
|
|
179
|
+
{disabled}
|
|
180
|
+
ariaLabel={attachLabel}
|
|
181
|
+
testId={attachTestId}
|
|
182
|
+
>
|
|
115
183
|
{#if typeof attachIcon === 'function'}
|
|
116
184
|
{@render attachIcon()}
|
|
117
185
|
{:else}
|
|
@@ -137,20 +205,22 @@
|
|
|
137
205
|
|
|
138
206
|
<textarea
|
|
139
207
|
class="input"
|
|
208
|
+
data-pw={inputTestId ?? null}
|
|
140
209
|
bind:value
|
|
141
210
|
{placeholder}
|
|
142
211
|
{disabled}
|
|
143
212
|
rows="1"
|
|
144
|
-
aria-label={placeholder.length > 0 ? placeholder : 'Message'}
|
|
213
|
+
aria-label={inputAriaLabel ?? (placeholder.length > 0 ? placeholder : 'Message')}
|
|
145
214
|
maxlength={maxLength > 0 ? maxLength : null}
|
|
146
215
|
oninput={handleInput}
|
|
147
216
|
onkeydown={handleKeydown}
|
|
217
|
+
onpaste={(event) => onpaste?.(event)}
|
|
148
218
|
use:autoGrow={value}
|
|
149
219
|
></textarea>
|
|
150
220
|
|
|
151
221
|
{#if showVoice}
|
|
152
222
|
<div class="control voice" class:recording>
|
|
153
|
-
<Button onclick={() => onvoice?.()} {disabled} ariaLabel={voiceLabel}>
|
|
223
|
+
<Button onclick={() => onvoice?.()} {disabled} ariaLabel={voiceLabel} testId={voiceTestId}>
|
|
154
224
|
{#if typeof voiceIcon === 'function'}
|
|
155
225
|
{@render voiceIcon()}
|
|
156
226
|
{:else}
|
|
@@ -162,8 +232,8 @@
|
|
|
162
232
|
{/if}
|
|
163
233
|
|
|
164
234
|
{#if streaming}
|
|
165
|
-
<div class="control send stop">
|
|
166
|
-
<Button onclick={() => onstop?.()} ariaLabel={stopLabel}>
|
|
235
|
+
<div class="control send stop" data-pw={sendSlotTestId ?? null}>
|
|
236
|
+
<Button onclick={() => onstop?.()} ariaLabel={stopLabel} testId={stopTestId}>
|
|
167
237
|
{#if typeof stopIcon === 'function'}
|
|
168
238
|
{@render stopIcon()}
|
|
169
239
|
{:else}
|
|
@@ -172,9 +242,20 @@
|
|
|
172
242
|
{/if}
|
|
173
243
|
</Button>
|
|
174
244
|
</div>
|
|
245
|
+
{:else if typeof onaction === 'function' && !canSend && !recording}
|
|
246
|
+
<div class="control send action" data-pw={sendSlotTestId ?? null}>
|
|
247
|
+
<Button onclick={() => onaction()} {disabled} ariaLabel={actionLabel} testId={actionTestId}>
|
|
248
|
+
{#if typeof actionIcon === 'function'}
|
|
249
|
+
{@render actionIcon()}
|
|
250
|
+
{:else}
|
|
251
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
252
|
+
{@html micSvg}
|
|
253
|
+
{/if}
|
|
254
|
+
</Button>
|
|
255
|
+
</div>
|
|
175
256
|
{:else}
|
|
176
|
-
<div class="control send">
|
|
177
|
-
<Button onclick={submit} disabled={!canSend} ariaLabel={sendLabel}>
|
|
257
|
+
<div class="control send" data-pw={sendSlotTestId ?? null}>
|
|
258
|
+
<Button onclick={submit} disabled={!canSend} ariaLabel={sendLabel} testId={sendTestId}>
|
|
178
259
|
{#if typeof sendIcon === 'function'}
|
|
179
260
|
{@render sendIcon()}
|
|
180
261
|
{:else}
|
|
@@ -205,6 +286,10 @@
|
|
|
205
286
|
opacity: var(--chat-composer-disabled-opacity, 0.6);
|
|
206
287
|
}
|
|
207
288
|
|
|
289
|
+
.attachments-rich {
|
|
290
|
+
padding: var(--chat-composer-attachments-padding, 2px 4px 0);
|
|
291
|
+
}
|
|
292
|
+
|
|
208
293
|
.attachments {
|
|
209
294
|
display: flex;
|
|
210
295
|
flex-wrap: wrap;
|
|
@@ -233,6 +318,7 @@
|
|
|
233
318
|
background: transparent;
|
|
234
319
|
font-family: var(--chat-composer-font-family, inherit);
|
|
235
320
|
font-size: var(--chat-composer-font-size, 0.9375rem);
|
|
321
|
+
font-weight: var(--chat-composer-font-weight, 400);
|
|
236
322
|
line-height: var(--chat-composer-line-height, 1.5);
|
|
237
323
|
color: var(--chat-composer-color, #18181b);
|
|
238
324
|
padding: var(--chat-composer-input-padding, 6px 4px);
|