@r2digisolutions/components 0.15.3 → 0.16.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/molecules/Popover/Popover.svelte +1 -1
- package/dist/components/organisms/AssistantChat/AssistantChat.svelte +308 -0
- package/dist/components/organisms/AssistantChat/AssistantChat.svelte.d.ts +40 -0
- package/dist/components/organisms/AssistantFab/AssistantFab.svelte +27 -0
- package/dist/components/organisms/AssistantFab/AssistantFab.svelte.d.ts +8 -0
- package/dist/components/organisms/CommandPalette/CommandPalette.svelte +236 -130
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
ontoggle={onToggle}
|
|
231
231
|
style={panelStyle}
|
|
232
232
|
class={[
|
|
233
|
-
'popover-panel m-0 rounded-xl border-border bg-surface-elevated p-3 shadow-xl max-w-[min(
|
|
233
|
+
'popover-panel m-0 rounded-xl border-border bg-surface-elevated p-3 shadow-xl max-w-[min(26rem,calc(100vw-1rem))] border outline-none',
|
|
234
234
|
panelClass
|
|
235
235
|
]}
|
|
236
236
|
>
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import IconButton from '../../atoms/IconButton/IconButton.svelte';
|
|
4
|
+
import {
|
|
5
|
+
Sparkles,
|
|
6
|
+
X,
|
|
7
|
+
Send,
|
|
8
|
+
Mic,
|
|
9
|
+
MicOff,
|
|
10
|
+
Volume2,
|
|
11
|
+
VolumeX,
|
|
12
|
+
LoaderCircle,
|
|
13
|
+
RotateCcw
|
|
14
|
+
} from '@lucide/svelte';
|
|
15
|
+
|
|
16
|
+
export interface AssistantMessage {
|
|
17
|
+
id: string;
|
|
18
|
+
role: 'user' | 'assistant';
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AssistantChatProps {
|
|
23
|
+
open?: boolean;
|
|
24
|
+
title?: string;
|
|
25
|
+
statusLabel?: string;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
emptyHint?: string;
|
|
28
|
+
suggestions?: string[];
|
|
29
|
+
messages?: AssistantMessage[];
|
|
30
|
+
draft?: string;
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
error?: string | null;
|
|
33
|
+
voiceEnabled?: boolean;
|
|
34
|
+
voiceOutput?: boolean;
|
|
35
|
+
listening?: boolean;
|
|
36
|
+
newChatLabel?: string;
|
|
37
|
+
closeLabel?: string;
|
|
38
|
+
sendLabel?: string;
|
|
39
|
+
micLabel?: string;
|
|
40
|
+
stopMicLabel?: string;
|
|
41
|
+
voiceOnLabel?: string;
|
|
42
|
+
voiceOffLabel?: string;
|
|
43
|
+
class?: string;
|
|
44
|
+
result?: Snippet<[AssistantMessage]>;
|
|
45
|
+
footerExtra?: Snippet;
|
|
46
|
+
onsend?: () => void;
|
|
47
|
+
onclose?: () => void;
|
|
48
|
+
onnewchat?: () => void;
|
|
49
|
+
onmicclick?: () => void;
|
|
50
|
+
onvoiceoutputchange?: (on: boolean) => void;
|
|
51
|
+
onsuggestion?: (text: string) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
open = $bindable(false),
|
|
56
|
+
title = 'Asistente',
|
|
57
|
+
statusLabel,
|
|
58
|
+
placeholder = 'Escribe un mensaje…',
|
|
59
|
+
emptyHint = '¿En qué puedo ayudarte?',
|
|
60
|
+
suggestions = [],
|
|
61
|
+
messages = [],
|
|
62
|
+
draft = $bindable(''),
|
|
63
|
+
loading = false,
|
|
64
|
+
error = null,
|
|
65
|
+
voiceEnabled = true,
|
|
66
|
+
voiceOutput = $bindable(true),
|
|
67
|
+
listening = false,
|
|
68
|
+
newChatLabel = 'Nueva conversación',
|
|
69
|
+
closeLabel = 'Cerrar',
|
|
70
|
+
sendLabel = 'Enviar',
|
|
71
|
+
micLabel = 'Activar micrófono',
|
|
72
|
+
stopMicLabel = 'Detener micrófono',
|
|
73
|
+
voiceOnLabel = 'Voz activada',
|
|
74
|
+
voiceOffLabel = 'Voz desactivada',
|
|
75
|
+
class: className = '',
|
|
76
|
+
result,
|
|
77
|
+
footerExtra,
|
|
78
|
+
onsend,
|
|
79
|
+
onclose,
|
|
80
|
+
onnewchat,
|
|
81
|
+
onmicclick,
|
|
82
|
+
onvoiceoutputchange,
|
|
83
|
+
onsuggestion
|
|
84
|
+
}: AssistantChatProps = $props();
|
|
85
|
+
|
|
86
|
+
let scrollEl = $state<HTMLDivElement | null>(null);
|
|
87
|
+
|
|
88
|
+
const isEmpty = $derived(messages.length === 0 && !loading);
|
|
89
|
+
const canSend = $derived(draft.trim().length > 0 && !loading);
|
|
90
|
+
|
|
91
|
+
$effect(() => {
|
|
92
|
+
void messages.length;
|
|
93
|
+
void loading;
|
|
94
|
+
const el = scrollEl;
|
|
95
|
+
if (!el) return;
|
|
96
|
+
const raf = requestAnimationFrame(() => {
|
|
97
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
98
|
+
});
|
|
99
|
+
return () => cancelAnimationFrame(raf);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function handleClose() {
|
|
103
|
+
open = false;
|
|
104
|
+
onclose?.();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handleSend() {
|
|
108
|
+
if (!canSend) return;
|
|
109
|
+
onsend?.();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
113
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
handleSend();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function handleSuggestion(text: string) {
|
|
120
|
+
draft = text;
|
|
121
|
+
onsuggestion?.(text);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toggleVoice() {
|
|
125
|
+
voiceOutput = !voiceOutput;
|
|
126
|
+
onvoiceoutputchange?.(voiceOutput);
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<div
|
|
131
|
+
class={[
|
|
132
|
+
'flex flex-col overflow-hidden rounded-2xl border border-border bg-surface-elevated shadow-xl',
|
|
133
|
+
'h-[min(560px,70vh)] w-full min-w-[320px] max-w-[420px]',
|
|
134
|
+
className
|
|
135
|
+
]}
|
|
136
|
+
role="complementary"
|
|
137
|
+
aria-label={title}
|
|
138
|
+
>
|
|
139
|
+
<!-- Header -->
|
|
140
|
+
<header class="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2.5">
|
|
141
|
+
<span
|
|
142
|
+
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"
|
|
143
|
+
aria-hidden="true"
|
|
144
|
+
>
|
|
145
|
+
<Sparkles size={15} strokeWidth={2} />
|
|
146
|
+
</span>
|
|
147
|
+
<div class="min-w-0 flex-1">
|
|
148
|
+
<p class="truncate text-sm font-semibold text-primary">{title}</p>
|
|
149
|
+
{#if statusLabel}
|
|
150
|
+
<p class="truncate text-xs text-muted">{statusLabel}</p>
|
|
151
|
+
{/if}
|
|
152
|
+
</div>
|
|
153
|
+
<IconButton variant="ghost" size="xs" label={newChatLabel} onclick={() => onnewchat?.()}>
|
|
154
|
+
<RotateCcw size={14} strokeWidth={2} />
|
|
155
|
+
</IconButton>
|
|
156
|
+
<IconButton variant="ghost" size="xs" label={closeLabel} onclick={handleClose}>
|
|
157
|
+
<X size={14} strokeWidth={2} />
|
|
158
|
+
</IconButton>
|
|
159
|
+
</header>
|
|
160
|
+
|
|
161
|
+
<!-- Body -->
|
|
162
|
+
<div bind:this={scrollEl} class="min-h-0 flex-1 overflow-y-auto px-3 py-3">
|
|
163
|
+
{#if isEmpty}
|
|
164
|
+
<div class="flex flex-col items-center justify-center gap-4 py-8 text-center">
|
|
165
|
+
<span
|
|
166
|
+
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"
|
|
167
|
+
aria-hidden="true"
|
|
168
|
+
>
|
|
169
|
+
<Sparkles size={24} strokeWidth={1.5} />
|
|
170
|
+
</span>
|
|
171
|
+
<p class="text-sm text-muted">{emptyHint}</p>
|
|
172
|
+
{#if suggestions.length}
|
|
173
|
+
<div class="flex flex-wrap justify-center gap-2">
|
|
174
|
+
{#each suggestions as suggestion (suggestion)}
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
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"
|
|
178
|
+
onclick={() => handleSuggestion(suggestion)}
|
|
179
|
+
>
|
|
180
|
+
{suggestion}
|
|
181
|
+
</button>
|
|
182
|
+
{/each}
|
|
183
|
+
</div>
|
|
184
|
+
{/if}
|
|
185
|
+
</div>
|
|
186
|
+
{:else}
|
|
187
|
+
<div class="flex flex-col gap-3">
|
|
188
|
+
{#each messages as message (message.id)}
|
|
189
|
+
{#if message.role === 'user'}
|
|
190
|
+
<div class="flex justify-end">
|
|
191
|
+
<div
|
|
192
|
+
class="max-w-[80%] rounded-2xl rounded-br-sm bg-brand-600 px-3.5 py-2.5 text-sm leading-relaxed text-white"
|
|
193
|
+
>
|
|
194
|
+
{message.content}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
{:else}
|
|
198
|
+
<div class="flex flex-col gap-1.5">
|
|
199
|
+
<div class="flex items-start gap-2">
|
|
200
|
+
<span
|
|
201
|
+
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"
|
|
202
|
+
aria-hidden="true"
|
|
203
|
+
>
|
|
204
|
+
<Sparkles size={12} strokeWidth={2} />
|
|
205
|
+
</span>
|
|
206
|
+
<div
|
|
207
|
+
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"
|
|
208
|
+
>
|
|
209
|
+
{message.content}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
{#if result}
|
|
213
|
+
<div class="ml-8">
|
|
214
|
+
{@render result(message)}
|
|
215
|
+
</div>
|
|
216
|
+
{/if}
|
|
217
|
+
</div>
|
|
218
|
+
{/if}
|
|
219
|
+
{/each}
|
|
220
|
+
|
|
221
|
+
{#if loading}
|
|
222
|
+
<div class="flex items-center gap-2">
|
|
223
|
+
<span
|
|
224
|
+
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"
|
|
225
|
+
aria-hidden="true"
|
|
226
|
+
>
|
|
227
|
+
<Sparkles size={12} strokeWidth={2} />
|
|
228
|
+
</span>
|
|
229
|
+
<div
|
|
230
|
+
class="flex items-center gap-2 rounded-2xl rounded-bl-sm bg-surface-overlay px-3.5 py-2.5"
|
|
231
|
+
>
|
|
232
|
+
<LoaderCircle size={14} strokeWidth={2} class="animate-spin text-muted" />
|
|
233
|
+
<span class="text-xs text-muted">Procesando…</span>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
{/if}
|
|
237
|
+
|
|
238
|
+
{#if error}
|
|
239
|
+
<div
|
|
240
|
+
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"
|
|
241
|
+
>
|
|
242
|
+
{error}
|
|
243
|
+
</div>
|
|
244
|
+
{/if}
|
|
245
|
+
</div>
|
|
246
|
+
{/if}
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<!-- Footer -->
|
|
250
|
+
<footer class="shrink-0 border-t border-border px-3 py-2.5">
|
|
251
|
+
<div class="flex items-center gap-2">
|
|
252
|
+
{#if voiceEnabled}
|
|
253
|
+
<IconButton
|
|
254
|
+
variant="ghost"
|
|
255
|
+
size="sm"
|
|
256
|
+
label={listening ? stopMicLabel : micLabel}
|
|
257
|
+
onclick={() => onmicclick?.()}
|
|
258
|
+
class={listening ? 'text-red-500 hover:text-red-600' : ''}
|
|
259
|
+
>
|
|
260
|
+
{#if listening}
|
|
261
|
+
<MicOff size={16} strokeWidth={2} />
|
|
262
|
+
{:else}
|
|
263
|
+
<Mic size={16} strokeWidth={2} />
|
|
264
|
+
{/if}
|
|
265
|
+
</IconButton>
|
|
266
|
+
{/if}
|
|
267
|
+
|
|
268
|
+
<input
|
|
269
|
+
type="text"
|
|
270
|
+
bind:value={draft}
|
|
271
|
+
{placeholder}
|
|
272
|
+
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"
|
|
273
|
+
disabled={loading}
|
|
274
|
+
onkeydown={handleKeydown}
|
|
275
|
+
aria-label={placeholder}
|
|
276
|
+
/>
|
|
277
|
+
|
|
278
|
+
{#if voiceEnabled}
|
|
279
|
+
<IconButton
|
|
280
|
+
variant="ghost"
|
|
281
|
+
size="sm"
|
|
282
|
+
label={voiceOutput ? voiceOnLabel : voiceOffLabel}
|
|
283
|
+
onclick={toggleVoice}
|
|
284
|
+
>
|
|
285
|
+
{#if voiceOutput}
|
|
286
|
+
<Volume2 size={16} strokeWidth={2} />
|
|
287
|
+
{:else}
|
|
288
|
+
<VolumeX size={16} strokeWidth={2} />
|
|
289
|
+
{/if}
|
|
290
|
+
</IconButton>
|
|
291
|
+
{/if}
|
|
292
|
+
|
|
293
|
+
<IconButton
|
|
294
|
+
variant={canSend ? 'primary' : 'ghost'}
|
|
295
|
+
size="sm"
|
|
296
|
+
label={sendLabel}
|
|
297
|
+
disabled={!canSend}
|
|
298
|
+
onclick={handleSend}
|
|
299
|
+
>
|
|
300
|
+
<Send size={16} strokeWidth={2} />
|
|
301
|
+
</IconButton>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{#if footerExtra}
|
|
305
|
+
{@render footerExtra()}
|
|
306
|
+
{/if}
|
|
307
|
+
</footer>
|
|
308
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export interface AssistantMessage {
|
|
3
|
+
id: string;
|
|
4
|
+
role: 'user' | 'assistant';
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
interface AssistantChatProps {
|
|
8
|
+
open?: boolean;
|
|
9
|
+
title?: string;
|
|
10
|
+
statusLabel?: string;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
emptyHint?: string;
|
|
13
|
+
suggestions?: string[];
|
|
14
|
+
messages?: AssistantMessage[];
|
|
15
|
+
draft?: string;
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
error?: string | null;
|
|
18
|
+
voiceEnabled?: boolean;
|
|
19
|
+
voiceOutput?: boolean;
|
|
20
|
+
listening?: boolean;
|
|
21
|
+
newChatLabel?: string;
|
|
22
|
+
closeLabel?: string;
|
|
23
|
+
sendLabel?: string;
|
|
24
|
+
micLabel?: string;
|
|
25
|
+
stopMicLabel?: string;
|
|
26
|
+
voiceOnLabel?: string;
|
|
27
|
+
voiceOffLabel?: string;
|
|
28
|
+
class?: string;
|
|
29
|
+
result?: Snippet<[AssistantMessage]>;
|
|
30
|
+
footerExtra?: Snippet;
|
|
31
|
+
onsend?: () => void;
|
|
32
|
+
onclose?: () => void;
|
|
33
|
+
onnewchat?: () => void;
|
|
34
|
+
onmicclick?: () => void;
|
|
35
|
+
onvoiceoutputchange?: (on: boolean) => void;
|
|
36
|
+
onsuggestion?: (text: string) => void;
|
|
37
|
+
}
|
|
38
|
+
declare const AssistantChat: import("svelte").Component<AssistantChatProps, {}, "open" | "draft" | "voiceOutput">;
|
|
39
|
+
type AssistantChat = ReturnType<typeof AssistantChat>;
|
|
40
|
+
export default AssistantChat;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import IconButton from '../../atoms/IconButton/IconButton.svelte';
|
|
3
|
+
import { Sparkles } from '@lucide/svelte';
|
|
4
|
+
|
|
5
|
+
interface AssistantFabProps {
|
|
6
|
+
label?: string;
|
|
7
|
+
onclick?: () => void;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
label = 'Abrir asistente',
|
|
13
|
+
onclick,
|
|
14
|
+
class: className = ''
|
|
15
|
+
}: AssistantFabProps = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<IconButton
|
|
19
|
+
variant="primary"
|
|
20
|
+
size="xl"
|
|
21
|
+
rounded
|
|
22
|
+
{label}
|
|
23
|
+
onclick={() => onclick?.()}
|
|
24
|
+
class={['shadow-lg shadow-brand-500/30', className].filter(Boolean).join(' ')}
|
|
25
|
+
>
|
|
26
|
+
<Sparkles size={22} strokeWidth={2} />
|
|
27
|
+
</IconButton>
|
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
|
|
49
49
|
let query = $state('');
|
|
50
50
|
let activeIndex = $state(0);
|
|
51
|
+
let dialogEl = $state<HTMLDialogElement | null>(null);
|
|
52
|
+
let listEl = $state<HTMLDivElement | null>(null);
|
|
53
|
+
/** El click que abre la paleta llega al ::backdrop y la cerraría al instante. */
|
|
54
|
+
let ignoreBackdropUntil = 0;
|
|
55
|
+
const listId = 'command-palette-list';
|
|
51
56
|
|
|
52
57
|
const resolvedPlaceholder = $derived(placeholder ?? i18n.t('commandPalettePlaceholder'));
|
|
53
58
|
const resolvedEmpty = $derived(emptyLabel ?? i18n.t('noResults'));
|
|
@@ -72,36 +77,89 @@
|
|
|
72
77
|
});
|
|
73
78
|
|
|
74
79
|
const groups = $derived.by(() => {
|
|
75
|
-
const map = new Map<string, CommandItem[]>();
|
|
76
|
-
|
|
80
|
+
const map = new Map<string, { item: CommandItem; index: number }[]>();
|
|
81
|
+
filtered.forEach((item, index) => {
|
|
77
82
|
const key = item.group || dialogLabel;
|
|
78
83
|
const list = map.get(key) ?? [];
|
|
79
|
-
list.push(item);
|
|
84
|
+
list.push({ item, index });
|
|
80
85
|
map.set(key, list);
|
|
81
|
-
}
|
|
86
|
+
});
|
|
82
87
|
return [...map.entries()];
|
|
83
88
|
});
|
|
84
89
|
|
|
85
90
|
const flat = $derived(filtered);
|
|
86
91
|
const showIconColumn = $derived(flat.some((item) => item.icon));
|
|
92
|
+
const activeItem = $derived(flat[activeIndex] ?? null);
|
|
93
|
+
const activeOptionId = $derived(activeItem ? optionId(activeItem.id) : undefined);
|
|
94
|
+
|
|
95
|
+
function optionId(id: string) {
|
|
96
|
+
return `${listId}-${id}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function firstEnabledIndex() {
|
|
100
|
+
return flat.findIndex((item) => !item.disabled);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function nextEnabled(from: number, dir: 1 | -1) {
|
|
104
|
+
if (!flat.length) return 0;
|
|
105
|
+
let i = from;
|
|
106
|
+
for (let n = 0; n < flat.length; n++) {
|
|
107
|
+
i = (i + dir + flat.length) % flat.length;
|
|
108
|
+
if (!flat[i]?.disabled) return i;
|
|
109
|
+
}
|
|
110
|
+
return from;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Desplaza solo el listado, no el <dialog> (scrollIntoView arrastra ancestros). */
|
|
114
|
+
function keepVisible(node: HTMLElement) {
|
|
115
|
+
const frame = requestAnimationFrame(() => {
|
|
116
|
+
const list = listEl;
|
|
117
|
+
if (!list) return;
|
|
118
|
+
const listRect = list.getBoundingClientRect();
|
|
119
|
+
const rect = node.getBoundingClientRect();
|
|
120
|
+
if (rect.bottom > listRect.bottom) {
|
|
121
|
+
list.scrollTop += rect.bottom - listRect.bottom;
|
|
122
|
+
} else if (rect.top < listRect.top) {
|
|
123
|
+
list.scrollTop -= listRect.top - rect.top;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return () => cancelAnimationFrame(frame);
|
|
127
|
+
}
|
|
87
128
|
|
|
88
129
|
$effect(() => {
|
|
89
|
-
if (
|
|
130
|
+
if (!dialogEl) return;
|
|
131
|
+
if (open && !dialogEl.open) {
|
|
90
132
|
query = '';
|
|
91
|
-
activeIndex = 0;
|
|
133
|
+
activeIndex = Math.max(0, firstEnabledIndex());
|
|
92
134
|
untrack(() => onquery?.(''));
|
|
135
|
+
ignoreBackdropUntil = Date.now() + 400;
|
|
136
|
+
dialogEl.showModal();
|
|
137
|
+
} else if (!open && dialogEl.open) {
|
|
138
|
+
dialogEl.close();
|
|
93
139
|
}
|
|
94
140
|
});
|
|
95
141
|
|
|
96
|
-
function focusInput(node: HTMLInputElement) {
|
|
97
|
-
queueMicrotask(() => node.focus());
|
|
98
|
-
}
|
|
99
|
-
|
|
100
142
|
function close() {
|
|
101
143
|
open = false;
|
|
102
144
|
onclose?.();
|
|
103
145
|
}
|
|
104
146
|
|
|
147
|
+
function handleDialogClose() {
|
|
148
|
+
if (open) close();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handleBackdropClick(event: MouseEvent) {
|
|
152
|
+
if (!dialogEl) return;
|
|
153
|
+
if (Date.now() < ignoreBackdropUntil) return;
|
|
154
|
+
const rect = dialogEl.getBoundingClientRect();
|
|
155
|
+
const inside =
|
|
156
|
+
event.clientX >= rect.left &&
|
|
157
|
+
event.clientX <= rect.right &&
|
|
158
|
+
event.clientY >= rect.top &&
|
|
159
|
+
event.clientY <= rect.bottom;
|
|
160
|
+
if (!inside) close();
|
|
161
|
+
}
|
|
162
|
+
|
|
105
163
|
function choose(item: CommandItem) {
|
|
106
164
|
if (item.disabled) return;
|
|
107
165
|
onselect?.(item);
|
|
@@ -109,18 +167,19 @@
|
|
|
109
167
|
}
|
|
110
168
|
|
|
111
169
|
function onKeydown(e: KeyboardEvent) {
|
|
112
|
-
if (!open) return;
|
|
113
|
-
if (e.key === 'Escape') {
|
|
114
|
-
e.preventDefault();
|
|
115
|
-
close();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
170
|
+
if (!open || !flat.length) return;
|
|
118
171
|
if (e.key === 'ArrowDown') {
|
|
119
172
|
e.preventDefault();
|
|
120
|
-
activeIndex =
|
|
173
|
+
activeIndex = nextEnabled(activeIndex, 1);
|
|
121
174
|
} else if (e.key === 'ArrowUp') {
|
|
122
175
|
e.preventDefault();
|
|
123
|
-
activeIndex =
|
|
176
|
+
activeIndex = nextEnabled(activeIndex, -1);
|
|
177
|
+
} else if (e.key === 'Home') {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
activeIndex = nextEnabled(-1, 1);
|
|
180
|
+
} else if (e.key === 'End') {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
activeIndex = nextEnabled(flat.length, -1);
|
|
124
183
|
} else if (e.key === 'Enter') {
|
|
125
184
|
e.preventDefault();
|
|
126
185
|
const item = flat[activeIndex];
|
|
@@ -129,128 +188,175 @@
|
|
|
129
188
|
}
|
|
130
189
|
|
|
131
190
|
function onInput() {
|
|
132
|
-
activeIndex = 0;
|
|
191
|
+
activeIndex = Math.max(0, firstEnabledIndex());
|
|
133
192
|
onquery?.(query);
|
|
134
193
|
}
|
|
135
194
|
</script>
|
|
136
195
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
196
|
+
<dialog
|
|
197
|
+
bind:this={dialogEl}
|
|
198
|
+
class={['command-palette', className]}
|
|
199
|
+
aria-label={dialogLabel}
|
|
200
|
+
onclose={handleDialogClose}
|
|
201
|
+
onclick={handleBackdropClick}
|
|
202
|
+
>
|
|
145
203
|
<div
|
|
146
|
-
class="
|
|
204
|
+
class="rounded-2xl border-border bg-surface-elevated shadow-2xl w-full overflow-hidden border"
|
|
147
205
|
>
|
|
148
|
-
<div
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class="h-4 w-4 text-muted shrink-0"
|
|
162
|
-
viewBox="0 0 24 24"
|
|
163
|
-
fill="none"
|
|
164
|
-
stroke="currentColor"
|
|
165
|
-
stroke-width="2"
|
|
166
|
-
aria-hidden="true"
|
|
167
|
-
>
|
|
168
|
-
<path
|
|
169
|
-
stroke-linecap="round"
|
|
170
|
-
stroke-linejoin="round"
|
|
171
|
-
d="M21 21l-4.35-4.35M11 18a7 7 0 100-14 7 7 0 000 14z"
|
|
172
|
-
/>
|
|
173
|
-
</svg>
|
|
174
|
-
<input
|
|
175
|
-
bind:value={query}
|
|
176
|
-
{@attach focusInput}
|
|
177
|
-
placeholder={resolvedPlaceholder}
|
|
178
|
-
class="h-12 text-sm text-primary placeholder:text-muted w-full bg-transparent outline-none"
|
|
179
|
-
autocomplete="off"
|
|
180
|
-
spellcheck="false"
|
|
181
|
-
aria-autocomplete="list"
|
|
182
|
-
oninput={onInput}
|
|
206
|
+
<div class="gap-2 border-border px-3 flex items-center border-b">
|
|
207
|
+
<svg
|
|
208
|
+
class="h-4 w-4 text-muted shrink-0"
|
|
209
|
+
viewBox="0 0 24 24"
|
|
210
|
+
fill="none"
|
|
211
|
+
stroke="currentColor"
|
|
212
|
+
stroke-width="2"
|
|
213
|
+
aria-hidden="true"
|
|
214
|
+
>
|
|
215
|
+
<path
|
|
216
|
+
stroke-linecap="round"
|
|
217
|
+
stroke-linejoin="round"
|
|
218
|
+
d="M21 21l-4.35-4.35M11 18a7 7 0 100-14 7 7 0 000 14z"
|
|
183
219
|
/>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
220
|
+
</svg>
|
|
221
|
+
<input
|
|
222
|
+
bind:value={query}
|
|
223
|
+
placeholder={resolvedPlaceholder}
|
|
224
|
+
class="h-12 text-sm text-primary placeholder:text-muted w-full bg-transparent outline-none"
|
|
225
|
+
autocomplete="off"
|
|
226
|
+
spellcheck="false"
|
|
227
|
+
role="combobox"
|
|
228
|
+
aria-autocomplete="list"
|
|
229
|
+
aria-expanded="true"
|
|
230
|
+
aria-controls={listId}
|
|
231
|
+
aria-activedescendant={activeOptionId}
|
|
232
|
+
oninput={onInput}
|
|
233
|
+
onkeydown={onKeydown}
|
|
234
|
+
/>
|
|
235
|
+
{#if loading}
|
|
236
|
+
<span
|
|
237
|
+
class="h-4 w-4 animate-spin border-border border-t-brand-500 shrink-0 rounded-full border-2"
|
|
238
|
+
aria-hidden="true"
|
|
239
|
+
></span>
|
|
240
|
+
{/if}
|
|
241
|
+
<Kbd keys={['Esc']} size="sm" />
|
|
242
|
+
</div>
|
|
192
243
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
{#if
|
|
238
|
-
<
|
|
239
|
-
>{item.subtitle}</span
|
|
240
|
-
>
|
|
244
|
+
<div bind:this={listEl} id={listId} class="max-h-80 p-2 overflow-y-auto" role="listbox">
|
|
245
|
+
{#if flat.length === 0}
|
|
246
|
+
<p class="px-3 py-6 text-sm text-muted text-center">{resolvedEmpty}</p>
|
|
247
|
+
{:else}
|
|
248
|
+
{#each groups as [groupName, groupItems] (groupName)}
|
|
249
|
+
<p class="px-2 py-1.5 font-semibold tracking-wide text-muted text-[11px] uppercase">
|
|
250
|
+
{groupName}
|
|
251
|
+
</p>
|
|
252
|
+
<ul class="mb-2 gap-0.5 flex flex-col">
|
|
253
|
+
{#each groupItems as { item, index } (item.id)}
|
|
254
|
+
{@const Icon = item.icon}
|
|
255
|
+
{@const active = index === activeIndex}
|
|
256
|
+
<li>
|
|
257
|
+
<button
|
|
258
|
+
id={optionId(item.id)}
|
|
259
|
+
type="button"
|
|
260
|
+
role="option"
|
|
261
|
+
tabindex="-1"
|
|
262
|
+
aria-selected={active}
|
|
263
|
+
disabled={item.disabled}
|
|
264
|
+
onclick={() => choose(item)}
|
|
265
|
+
onmouseenter={() => {
|
|
266
|
+
if (!item.disabled) activeIndex = index;
|
|
267
|
+
}}
|
|
268
|
+
{@attach active ? keepVisible : undefined}
|
|
269
|
+
class={[
|
|
270
|
+
'gap-3 rounded-xl px-3 py-2 text-sm flex w-full items-center justify-between text-left transition-colors',
|
|
271
|
+
active
|
|
272
|
+
? 'bg-brand-50 text-brand-700 dark:bg-brand-950/40 dark:text-brand-300'
|
|
273
|
+
: 'text-primary hover:bg-surface-overlay',
|
|
274
|
+
item.disabled && 'cursor-not-allowed opacity-40'
|
|
275
|
+
]}
|
|
276
|
+
>
|
|
277
|
+
<span class="gap-3 min-w-0 flex flex-1 items-center">
|
|
278
|
+
{#if showIconColumn}
|
|
279
|
+
<span
|
|
280
|
+
class={[
|
|
281
|
+
'h-8 w-8 rounded-lg flex shrink-0 items-center justify-center',
|
|
282
|
+
active
|
|
283
|
+
? 'bg-brand-100/80 text-brand-700 dark:bg-brand-900/50 dark:text-brand-300'
|
|
284
|
+
: 'bg-surface-overlay text-muted'
|
|
285
|
+
]}
|
|
286
|
+
aria-hidden="true"
|
|
287
|
+
>
|
|
288
|
+
{#if Icon}
|
|
289
|
+
<Icon size={16} strokeWidth={2} />
|
|
241
290
|
{/if}
|
|
242
291
|
</span>
|
|
243
|
-
</span>
|
|
244
|
-
{#if item.shortcut?.length}
|
|
245
|
-
<Kbd keys={item.shortcut} size="sm" />
|
|
246
292
|
{/if}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
293
|
+
<span class="min-w-0 flex-1">
|
|
294
|
+
<span class="block truncate">{item.label}</span>
|
|
295
|
+
{#if item.subtitle}
|
|
296
|
+
<span class="mt-0.5 text-xs text-muted block truncate">{item.subtitle}</span
|
|
297
|
+
>
|
|
298
|
+
{/if}
|
|
299
|
+
</span>
|
|
300
|
+
</span>
|
|
301
|
+
{#if item.shortcut?.length}
|
|
302
|
+
<Kbd keys={item.shortcut} size="sm" />
|
|
303
|
+
{/if}
|
|
304
|
+
</button>
|
|
305
|
+
</li>
|
|
306
|
+
{/each}
|
|
307
|
+
</ul>
|
|
308
|
+
{/each}
|
|
309
|
+
{/if}
|
|
254
310
|
</div>
|
|
255
311
|
</div>
|
|
256
|
-
|
|
312
|
+
</dialog>
|
|
313
|
+
|
|
314
|
+
<style>
|
|
315
|
+
.command-palette {
|
|
316
|
+
margin: 12vh auto auto;
|
|
317
|
+
padding: 0;
|
|
318
|
+
border: none;
|
|
319
|
+
background: transparent;
|
|
320
|
+
color: inherit;
|
|
321
|
+
width: calc(100% - 2rem);
|
|
322
|
+
max-width: 36rem;
|
|
323
|
+
overflow: visible;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.command-palette::backdrop {
|
|
327
|
+
background: oklch(15% 0.02 265 / 0.45);
|
|
328
|
+
backdrop-filter: blur(6px);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
:global(.dark) .command-palette::backdrop {
|
|
332
|
+
background: oklch(0% 0 0 / 0.65);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.command-palette[open] {
|
|
336
|
+
animation: palette-in 140ms ease-out;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.command-palette[open]::backdrop {
|
|
340
|
+
animation: palette-backdrop-in 140ms ease-out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@keyframes palette-in {
|
|
344
|
+
from {
|
|
345
|
+
opacity: 0;
|
|
346
|
+
transform: translateY(6px) scale(0.98);
|
|
347
|
+
}
|
|
348
|
+
to {
|
|
349
|
+
opacity: 1;
|
|
350
|
+
transform: translateY(0) scale(1);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@keyframes palette-backdrop-in {
|
|
355
|
+
from {
|
|
356
|
+
opacity: 0;
|
|
357
|
+
}
|
|
358
|
+
to {
|
|
359
|
+
opacity: 1;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
</style>
|
package/dist/index.d.ts
CHANGED
|
@@ -698,6 +698,9 @@ export { default as RegisterForm } from './components/organisms/RegisterForm/Reg
|
|
|
698
698
|
export { default as ForgotPasswordForm } from './components/organisms/ForgotPasswordForm/ForgotPasswordForm.svelte';
|
|
699
699
|
export { default as OtpVerify } from './components/organisms/OtpVerify/OtpVerify.svelte';
|
|
700
700
|
export { default as ChatWindow } from './components/organisms/ChatWindow/ChatWindow.svelte';
|
|
701
|
+
export { default as AssistantChat } from './components/organisms/AssistantChat/AssistantChat.svelte';
|
|
702
|
+
export type { AssistantMessage } from './components/organisms/AssistantChat/AssistantChat.svelte';
|
|
703
|
+
export { default as AssistantFab } from './components/organisms/AssistantFab/AssistantFab.svelte';
|
|
701
704
|
export { default as CommentThread } from './components/organisms/CommentThread/CommentThread.svelte';
|
|
702
705
|
export type { CommentItem } from './components/organisms/CommentThread/CommentThread.svelte';
|
|
703
706
|
export { default as ActivityFeed } from './components/organisms/ActivityFeed/ActivityFeed.svelte';
|
package/dist/index.js
CHANGED
|
@@ -483,6 +483,8 @@ export { default as RegisterForm } from './components/organisms/RegisterForm/Reg
|
|
|
483
483
|
export { default as ForgotPasswordForm } from './components/organisms/ForgotPasswordForm/ForgotPasswordForm.svelte';
|
|
484
484
|
export { default as OtpVerify } from './components/organisms/OtpVerify/OtpVerify.svelte';
|
|
485
485
|
export { default as ChatWindow } from './components/organisms/ChatWindow/ChatWindow.svelte';
|
|
486
|
+
export { default as AssistantChat } from './components/organisms/AssistantChat/AssistantChat.svelte';
|
|
487
|
+
export { default as AssistantFab } from './components/organisms/AssistantFab/AssistantFab.svelte';
|
|
486
488
|
export { default as CommentThread } from './components/organisms/CommentThread/CommentThread.svelte';
|
|
487
489
|
export { default as ActivityFeed } from './components/organisms/ActivityFeed/ActivityFeed.svelte';
|
|
488
490
|
export { default as SettingsLayout } from './components/organisms/SettingsLayout/SettingsLayout.svelte';
|