@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.
@@ -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
- 'flex flex-col overflow-hidden rounded-2xl border border-border bg-surface-elevated shadow-xl',
135
- 'h-[min(560px,70vh)] w-full min-w-[320px] max-w-[420px]',
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="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2.5">
145
+ <header class="gap-2 border-border px-3 py-2.5 flex shrink-0 items-center border-b">
143
146
  <span
144
- class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400"
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="truncate text-sm font-semibold text-primary">{title}</p>
153
+ <p class="text-sm font-semibold text-primary truncate">{title}</p>
151
154
  {#if statusLabel}
152
- <p class="truncate text-xs text-muted">{statusLabel}</p>
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 px-3 py-3">
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 gap-4 py-8 text-center">
169
+ <div class="gap-4 py-8 flex flex-col items-center justify-center text-center">
167
170
  <span
168
- class="flex h-12 w-12 items-center justify-center rounded-2xl bg-brand-50 text-brand-500 dark:bg-brand-950/30 dark:text-brand-400"
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 gap-2">
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="rounded-full border border-border bg-surface px-3 py-1.5 text-xs text-secondary transition-colors hover:border-brand-400 hover:text-brand-600 dark:hover:text-brand-400"
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 gap-3">
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="max-w-[80%] rounded-2xl rounded-br-sm bg-brand-600 px-3.5 py-2.5 text-sm leading-relaxed text-white"
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 gap-1.5">
201
- <div class="flex items-start gap-2">
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 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400"
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="max-w-[calc(100%-2rem)] rounded-2xl rounded-bl-sm bg-surface-overlay px-3.5 py-2.5 text-sm leading-relaxed text-primary"
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 gap-2">
226
+ {#if loading && showLoadingBubble}
227
+ <div class="gap-2 flex items-center">
225
228
  <span
226
- class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400"
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="flex items-center gap-2 rounded-2xl rounded-bl-sm bg-surface-overlay px-3.5 py-2.5"
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="shrink-0 border-t border-border px-3 py-2.5">
253
- <div class="flex items-center gap-1.5">
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="h-9 min-w-0 flex-1 rounded-xl border border-border bg-surface px-3 text-sm text-primary outline-none transition-shadow placeholder:text-muted focus:border-brand-400 focus:ring-2 focus:ring-brand-500/20"
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 gap-0.5">
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
- query = '';
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.16.3",
3
+ "version": "0.17.0",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",