@r2digisolutions/components 0.16.3 → 0.17.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/components/organisms/AssistantChat/AssistantChat.svelte +36 -34
- package/dist/components/organisms/AssistantChat/AssistantChat.svelte.d.ts +2 -0
- package/dist/components/organisms/CommandPalette/CommandPalette.svelte +41 -5
- package/dist/components/organisms/CommandPalette/CommandPalette.svelte.d.ts +9 -1
- package/package.json +1 -1
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
draft?: string;
|
|
31
31
|
loading?: boolean;
|
|
32
32
|
loadingLabel?: string;
|
|
33
|
+
/** When false, input stays disabled via `loading` but the spinner bubble is hidden (parent paints status in a message). */
|
|
34
|
+
showLoadingBubble?: boolean;
|
|
33
35
|
error?: string | null;
|
|
34
36
|
voiceEnabled?: boolean;
|
|
35
37
|
voiceOutput?: boolean;
|
|
@@ -63,6 +65,7 @@
|
|
|
63
65
|
draft = $bindable(''),
|
|
64
66
|
loading = false,
|
|
65
67
|
loadingLabel = 'Procesando…',
|
|
68
|
+
showLoadingBubble = true,
|
|
66
69
|
error = null,
|
|
67
70
|
voiceEnabled = true,
|
|
68
71
|
voiceOutput = $bindable(true),
|
|
@@ -131,25 +134,25 @@
|
|
|
131
134
|
|
|
132
135
|
<div
|
|
133
136
|
class={[
|
|
134
|
-
'
|
|
135
|
-
'h-[min(560px,70vh)] w-full
|
|
137
|
+
'rounded-2xl border-border bg-surface-elevated shadow-xl flex flex-col overflow-hidden border',
|
|
138
|
+
'h-[min(560px,70vh)] w-full max-w-[420px] min-w-[320px]',
|
|
136
139
|
className
|
|
137
140
|
]}
|
|
138
141
|
role="complementary"
|
|
139
142
|
aria-label={title}
|
|
140
143
|
>
|
|
141
144
|
<!-- Header -->
|
|
142
|
-
<header class="
|
|
145
|
+
<header class="gap-2 border-border px-3 py-2.5 flex shrink-0 items-center border-b">
|
|
143
146
|
<span
|
|
144
|
-
class="
|
|
147
|
+
class="h-7 w-7 rounded-lg bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400 flex shrink-0 items-center justify-center"
|
|
145
148
|
aria-hidden="true"
|
|
146
149
|
>
|
|
147
150
|
<Sparkles size={15} strokeWidth={2} />
|
|
148
151
|
</span>
|
|
149
152
|
<div class="min-w-0 flex-1">
|
|
150
|
-
<p class="
|
|
153
|
+
<p class="text-sm font-semibold text-primary truncate">{title}</p>
|
|
151
154
|
{#if statusLabel}
|
|
152
|
-
<p class="
|
|
155
|
+
<p class="text-xs text-muted truncate">{statusLabel}</p>
|
|
153
156
|
{/if}
|
|
154
157
|
</div>
|
|
155
158
|
<IconButton variant="ghost" size="xs" label={newChatLabel} onclick={() => onnewchat?.()}>
|
|
@@ -161,22 +164,22 @@
|
|
|
161
164
|
</header>
|
|
162
165
|
|
|
163
166
|
<!-- Body -->
|
|
164
|
-
<div bind:this={scrollEl} class="min-h-0 flex-1 overflow-y-auto
|
|
167
|
+
<div bind:this={scrollEl} class="min-h-0 px-3 py-3 flex-1 overflow-y-auto">
|
|
165
168
|
{#if isEmpty}
|
|
166
|
-
<div class="flex flex-col items-center justify-center
|
|
169
|
+
<div class="gap-4 py-8 flex flex-col items-center justify-center text-center">
|
|
167
170
|
<span
|
|
168
|
-
class="
|
|
171
|
+
class="h-12 w-12 rounded-2xl bg-brand-50 text-brand-500 dark:bg-brand-950/30 dark:text-brand-400 flex items-center justify-center"
|
|
169
172
|
aria-hidden="true"
|
|
170
173
|
>
|
|
171
174
|
<Sparkles size={24} strokeWidth={1.5} />
|
|
172
175
|
</span>
|
|
173
176
|
<p class="text-sm text-muted">{emptyHint}</p>
|
|
174
177
|
{#if suggestions.length}
|
|
175
|
-
<div class="flex flex-wrap justify-center
|
|
178
|
+
<div class="gap-2 flex flex-wrap justify-center">
|
|
176
179
|
{#each suggestions as suggestion (suggestion)}
|
|
177
180
|
<button
|
|
178
181
|
type="button"
|
|
179
|
-
class="
|
|
182
|
+
class="border-border bg-surface px-3 py-1.5 text-xs text-secondary hover:border-brand-400 hover:text-brand-600 dark:hover:text-brand-400 rounded-full border transition-colors"
|
|
180
183
|
onclick={() => handleSuggestion(suggestion)}
|
|
181
184
|
>
|
|
182
185
|
{suggestion}
|
|
@@ -186,27 +189,27 @@
|
|
|
186
189
|
{/if}
|
|
187
190
|
</div>
|
|
188
191
|
{:else}
|
|
189
|
-
<div class="flex flex-col
|
|
192
|
+
<div class="gap-3 flex flex-col">
|
|
190
193
|
{#each messages as message (message.id)}
|
|
191
194
|
{#if message.role === 'user'}
|
|
192
195
|
<div class="flex justify-end">
|
|
193
196
|
<div
|
|
194
|
-
class="
|
|
197
|
+
class="rounded-2xl rounded-br-sm bg-brand-600 px-3.5 py-2.5 text-sm leading-relaxed text-white max-w-[80%]"
|
|
195
198
|
>
|
|
196
199
|
{message.content}
|
|
197
200
|
</div>
|
|
198
201
|
</div>
|
|
199
202
|
{:else}
|
|
200
|
-
<div class="flex flex-col
|
|
201
|
-
<div class="flex items-start
|
|
203
|
+
<div class="gap-1.5 flex flex-col">
|
|
204
|
+
<div class="gap-2 flex items-start">
|
|
202
205
|
<span
|
|
203
|
-
class="mt-0.5
|
|
206
|
+
class="mt-0.5 h-6 w-6 bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400 flex shrink-0 items-center justify-center rounded-full"
|
|
204
207
|
aria-hidden="true"
|
|
205
208
|
>
|
|
206
209
|
<Sparkles size={12} strokeWidth={2} />
|
|
207
210
|
</span>
|
|
208
211
|
<div
|
|
209
|
-
class="
|
|
212
|
+
class="rounded-2xl rounded-bl-sm bg-surface-overlay px-3.5 py-2.5 text-sm leading-relaxed text-primary max-w-[calc(100%-2rem)]"
|
|
210
213
|
>
|
|
211
214
|
{message.content}
|
|
212
215
|
</div>
|
|
@@ -220,48 +223,43 @@
|
|
|
220
223
|
{/if}
|
|
221
224
|
{/each}
|
|
222
225
|
|
|
223
|
-
{#if loading}
|
|
224
|
-
<div class="flex items-center
|
|
226
|
+
{#if loading && showLoadingBubble}
|
|
227
|
+
<div class="gap-2 flex items-center">
|
|
225
228
|
<span
|
|
226
|
-
class="
|
|
229
|
+
class="h-6 w-6 bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400 flex shrink-0 items-center justify-center rounded-full"
|
|
227
230
|
aria-hidden="true"
|
|
228
231
|
>
|
|
229
232
|
<Sparkles size={12} strokeWidth={2} />
|
|
230
233
|
</span>
|
|
231
234
|
<div
|
|
232
|
-
class="
|
|
235
|
+
class="gap-2 rounded-2xl rounded-bl-sm bg-surface-overlay px-3.5 py-2.5 flex items-center"
|
|
233
236
|
>
|
|
234
237
|
<LoaderCircle size={14} strokeWidth={2} class="animate-spin text-muted" />
|
|
235
238
|
<span class="text-xs text-muted">{loadingLabel}</span>
|
|
236
239
|
</div>
|
|
237
240
|
</div>
|
|
238
241
|
{/if}
|
|
239
|
-
|
|
240
|
-
{#if error}
|
|
241
|
-
<div
|
|
242
|
-
class="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-600 dark:border-red-800/40 dark:bg-red-950/20 dark:text-red-400"
|
|
243
|
-
>
|
|
244
|
-
{error}
|
|
245
|
-
</div>
|
|
246
|
-
{/if}
|
|
247
242
|
</div>
|
|
248
243
|
{/if}
|
|
249
244
|
</div>
|
|
250
245
|
|
|
251
246
|
<!-- Footer -->
|
|
252
|
-
<footer class="
|
|
253
|
-
<div class="flex items-center
|
|
247
|
+
<footer class="border-border px-3 py-2.5 shrink-0 border-t">
|
|
248
|
+
<div class="gap-1.5 flex items-center">
|
|
254
249
|
<input
|
|
255
250
|
type="text"
|
|
256
251
|
bind:value={draft}
|
|
257
|
-
{placeholder}
|
|
258
|
-
class=
|
|
252
|
+
placeholder={listening ? statusLabel || placeholder : placeholder}
|
|
253
|
+
class={[
|
|
254
|
+
'h-9 min-w-0 rounded-xl bg-surface px-3 text-sm text-primary placeholder:text-muted focus:border-brand-400 focus:ring-brand-500/20 flex-1 border transition-shadow outline-none focus:ring-2',
|
|
255
|
+
listening ? 'border-red-400 ring-red-500/20 ring-2' : 'border-border'
|
|
256
|
+
]}
|
|
259
257
|
disabled={loading}
|
|
260
258
|
onkeydown={handleKeydown}
|
|
261
259
|
aria-label={placeholder}
|
|
262
260
|
/>
|
|
263
261
|
|
|
264
|
-
<div class="flex shrink-0 items-center
|
|
262
|
+
<div class="gap-0.5 flex shrink-0 items-center">
|
|
265
263
|
{#if voiceEnabled}
|
|
266
264
|
<IconButton
|
|
267
265
|
variant="ghost"
|
|
@@ -302,6 +300,10 @@
|
|
|
302
300
|
</div>
|
|
303
301
|
</div>
|
|
304
302
|
|
|
303
|
+
{#if error}
|
|
304
|
+
<p class="mt-1.5 px-1 text-xs text-red-600 dark:text-red-400">{error}</p>
|
|
305
|
+
{/if}
|
|
306
|
+
|
|
305
307
|
{#if footerExtra}
|
|
306
308
|
{@render footerExtra()}
|
|
307
309
|
{/if}
|
|
@@ -15,6 +15,8 @@ interface AssistantChatProps {
|
|
|
15
15
|
draft?: string;
|
|
16
16
|
loading?: boolean;
|
|
17
17
|
loadingLabel?: string;
|
|
18
|
+
/** When false, input stays disabled via `loading` but the spinner bubble is hidden (parent paints status in a message). */
|
|
19
|
+
showLoadingBubble?: boolean;
|
|
18
20
|
error?: string | null;
|
|
19
21
|
voiceEnabled?: boolean;
|
|
20
22
|
voiceOutput?: boolean;
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import type { Component } from 'svelte';
|
|
3
3
|
import { untrack } from 'svelte';
|
|
4
4
|
import Kbd from '../../atoms/Kbd/Kbd.svelte';
|
|
5
|
+
import IconButton from '../../atoms/IconButton/IconButton.svelte';
|
|
5
6
|
import { i18n } from '../../../utils/i18n.svelte';
|
|
7
|
+
import { Mic, MicOff } from '@lucide/svelte';
|
|
6
8
|
|
|
7
9
|
export type CommandIcon = Component<{
|
|
8
10
|
class?: string;
|
|
@@ -24,29 +26,42 @@
|
|
|
24
26
|
|
|
25
27
|
interface CommandPaletteProps {
|
|
26
28
|
open?: boolean;
|
|
29
|
+
query?: string;
|
|
27
30
|
items?: CommandItem[];
|
|
28
31
|
placeholder?: string;
|
|
29
32
|
emptyLabel?: string;
|
|
30
33
|
loading?: boolean;
|
|
34
|
+
voiceEnabled?: boolean;
|
|
35
|
+
listening?: boolean;
|
|
36
|
+
/** When false, opening keeps the current query (voice dictation). */
|
|
37
|
+
resetQueryOnOpen?: boolean;
|
|
38
|
+
micLabel?: string;
|
|
39
|
+
stopMicLabel?: string;
|
|
31
40
|
class?: string;
|
|
32
41
|
onselect?: (item: CommandItem) => void;
|
|
33
42
|
onclose?: () => void;
|
|
34
43
|
onquery?: (query: string) => void;
|
|
44
|
+
onmicclick?: () => void;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
let {
|
|
38
48
|
open = $bindable(false),
|
|
49
|
+
query = $bindable(''),
|
|
39
50
|
items = [],
|
|
40
51
|
placeholder,
|
|
41
52
|
emptyLabel,
|
|
42
53
|
loading = false,
|
|
54
|
+
voiceEnabled = false,
|
|
55
|
+
listening = false,
|
|
56
|
+
resetQueryOnOpen = true,
|
|
57
|
+
micLabel = 'Hablar',
|
|
58
|
+
stopMicLabel = 'Parar micrófono',
|
|
43
59
|
class: className = '',
|
|
44
60
|
onselect,
|
|
45
61
|
onclose,
|
|
46
|
-
onquery
|
|
62
|
+
onquery,
|
|
63
|
+
onmicclick
|
|
47
64
|
}: CommandPaletteProps = $props();
|
|
48
|
-
|
|
49
|
-
let query = $state('');
|
|
50
65
|
let activeIndex = $state(0);
|
|
51
66
|
let dialogEl = $state<HTMLDialogElement | null>(null);
|
|
52
67
|
let listEl = $state<HTMLDivElement | null>(null);
|
|
@@ -129,9 +144,11 @@
|
|
|
129
144
|
$effect(() => {
|
|
130
145
|
if (!dialogEl) return;
|
|
131
146
|
if (open && !dialogEl.open) {
|
|
132
|
-
|
|
147
|
+
if (resetQueryOnOpen) {
|
|
148
|
+
query = '';
|
|
149
|
+
untrack(() => onquery?.(''));
|
|
150
|
+
}
|
|
133
151
|
activeIndex = Math.max(0, firstEnabledIndex());
|
|
134
|
-
untrack(() => onquery?.(''));
|
|
135
152
|
ignoreBackdropUntil = Date.now() + 400;
|
|
136
153
|
dialogEl.showModal();
|
|
137
154
|
} else if (!open && dialogEl.open) {
|
|
@@ -232,6 +249,25 @@
|
|
|
232
249
|
oninput={onInput}
|
|
233
250
|
onkeydown={onKeydown}
|
|
234
251
|
/>
|
|
252
|
+
{#if voiceEnabled}
|
|
253
|
+
<IconButton
|
|
254
|
+
variant="ghost"
|
|
255
|
+
size="xs"
|
|
256
|
+
label={listening ? stopMicLabel : micLabel}
|
|
257
|
+
aria-pressed={listening}
|
|
258
|
+
onclick={(e) => {
|
|
259
|
+
e.stopPropagation();
|
|
260
|
+
onmicclick?.();
|
|
261
|
+
}}
|
|
262
|
+
class={listening ? 'text-red-500 hover:text-red-600' : 'text-muted'}
|
|
263
|
+
>
|
|
264
|
+
{#if listening}
|
|
265
|
+
<MicOff size={16} strokeWidth={2} />
|
|
266
|
+
{:else}
|
|
267
|
+
<Mic size={16} strokeWidth={2} />
|
|
268
|
+
{/if}
|
|
269
|
+
</IconButton>
|
|
270
|
+
{/if}
|
|
235
271
|
{#if loading}
|
|
236
272
|
<span
|
|
237
273
|
class="h-4 w-4 animate-spin border-border border-t-brand-500 shrink-0 rounded-full border-2"
|
|
@@ -17,15 +17,23 @@ export interface CommandItem {
|
|
|
17
17
|
}
|
|
18
18
|
interface CommandPaletteProps {
|
|
19
19
|
open?: boolean;
|
|
20
|
+
query?: string;
|
|
20
21
|
items?: CommandItem[];
|
|
21
22
|
placeholder?: string;
|
|
22
23
|
emptyLabel?: string;
|
|
23
24
|
loading?: boolean;
|
|
25
|
+
voiceEnabled?: boolean;
|
|
26
|
+
listening?: boolean;
|
|
27
|
+
/** When false, opening keeps the current query (voice dictation). */
|
|
28
|
+
resetQueryOnOpen?: boolean;
|
|
29
|
+
micLabel?: string;
|
|
30
|
+
stopMicLabel?: string;
|
|
24
31
|
class?: string;
|
|
25
32
|
onselect?: (item: CommandItem) => void;
|
|
26
33
|
onclose?: () => void;
|
|
27
34
|
onquery?: (query: string) => void;
|
|
35
|
+
onmicclick?: () => void;
|
|
28
36
|
}
|
|
29
|
-
declare const CommandPalette: Component<CommandPaletteProps, {}, "open">;
|
|
37
|
+
declare const CommandPalette: Component<CommandPaletteProps, {}, "open" | "query">;
|
|
30
38
|
type CommandPalette = ReturnType<typeof CommandPalette>;
|
|
31
39
|
export default CommandPalette;
|