@d34dman/flowdrop 0.0.43 → 0.0.44
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/README.md +8 -8
- package/dist/api/enhanced-client.d.ts +3 -1
- package/dist/api/enhanced-client.js +35 -5
- package/dist/components/App.svelte +68 -34
- package/dist/components/ConfigForm.svelte +169 -142
- package/dist/components/ConfigForm.svelte.d.ts +4 -2
- package/dist/components/ConfigPanel.svelte +42 -15
- package/dist/components/LogsSidebar.svelte +20 -19
- package/dist/components/Navbar.svelte +150 -80
- package/dist/components/Navbar.svelte.d.ts +8 -0
- package/dist/components/NodeSidebar.svelte +330 -217
- package/dist/components/PipelineStatus.svelte +6 -1
- package/dist/components/ReadOnlyDetails.svelte +14 -14
- package/dist/components/SchemaForm.svelte +49 -30
- package/dist/components/SchemaForm.svelte.d.ts +11 -1
- package/dist/components/SettingsModal.svelte +279 -0
- package/dist/components/SettingsModal.svelte.d.ts +23 -0
- package/dist/components/SettingsPanel.svelte +615 -0
- package/dist/components/SettingsPanel.svelte.d.ts +21 -0
- package/dist/components/ThemeToggle.svelte +186 -0
- package/dist/components/ThemeToggle.svelte.d.ts +14 -0
- package/dist/components/WorkflowEditor.svelte +110 -36
- package/dist/components/form/FormArray.svelte +81 -81
- package/dist/components/form/FormAutocomplete.svelte +1014 -0
- package/dist/components/form/FormAutocomplete.svelte.d.ts +25 -0
- package/dist/components/form/FormCheckboxGroup.svelte +16 -16
- package/dist/components/form/FormCodeEditor.svelte +26 -26
- package/dist/components/form/FormField.svelte +52 -21
- package/dist/components/form/FormFieldLight.svelte +19 -19
- package/dist/components/form/FormFieldWrapper.svelte +4 -4
- package/dist/components/form/FormMarkdownEditor.svelte +124 -57
- package/dist/components/form/FormNumberField.svelte +13 -13
- package/dist/components/form/FormRangeField.svelte +16 -16
- package/dist/components/form/FormSelect.svelte +15 -15
- package/dist/components/form/FormTemplateEditor.svelte +34 -34
- package/dist/components/form/FormTextField.svelte +13 -13
- package/dist/components/form/FormTextarea.svelte +13 -13
- package/dist/components/form/FormToggle.svelte +8 -8
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/types.d.ts +133 -8
- package/dist/components/form/types.js +50 -1
- package/dist/components/interrupt/ChoicePrompt.svelte +45 -38
- package/dist/components/interrupt/ConfirmationPrompt.svelte +35 -35
- package/dist/components/interrupt/FormPrompt.svelte +27 -20
- package/dist/components/interrupt/InterruptBubble.svelte +50 -50
- package/dist/components/interrupt/TextInputPrompt.svelte +39 -32
- package/dist/components/layouts/MainLayout.svelte +233 -34
- package/dist/components/layouts/MainLayout.svelte.d.ts +12 -0
- package/dist/components/nodes/GatewayNode.svelte +102 -73
- package/dist/components/nodes/IdeaNode.svelte +53 -52
- package/dist/components/nodes/NotesNode.svelte +120 -88
- package/dist/components/nodes/SimpleNode.svelte +67 -47
- package/dist/components/nodes/SquareNode.svelte +86 -49
- package/dist/components/nodes/TerminalNode.svelte +122 -72
- package/dist/components/nodes/ToolNode.svelte +96 -65
- package/dist/components/nodes/WorkflowNode.svelte +91 -67
- package/dist/components/playground/ChatPanel.svelte +76 -76
- package/dist/components/playground/ExecutionLogs.svelte +71 -69
- package/dist/components/playground/InputCollector.svelte +59 -59
- package/dist/components/playground/MessageBubble.svelte +111 -112
- package/dist/components/playground/Playground.svelte +184 -138
- package/dist/components/playground/PlaygroundModal.svelte +18 -19
- package/dist/components/playground/SessionManager.svelte +68 -67
- package/dist/config/defaultPortConfig.js +22 -22
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/form/fieldRegistry.d.ts +17 -1
- package/dist/form/fieldRegistry.js +18 -2
- package/dist/form/index.d.ts +20 -2
- package/dist/form/index.js +19 -1
- package/dist/helpers/workflowEditorHelper.js +23 -11
- package/dist/index.d.ts +5 -0
- package/dist/index.js +13 -0
- package/dist/services/autoSaveService.d.ts +112 -0
- package/dist/services/autoSaveService.js +223 -0
- package/dist/services/settingsService.d.ts +92 -0
- package/dist/services/settingsService.js +202 -0
- package/dist/services/toastService.d.ts +9 -0
- package/dist/services/toastService.js +30 -1
- package/dist/stores/settingsStore.d.ts +128 -0
- package/dist/stores/settingsStore.js +488 -0
- package/dist/stores/themeStore.d.ts +68 -0
- package/dist/stores/themeStore.js +215 -0
- package/dist/styles/base.css +298 -621
- package/dist/styles/toast.css +33 -0
- package/dist/styles/tokens.css +366 -0
- package/dist/types/index.d.ts +78 -0
- package/dist/types/index.js +2 -0
- package/dist/types/playground.d.ts +12 -0
- package/dist/types/settings.d.ts +185 -0
- package/dist/types/settings.js +101 -0
- package/dist/utils/colors.d.ts +100 -7
- package/dist/utils/colors.js +228 -67
- package/package.json +3 -3
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
FormAutocomplete Component
|
|
3
|
+
Text input with autocomplete suggestions fetched from a callback URL
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- Single or multiple selection support
|
|
7
|
+
- Selected values displayed as removable tags with light blue background
|
|
8
|
+
- Debounced fetching on input
|
|
9
|
+
- Optional fetch on focus
|
|
10
|
+
- Loading state indicator
|
|
11
|
+
- Keyboard navigation (arrow keys, enter, escape, backspace to remove)
|
|
12
|
+
- Click-away to close dropdown
|
|
13
|
+
- Response mapping with labelField/valueField
|
|
14
|
+
- Error handling with retry
|
|
15
|
+
- Full accessibility support (ARIA combobox pattern)
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
- For single selection: autocomplete.multiple = false (default)
|
|
19
|
+
- For multiple selection: autocomplete.multiple = true
|
|
20
|
+
- Selected values appear as tags with "x" button to remove
|
|
21
|
+
-->
|
|
22
|
+
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
import { getContext, onMount } from 'svelte';
|
|
25
|
+
import Icon from '@iconify/svelte';
|
|
26
|
+
import type { AutocompleteConfig, AuthProvider } from '../../types/index.js';
|
|
27
|
+
import type { FieldOption } from './types.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Props interface for FormAutocomplete component
|
|
31
|
+
*/
|
|
32
|
+
interface Props {
|
|
33
|
+
/** Field identifier */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Current selected value (string for single, string[] for multiple) */
|
|
36
|
+
value: string | string[];
|
|
37
|
+
/** Autocomplete configuration */
|
|
38
|
+
autocomplete: AutocompleteConfig;
|
|
39
|
+
/** Whether the field is required */
|
|
40
|
+
required?: boolean;
|
|
41
|
+
/** Placeholder text */
|
|
42
|
+
placeholder?: string;
|
|
43
|
+
/** Whether the field is disabled */
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** ARIA description ID */
|
|
46
|
+
ariaDescribedBy?: string;
|
|
47
|
+
/** Callback when value changes */
|
|
48
|
+
onChange: (value: string | string[]) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let {
|
|
52
|
+
id,
|
|
53
|
+
value = '',
|
|
54
|
+
autocomplete,
|
|
55
|
+
required = false,
|
|
56
|
+
placeholder = '',
|
|
57
|
+
disabled = false,
|
|
58
|
+
ariaDescribedBy,
|
|
59
|
+
onChange
|
|
60
|
+
}: Props = $props();
|
|
61
|
+
|
|
62
|
+
// Get AuthProvider from context (set by SchemaForm or parent)
|
|
63
|
+
const authProvider = getContext<AuthProvider | undefined>('flowdrop:authProvider');
|
|
64
|
+
const baseUrl = getContext<string | undefined>('flowdrop:baseUrl') ?? '';
|
|
65
|
+
|
|
66
|
+
// Configuration with defaults
|
|
67
|
+
const queryParam = $derived(autocomplete.queryParam ?? 'q');
|
|
68
|
+
const minChars = $derived(autocomplete.minChars ?? 0);
|
|
69
|
+
const debounceMs = $derived(autocomplete.debounceMs ?? 300);
|
|
70
|
+
const fetchOnFocus = $derived(autocomplete.fetchOnFocus ?? false);
|
|
71
|
+
const labelField = $derived(autocomplete.labelField ?? 'label');
|
|
72
|
+
const valueField = $derived(autocomplete.valueField ?? 'value');
|
|
73
|
+
const allowFreeText = $derived(autocomplete.allowFreeText ?? false);
|
|
74
|
+
const multiple = $derived(autocomplete.multiple ?? false);
|
|
75
|
+
|
|
76
|
+
// Component state
|
|
77
|
+
let inputElement: HTMLInputElement | undefined = $state(undefined);
|
|
78
|
+
let containerElement: HTMLDivElement | undefined = $state(undefined);
|
|
79
|
+
let popoverElement: HTMLDivElement | undefined = $state(undefined);
|
|
80
|
+
let inputValue = $state('');
|
|
81
|
+
let suggestions = $state<FieldOption[]>([]);
|
|
82
|
+
let isOpen = $state(false);
|
|
83
|
+
let isLoading = $state(false);
|
|
84
|
+
let error = $state<string | null>(null);
|
|
85
|
+
let highlightedIndex = $state(-1);
|
|
86
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
87
|
+
let abortController: AbortController | null = null;
|
|
88
|
+
|
|
89
|
+
// Popover positioning style
|
|
90
|
+
let popoverStyle = $state('');
|
|
91
|
+
|
|
92
|
+
// Cache of value-to-label mappings for selected items
|
|
93
|
+
let labelCache = $state<Map<string, string>>(new Map());
|
|
94
|
+
|
|
95
|
+
// Generate unique IDs for accessibility
|
|
96
|
+
const listboxId = `${id}-listbox`;
|
|
97
|
+
const getOptionId = (index: number): string => `${id}-option-${index}`;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the selected values as an array (normalizes single/multiple)
|
|
101
|
+
*/
|
|
102
|
+
const selectedValues = $derived<string[]>(
|
|
103
|
+
multiple
|
|
104
|
+
? Array.isArray(value)
|
|
105
|
+
? value
|
|
106
|
+
: value
|
|
107
|
+
? [String(value)]
|
|
108
|
+
: []
|
|
109
|
+
: value
|
|
110
|
+
? [String(value)]
|
|
111
|
+
: []
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a value is selected
|
|
116
|
+
*/
|
|
117
|
+
function isSelected(optionValue: string): boolean {
|
|
118
|
+
return selectedValues.includes(optionValue);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get display label for a selected value
|
|
123
|
+
*/
|
|
124
|
+
function getDisplayLabel(val: string): string {
|
|
125
|
+
// Check cache first
|
|
126
|
+
if (labelCache.has(val)) {
|
|
127
|
+
return labelCache.get(val) ?? val;
|
|
128
|
+
}
|
|
129
|
+
// Check current suggestions
|
|
130
|
+
const match = suggestions.find((s) => s.value === val);
|
|
131
|
+
if (match) {
|
|
132
|
+
labelCache.set(val, match.label);
|
|
133
|
+
return match.label;
|
|
134
|
+
}
|
|
135
|
+
// Return value as fallback
|
|
136
|
+
return val;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build the full URL for fetching suggestions
|
|
141
|
+
* @param query - The search query
|
|
142
|
+
* @returns Full URL with query parameter
|
|
143
|
+
*/
|
|
144
|
+
function buildUrl(query: string): string {
|
|
145
|
+
const url = autocomplete.url.startsWith('http')
|
|
146
|
+
? autocomplete.url
|
|
147
|
+
: `${baseUrl}${autocomplete.url}`;
|
|
148
|
+
|
|
149
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
150
|
+
return `${url}${separator}${encodeURIComponent(queryParam)}=${encodeURIComponent(query)}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Map API response to FieldOption array
|
|
155
|
+
* @param data - Response data from API
|
|
156
|
+
* @returns Array of FieldOption objects
|
|
157
|
+
*/
|
|
158
|
+
function mapResponse(data: unknown): FieldOption[] {
|
|
159
|
+
if (!Array.isArray(data)) {
|
|
160
|
+
console.warn('[FormAutocomplete] Response is not an array:', data);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return data.map((item: Record<string, unknown>) => ({
|
|
165
|
+
label: String(item[labelField] ?? item[valueField] ?? ''),
|
|
166
|
+
value: String(item[valueField] ?? '')
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Fetch suggestions from the callback URL
|
|
172
|
+
* @param query - The search query
|
|
173
|
+
*/
|
|
174
|
+
async function fetchSuggestions(query: string): Promise<void> {
|
|
175
|
+
// Cancel any pending request
|
|
176
|
+
if (abortController) {
|
|
177
|
+
abortController.abort();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check minimum characters requirement
|
|
181
|
+
if (query.length < minChars && query.length > 0) {
|
|
182
|
+
suggestions = [];
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
isLoading = true;
|
|
187
|
+
error = null;
|
|
188
|
+
abortController = new AbortController();
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// Build headers with authentication
|
|
192
|
+
const headers: Record<string, string> = {
|
|
193
|
+
Accept: 'application/json',
|
|
194
|
+
'Content-Type': 'application/json'
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Add auth headers if provider is available
|
|
198
|
+
if (authProvider) {
|
|
199
|
+
const authHeaders = await authProvider.getAuthHeaders();
|
|
200
|
+
Object.assign(headers, authHeaders);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Fetch with timeout
|
|
204
|
+
const timeoutId = setTimeout(() => {
|
|
205
|
+
abortController?.abort();
|
|
206
|
+
}, 5000);
|
|
207
|
+
|
|
208
|
+
const response = await fetch(buildUrl(query), {
|
|
209
|
+
method: 'GET',
|
|
210
|
+
headers,
|
|
211
|
+
signal: abortController.signal
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
clearTimeout(timeoutId);
|
|
215
|
+
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const data = await response.json();
|
|
221
|
+
const mapped = mapResponse(data);
|
|
222
|
+
|
|
223
|
+
// Update label cache with fetched suggestions
|
|
224
|
+
mapped.forEach((opt) => {
|
|
225
|
+
labelCache.set(opt.value, opt.label);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
suggestions = mapped;
|
|
229
|
+
highlightedIndex = -1;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
232
|
+
// Request was cancelled, ignore
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
console.error('[FormAutocomplete] Fetch error:', err);
|
|
236
|
+
error = err instanceof Error ? err.message : 'Failed to fetch suggestions';
|
|
237
|
+
suggestions = [];
|
|
238
|
+
} finally {
|
|
239
|
+
isLoading = false;
|
|
240
|
+
abortController = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Debounced fetch for input changes
|
|
246
|
+
* @param query - The search query
|
|
247
|
+
*/
|
|
248
|
+
function debouncedFetch(query: string): void {
|
|
249
|
+
if (debounceTimer) {
|
|
250
|
+
clearTimeout(debounceTimer);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
debounceTimer = setTimeout(() => {
|
|
254
|
+
fetchSuggestions(query);
|
|
255
|
+
}, debounceMs);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Handle input value changes
|
|
260
|
+
* @param event - Input event
|
|
261
|
+
*/
|
|
262
|
+
function handleInput(event: Event): void {
|
|
263
|
+
const target = event.currentTarget as HTMLInputElement;
|
|
264
|
+
inputValue = target.value;
|
|
265
|
+
|
|
266
|
+
// Open dropdown
|
|
267
|
+
showDropdown();
|
|
268
|
+
|
|
269
|
+
// If allowFreeText and single mode, update the value immediately
|
|
270
|
+
if (allowFreeText && !multiple) {
|
|
271
|
+
onChange(inputValue);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Fetch suggestions with debounce
|
|
275
|
+
debouncedFetch(inputValue);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Handle input focus
|
|
280
|
+
*/
|
|
281
|
+
function handleFocus(): void {
|
|
282
|
+
if (fetchOnFocus && suggestions.length === 0 && !isLoading) {
|
|
283
|
+
fetchSuggestions(inputValue);
|
|
284
|
+
}
|
|
285
|
+
showDropdown();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Handle input blur
|
|
290
|
+
* Delayed to allow click events on options to fire first
|
|
291
|
+
*/
|
|
292
|
+
function handleBlur(): void {
|
|
293
|
+
setTimeout(() => {
|
|
294
|
+
hideDropdown();
|
|
295
|
+
|
|
296
|
+
// If not allowFreeText and single mode, validate the input
|
|
297
|
+
if (!allowFreeText && !multiple && inputValue !== '') {
|
|
298
|
+
const currentVal = selectedValues;
|
|
299
|
+
const matchingSuggestion = suggestions.find(
|
|
300
|
+
(s) => s.value === currentVal[0] || s.label.toLowerCase() === inputValue.toLowerCase()
|
|
301
|
+
);
|
|
302
|
+
if (!matchingSuggestion && currentVal.length === 0) {
|
|
303
|
+
inputValue = '';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}, 200);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Handle option selection
|
|
311
|
+
* @param option - Selected option
|
|
312
|
+
*/
|
|
313
|
+
function selectOption(option: FieldOption): void {
|
|
314
|
+
// Update label cache
|
|
315
|
+
labelCache.set(option.value, option.label);
|
|
316
|
+
|
|
317
|
+
if (multiple) {
|
|
318
|
+
const current = selectedValues;
|
|
319
|
+
if (current.includes(option.value)) {
|
|
320
|
+
// Remove if already selected
|
|
321
|
+
const newValues = current.filter((v) => v !== option.value);
|
|
322
|
+
onChange(newValues);
|
|
323
|
+
} else {
|
|
324
|
+
// Add to selection
|
|
325
|
+
const newValues = [...current, option.value];
|
|
326
|
+
onChange(newValues);
|
|
327
|
+
}
|
|
328
|
+
// Clear input and keep dropdown open for more selections
|
|
329
|
+
inputValue = '';
|
|
330
|
+
inputElement?.focus();
|
|
331
|
+
} else {
|
|
332
|
+
// Single selection mode
|
|
333
|
+
inputValue = '';
|
|
334
|
+
onChange(option.value);
|
|
335
|
+
hideDropdown();
|
|
336
|
+
}
|
|
337
|
+
highlightedIndex = -1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Remove a selected tag
|
|
342
|
+
* @param valueToRemove - The value to remove
|
|
343
|
+
*/
|
|
344
|
+
function removeTag(valueToRemove: string): void {
|
|
345
|
+
if (disabled) return;
|
|
346
|
+
|
|
347
|
+
if (multiple) {
|
|
348
|
+
const current = selectedValues;
|
|
349
|
+
const newValues = current.filter((v) => v !== valueToRemove);
|
|
350
|
+
onChange(newValues);
|
|
351
|
+
} else {
|
|
352
|
+
onChange('');
|
|
353
|
+
}
|
|
354
|
+
inputElement?.focus();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Handle keyboard navigation
|
|
359
|
+
* @param event - Keyboard event
|
|
360
|
+
*/
|
|
361
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
362
|
+
// Handle backspace to remove last tag in multiple mode
|
|
363
|
+
if (event.key === 'Backspace' && inputValue === '' && selectedValues.length > 0) {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
const current = selectedValues;
|
|
366
|
+
if (multiple) {
|
|
367
|
+
const newValues = current.slice(0, -1);
|
|
368
|
+
onChange(newValues);
|
|
369
|
+
} else {
|
|
370
|
+
onChange('');
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!isOpen && event.key !== 'ArrowDown' && event.key !== 'Enter') {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
switch (event.key) {
|
|
380
|
+
case 'ArrowDown':
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
if (!isOpen) {
|
|
383
|
+
showDropdown();
|
|
384
|
+
if (fetchOnFocus && suggestions.length === 0) {
|
|
385
|
+
fetchSuggestions(inputValue);
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
highlightedIndex = Math.min(highlightedIndex + 1, suggestions.length - 1);
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
|
|
392
|
+
case 'ArrowUp':
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
highlightedIndex = Math.max(highlightedIndex - 1, -1);
|
|
395
|
+
break;
|
|
396
|
+
|
|
397
|
+
case 'Enter':
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) {
|
|
400
|
+
selectOption(suggestions[highlightedIndex]);
|
|
401
|
+
} else if (allowFreeText && inputValue !== '') {
|
|
402
|
+
if (multiple) {
|
|
403
|
+
const current = selectedValues;
|
|
404
|
+
if (!current.includes(inputValue)) {
|
|
405
|
+
onChange([...current, inputValue]);
|
|
406
|
+
}
|
|
407
|
+
inputValue = '';
|
|
408
|
+
} else {
|
|
409
|
+
onChange(inputValue);
|
|
410
|
+
hideDropdown();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
break;
|
|
414
|
+
|
|
415
|
+
case 'Escape':
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
hideDropdown();
|
|
418
|
+
highlightedIndex = -1;
|
|
419
|
+
break;
|
|
420
|
+
|
|
421
|
+
case 'Tab':
|
|
422
|
+
// Allow tab to close dropdown naturally
|
|
423
|
+
hideDropdown();
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Retry failed fetch
|
|
430
|
+
*/
|
|
431
|
+
function handleRetry(): void {
|
|
432
|
+
error = null;
|
|
433
|
+
fetchSuggestions(inputValue);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Clear all selections
|
|
438
|
+
*/
|
|
439
|
+
function handleClearAll(): void {
|
|
440
|
+
inputValue = '';
|
|
441
|
+
onChange(multiple ? [] : '');
|
|
442
|
+
suggestions = [];
|
|
443
|
+
inputElement?.focus();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Sync label cache when value prop changes externally
|
|
448
|
+
*/
|
|
449
|
+
$effect(() => {
|
|
450
|
+
// When value changes, try to find labels from suggestions
|
|
451
|
+
const vals = selectedValues;
|
|
452
|
+
vals.forEach((val) => {
|
|
453
|
+
if (!labelCache.has(val)) {
|
|
454
|
+
const match = suggestions.find((s) => s.value === val);
|
|
455
|
+
if (match) {
|
|
456
|
+
labelCache.set(val, match.label);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Calculate popover position relative to viewport
|
|
464
|
+
* Popover API renders in top layer, bypassing all stacking contexts
|
|
465
|
+
*/
|
|
466
|
+
function updatePopoverPosition(): void {
|
|
467
|
+
if (!containerElement) return;
|
|
468
|
+
|
|
469
|
+
const rect = containerElement.getBoundingClientRect();
|
|
470
|
+
const viewportHeight = window.innerHeight;
|
|
471
|
+
const maxDropdownHeight = 240; // 15rem in pixels approximately
|
|
472
|
+
const spaceBelow = viewportHeight - rect.bottom;
|
|
473
|
+
const spaceAbove = rect.top;
|
|
474
|
+
|
|
475
|
+
const left = rect.left;
|
|
476
|
+
const width = rect.width;
|
|
477
|
+
|
|
478
|
+
if (spaceBelow < maxDropdownHeight && spaceAbove > spaceBelow) {
|
|
479
|
+
// Position above the input
|
|
480
|
+
const bottom = viewportHeight - rect.top + 4;
|
|
481
|
+
const maxHeight = Math.min(spaceAbove - 8, maxDropdownHeight);
|
|
482
|
+
popoverStyle = `bottom: ${bottom}px; left: ${left}px; width: ${width}px; max-height: ${maxHeight}px;`;
|
|
483
|
+
} else {
|
|
484
|
+
// Position below the input (default)
|
|
485
|
+
const top = rect.bottom + 4;
|
|
486
|
+
const maxHeight = Math.min(spaceBelow - 8, maxDropdownHeight);
|
|
487
|
+
popoverStyle = `top: ${top}px; left: ${left}px; width: ${width}px; max-height: ${maxHeight}px;`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Show the popover dropdown
|
|
493
|
+
*/
|
|
494
|
+
function showDropdown(): void {
|
|
495
|
+
if (!popoverElement || disabled) return;
|
|
496
|
+
|
|
497
|
+
updatePopoverPosition();
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
popoverElement.showPopover();
|
|
501
|
+
isOpen = true;
|
|
502
|
+
} catch {
|
|
503
|
+
// Fallback for browsers without popover support - just set isOpen
|
|
504
|
+
isOpen = true;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Hide the popover dropdown
|
|
510
|
+
*/
|
|
511
|
+
function hideDropdown(): void {
|
|
512
|
+
if (!popoverElement) return;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
popoverElement.hidePopover();
|
|
516
|
+
} catch {
|
|
517
|
+
// Fallback for browsers without popover support
|
|
518
|
+
}
|
|
519
|
+
isOpen = false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Effect to update popover position on scroll/resize when open
|
|
524
|
+
*/
|
|
525
|
+
$effect(() => {
|
|
526
|
+
if (isOpen && containerElement) {
|
|
527
|
+
const handlePositionUpdate = (): void => {
|
|
528
|
+
updatePopoverPosition();
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
window.addEventListener('scroll', handlePositionUpdate, true);
|
|
532
|
+
window.addEventListener('resize', handlePositionUpdate);
|
|
533
|
+
|
|
534
|
+
return () => {
|
|
535
|
+
window.removeEventListener('scroll', handlePositionUpdate, true);
|
|
536
|
+
window.removeEventListener('resize', handlePositionUpdate);
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Cleanup on unmount
|
|
543
|
+
*/
|
|
544
|
+
onMount(() => {
|
|
545
|
+
return () => {
|
|
546
|
+
// Cleanup debounce timer
|
|
547
|
+
if (debounceTimer) {
|
|
548
|
+
clearTimeout(debounceTimer);
|
|
549
|
+
}
|
|
550
|
+
// Cleanup abort controller
|
|
551
|
+
if (abortController) {
|
|
552
|
+
abortController.abort();
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
</script>
|
|
557
|
+
|
|
558
|
+
<div
|
|
559
|
+
bind:this={containerElement}
|
|
560
|
+
class="form-autocomplete"
|
|
561
|
+
class:form-autocomplete--disabled={disabled}
|
|
562
|
+
class:form-autocomplete--multiple={multiple}
|
|
563
|
+
class:form-autocomplete--has-value={selectedValues.length > 0}
|
|
564
|
+
>
|
|
565
|
+
<!-- Main input container styled like a textfield/textarea -->
|
|
566
|
+
<div
|
|
567
|
+
class="form-autocomplete__field"
|
|
568
|
+
class:form-autocomplete__field--focused={isOpen}
|
|
569
|
+
onclick={() => inputElement?.focus()}
|
|
570
|
+
onkeydown={() => {}}
|
|
571
|
+
role="presentation"
|
|
572
|
+
>
|
|
573
|
+
<!-- Selected tags -->
|
|
574
|
+
{#if selectedValues.length > 0}
|
|
575
|
+
<div class="form-autocomplete__tags">
|
|
576
|
+
{#each selectedValues as selectedVal (selectedVal)}
|
|
577
|
+
<span class="form-autocomplete__tag">
|
|
578
|
+
<span class="form-autocomplete__tag-label">{getDisplayLabel(selectedVal)}</span>
|
|
579
|
+
{#if !disabled}
|
|
580
|
+
<button
|
|
581
|
+
type="button"
|
|
582
|
+
class="form-autocomplete__tag-remove"
|
|
583
|
+
aria-label={`Remove ${getDisplayLabel(selectedVal)}`}
|
|
584
|
+
onclick={(e) => {
|
|
585
|
+
e.stopPropagation();
|
|
586
|
+
removeTag(selectedVal);
|
|
587
|
+
}}
|
|
588
|
+
tabindex={-1}
|
|
589
|
+
>
|
|
590
|
+
<Icon icon="heroicons:x-mark" />
|
|
591
|
+
</button>
|
|
592
|
+
{/if}
|
|
593
|
+
</span>
|
|
594
|
+
{/each}
|
|
595
|
+
</div>
|
|
596
|
+
{/if}
|
|
597
|
+
|
|
598
|
+
<!-- Input area -->
|
|
599
|
+
<div class="form-autocomplete__input-area">
|
|
600
|
+
<input
|
|
601
|
+
bind:this={inputElement}
|
|
602
|
+
type="text"
|
|
603
|
+
{id}
|
|
604
|
+
class="form-autocomplete__input"
|
|
605
|
+
value={inputValue}
|
|
606
|
+
placeholder={selectedValues.length === 0 ? placeholder : ''}
|
|
607
|
+
{disabled}
|
|
608
|
+
aria-required={required}
|
|
609
|
+
aria-describedby={ariaDescribedBy}
|
|
610
|
+
role="combobox"
|
|
611
|
+
aria-expanded={isOpen}
|
|
612
|
+
aria-controls={listboxId}
|
|
613
|
+
aria-activedescendant={highlightedIndex >= 0 ? getOptionId(highlightedIndex) : undefined}
|
|
614
|
+
aria-autocomplete="list"
|
|
615
|
+
autocomplete="off"
|
|
616
|
+
oninput={handleInput}
|
|
617
|
+
onfocus={handleFocus}
|
|
618
|
+
onblur={handleBlur}
|
|
619
|
+
onkeydown={handleKeydown}
|
|
620
|
+
/>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<!-- Status icons -->
|
|
624
|
+
<div class="form-autocomplete__icons">
|
|
625
|
+
{#if isLoading}
|
|
626
|
+
<span class="form-autocomplete__spinner" aria-label="Loading suggestions">
|
|
627
|
+
<Icon icon="heroicons:arrow-path" />
|
|
628
|
+
</span>
|
|
629
|
+
{:else if selectedValues.length > 0 && !disabled}
|
|
630
|
+
<button
|
|
631
|
+
type="button"
|
|
632
|
+
class="form-autocomplete__clear"
|
|
633
|
+
aria-label="Clear all selections"
|
|
634
|
+
onclick={(e) => {
|
|
635
|
+
e.stopPropagation();
|
|
636
|
+
handleClearAll();
|
|
637
|
+
}}
|
|
638
|
+
tabindex={-1}
|
|
639
|
+
>
|
|
640
|
+
<Icon icon="heroicons:x-mark" />
|
|
641
|
+
</button>
|
|
642
|
+
{:else}
|
|
643
|
+
<span class="form-autocomplete__chevron" aria-hidden="true">
|
|
644
|
+
<Icon icon="heroicons:chevron-down" />
|
|
645
|
+
</span>
|
|
646
|
+
{/if}
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
|
|
650
|
+
<!-- Dropdown popover (uses Popover API to render in top layer, bypassing stacking contexts) -->
|
|
651
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
652
|
+
<div
|
|
653
|
+
bind:this={popoverElement}
|
|
654
|
+
id={listboxId}
|
|
655
|
+
class="form-autocomplete__popover"
|
|
656
|
+
popover="manual"
|
|
657
|
+
role="presentation"
|
|
658
|
+
style={popoverStyle}
|
|
659
|
+
onmousedown={(e) => e.preventDefault()}
|
|
660
|
+
>
|
|
661
|
+
<ul
|
|
662
|
+
class="form-autocomplete__listbox"
|
|
663
|
+
role="listbox"
|
|
664
|
+
aria-label="Suggestions"
|
|
665
|
+
>
|
|
666
|
+
{#if isLoading}
|
|
667
|
+
<li class="form-autocomplete__status form-autocomplete__status--loading">
|
|
668
|
+
<Icon icon="heroicons:arrow-path" class="form-autocomplete__status-icon" />
|
|
669
|
+
<span>Loading suggestions...</span>
|
|
670
|
+
</li>
|
|
671
|
+
{:else if error}
|
|
672
|
+
<li class="form-autocomplete__status form-autocomplete__status--error">
|
|
673
|
+
<Icon icon="heroicons:exclamation-triangle" class="form-autocomplete__status-icon" />
|
|
674
|
+
<span>{error}</span>
|
|
675
|
+
<button type="button" class="form-autocomplete__retry" onclick={handleRetry}>
|
|
676
|
+
Retry
|
|
677
|
+
</button>
|
|
678
|
+
</li>
|
|
679
|
+
{:else if suggestions.length === 0}
|
|
680
|
+
<li class="form-autocomplete__status form-autocomplete__status--empty">
|
|
681
|
+
<Icon icon="heroicons:magnifying-glass" class="form-autocomplete__status-icon" />
|
|
682
|
+
<span>No results found</span>
|
|
683
|
+
</li>
|
|
684
|
+
{:else}
|
|
685
|
+
{#each suggestions as option, index (option.value)}
|
|
686
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
687
|
+
<li
|
|
688
|
+
id={getOptionId(index)}
|
|
689
|
+
class="form-autocomplete__option"
|
|
690
|
+
class:form-autocomplete__option--highlighted={index === highlightedIndex}
|
|
691
|
+
class:form-autocomplete__option--selected={isSelected(option.value)}
|
|
692
|
+
role="option"
|
|
693
|
+
aria-selected={isSelected(option.value)}
|
|
694
|
+
onmouseenter={() => (highlightedIndex = index)}
|
|
695
|
+
onclick={() => selectOption(option)}
|
|
696
|
+
>
|
|
697
|
+
<span class="form-autocomplete__option-label">{option.label}</span>
|
|
698
|
+
{#if isSelected(option.value)}
|
|
699
|
+
<Icon icon="heroicons:check" class="form-autocomplete__option-check" />
|
|
700
|
+
{/if}
|
|
701
|
+
</li>
|
|
702
|
+
{/each}
|
|
703
|
+
{/if}
|
|
704
|
+
</ul>
|
|
705
|
+
</div>
|
|
706
|
+
</div>
|
|
707
|
+
|
|
708
|
+
<style>
|
|
709
|
+
.form-autocomplete {
|
|
710
|
+
position: relative;
|
|
711
|
+
width: 100%;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.form-autocomplete--disabled {
|
|
715
|
+
opacity: 0.6;
|
|
716
|
+
pointer-events: none;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* Field container styled like a textfield */
|
|
720
|
+
.form-autocomplete__field {
|
|
721
|
+
display: flex;
|
|
722
|
+
flex-wrap: wrap;
|
|
723
|
+
align-items: flex-start;
|
|
724
|
+
gap: var(--fd-space-1);
|
|
725
|
+
min-height: 2.625rem;
|
|
726
|
+
padding: var(--fd-space-1) 2.5rem var(--fd-space-1) var(--fd-space-3);
|
|
727
|
+
border: 1px solid var(--fd-border);
|
|
728
|
+
border-radius: var(--fd-radius-lg);
|
|
729
|
+
font-size: var(--fd-text-sm);
|
|
730
|
+
font-family: inherit;
|
|
731
|
+
color: var(--fd-foreground);
|
|
732
|
+
background-color: var(--fd-muted);
|
|
733
|
+
transition: all var(--fd-transition-normal);
|
|
734
|
+
box-shadow: var(--fd-shadow-sm);
|
|
735
|
+
cursor: text;
|
|
736
|
+
position: relative;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.form-autocomplete__field:hover {
|
|
740
|
+
border-color: var(--fd-border-strong);
|
|
741
|
+
background-color: var(--fd-background);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.form-autocomplete__field--focused {
|
|
745
|
+
border-color: var(--fd-primary);
|
|
746
|
+
background-color: var(--fd-background);
|
|
747
|
+
box-shadow:
|
|
748
|
+
0 0 0 3px rgba(59, 130, 246, 0.12),
|
|
749
|
+
var(--fd-shadow-sm);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/* Multiple mode - textarea-like styling */
|
|
753
|
+
.form-autocomplete--multiple .form-autocomplete__field {
|
|
754
|
+
min-height: 3rem;
|
|
755
|
+
align-content: flex-start;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/* Tags container */
|
|
759
|
+
.form-autocomplete__tags {
|
|
760
|
+
display: flex;
|
|
761
|
+
flex-wrap: wrap;
|
|
762
|
+
gap: var(--fd-space-1);
|
|
763
|
+
align-items: center;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/* Individual tag - selected item chip */
|
|
767
|
+
.form-autocomplete__tag {
|
|
768
|
+
display: inline-flex;
|
|
769
|
+
align-items: center;
|
|
770
|
+
gap: var(--fd-space-1);
|
|
771
|
+
padding: var(--fd-space-1) var(--fd-space-1) var(--fd-space-1) var(--fd-space-2);
|
|
772
|
+
background-color: var(--fd-primary-muted);
|
|
773
|
+
border: 1px solid var(--fd-primary-muted);
|
|
774
|
+
border-radius: var(--fd-radius-md);
|
|
775
|
+
font-size: 0.8125rem;
|
|
776
|
+
color: var(--fd-primary-hover);
|
|
777
|
+
line-height: 1.2;
|
|
778
|
+
max-width: 100%;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.form-autocomplete__tag-label {
|
|
782
|
+
overflow: hidden;
|
|
783
|
+
text-overflow: ellipsis;
|
|
784
|
+
white-space: nowrap;
|
|
785
|
+
max-width: 12rem;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.form-autocomplete__tag-remove {
|
|
789
|
+
display: flex;
|
|
790
|
+
align-items: center;
|
|
791
|
+
justify-content: center;
|
|
792
|
+
padding: 0.125rem;
|
|
793
|
+
margin-left: 0.125rem;
|
|
794
|
+
border: none;
|
|
795
|
+
border-radius: var(--fd-radius-sm);
|
|
796
|
+
background: transparent;
|
|
797
|
+
color: var(--fd-primary);
|
|
798
|
+
cursor: pointer;
|
|
799
|
+
transition: all var(--fd-transition-fast);
|
|
800
|
+
flex-shrink: 0;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.form-autocomplete__tag-remove:hover {
|
|
804
|
+
background-color: var(--fd-primary-muted);
|
|
805
|
+
color: var(--fd-primary-hover);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.form-autocomplete__tag-remove :global(svg) {
|
|
809
|
+
width: 0.875rem;
|
|
810
|
+
height: 0.875rem;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/* Input area */
|
|
814
|
+
.form-autocomplete__input-area {
|
|
815
|
+
flex: 1;
|
|
816
|
+
min-width: 4rem;
|
|
817
|
+
display: flex;
|
|
818
|
+
align-items: center;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.form-autocomplete__input {
|
|
822
|
+
width: 100%;
|
|
823
|
+
padding: var(--fd-space-1) 0;
|
|
824
|
+
border: none;
|
|
825
|
+
outline: none;
|
|
826
|
+
font-size: var(--fd-text-sm);
|
|
827
|
+
font-family: inherit;
|
|
828
|
+
color: var(--fd-foreground);
|
|
829
|
+
background-color: transparent;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.form-autocomplete__input::placeholder {
|
|
833
|
+
color: var(--fd-muted-foreground);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/* Status icons */
|
|
837
|
+
.form-autocomplete__icons {
|
|
838
|
+
position: absolute;
|
|
839
|
+
right: var(--fd-space-2);
|
|
840
|
+
top: 0.625rem;
|
|
841
|
+
display: flex;
|
|
842
|
+
align-items: center;
|
|
843
|
+
gap: var(--fd-space-1);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.form-autocomplete__chevron,
|
|
847
|
+
.form-autocomplete__spinner {
|
|
848
|
+
display: flex;
|
|
849
|
+
align-items: center;
|
|
850
|
+
justify-content: center;
|
|
851
|
+
color: var(--fd-muted-foreground);
|
|
852
|
+
pointer-events: none;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.form-autocomplete__chevron :global(svg),
|
|
856
|
+
.form-autocomplete__spinner :global(svg) {
|
|
857
|
+
width: 1rem;
|
|
858
|
+
height: 1rem;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.form-autocomplete__spinner {
|
|
862
|
+
animation: autocomplete-spin 1s linear infinite;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
@keyframes autocomplete-spin {
|
|
866
|
+
from {
|
|
867
|
+
transform: rotate(0deg);
|
|
868
|
+
}
|
|
869
|
+
to {
|
|
870
|
+
transform: rotate(360deg);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.form-autocomplete__clear {
|
|
875
|
+
display: flex;
|
|
876
|
+
align-items: center;
|
|
877
|
+
justify-content: center;
|
|
878
|
+
padding: var(--fd-space-1);
|
|
879
|
+
border: none;
|
|
880
|
+
border-radius: var(--fd-radius-sm);
|
|
881
|
+
background: transparent;
|
|
882
|
+
color: var(--fd-muted-foreground);
|
|
883
|
+
cursor: pointer;
|
|
884
|
+
transition: all var(--fd-transition-fast);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.form-autocomplete__clear:hover {
|
|
888
|
+
background-color: var(--fd-muted);
|
|
889
|
+
color: var(--fd-foreground);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.form-autocomplete__clear :global(svg) {
|
|
893
|
+
width: 1rem;
|
|
894
|
+
height: 1rem;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/* Popover container - renders in top layer via Popover API */
|
|
898
|
+
.form-autocomplete__popover {
|
|
899
|
+
position: fixed;
|
|
900
|
+
margin: 0;
|
|
901
|
+
padding: 0;
|
|
902
|
+
border: none;
|
|
903
|
+
background: transparent;
|
|
904
|
+
overflow: visible;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/* Reset default popover styles */
|
|
908
|
+
.form-autocomplete__popover:popover-open {
|
|
909
|
+
display: block;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/* Dropdown listbox inside popover */
|
|
913
|
+
.form-autocomplete__listbox {
|
|
914
|
+
margin: 0;
|
|
915
|
+
padding: var(--fd-space-1);
|
|
916
|
+
list-style: none;
|
|
917
|
+
background-color: var(--fd-background);
|
|
918
|
+
border: 1px solid var(--fd-border);
|
|
919
|
+
border-radius: var(--fd-radius-lg);
|
|
920
|
+
box-shadow: var(--fd-shadow-lg);
|
|
921
|
+
overflow-y: auto;
|
|
922
|
+
max-height: inherit;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.form-autocomplete__option {
|
|
926
|
+
display: flex;
|
|
927
|
+
align-items: center;
|
|
928
|
+
justify-content: space-between;
|
|
929
|
+
padding: var(--fd-space-2) var(--fd-space-3);
|
|
930
|
+
border-radius: var(--fd-radius-md);
|
|
931
|
+
cursor: pointer;
|
|
932
|
+
transition: background-color var(--fd-transition-fast);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.form-autocomplete__option:hover,
|
|
936
|
+
.form-autocomplete__option--highlighted {
|
|
937
|
+
background-color: var(--fd-muted);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
.form-autocomplete__option--selected {
|
|
941
|
+
background-color: var(--fd-primary-muted);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.form-autocomplete__option--highlighted.form-autocomplete__option--selected {
|
|
945
|
+
background-color: var(--fd-primary-muted);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.form-autocomplete__option-label {
|
|
949
|
+
font-size: var(--fd-text-sm);
|
|
950
|
+
color: var(--fd-foreground);
|
|
951
|
+
flex: 1;
|
|
952
|
+
overflow: hidden;
|
|
953
|
+
text-overflow: ellipsis;
|
|
954
|
+
white-space: nowrap;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.form-autocomplete__option :global(.form-autocomplete__option-check) {
|
|
958
|
+
width: 1rem;
|
|
959
|
+
height: 1rem;
|
|
960
|
+
color: var(--fd-primary);
|
|
961
|
+
flex-shrink: 0;
|
|
962
|
+
margin-left: var(--fd-space-2);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/* Status messages */
|
|
966
|
+
.form-autocomplete__status {
|
|
967
|
+
display: flex;
|
|
968
|
+
align-items: center;
|
|
969
|
+
gap: var(--fd-space-2);
|
|
970
|
+
padding: var(--fd-space-3);
|
|
971
|
+
font-size: var(--fd-text-sm);
|
|
972
|
+
color: var(--fd-muted-foreground);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.form-autocomplete__status--loading {
|
|
976
|
+
color: var(--fd-primary);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.form-autocomplete__status--error {
|
|
980
|
+
color: var(--fd-error);
|
|
981
|
+
flex-wrap: wrap;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.form-autocomplete__status--empty {
|
|
985
|
+
color: var(--fd-muted-foreground);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.form-autocomplete__status :global(.form-autocomplete__status-icon) {
|
|
989
|
+
width: 1rem;
|
|
990
|
+
height: 1rem;
|
|
991
|
+
flex-shrink: 0;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.form-autocomplete__status--loading :global(.form-autocomplete__status-icon) {
|
|
995
|
+
animation: autocomplete-spin 1s linear infinite;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.form-autocomplete__retry {
|
|
999
|
+
margin-left: auto;
|
|
1000
|
+
padding: var(--fd-space-1) var(--fd-space-2);
|
|
1001
|
+
border: 1px solid var(--fd-error);
|
|
1002
|
+
border-radius: var(--fd-radius-sm);
|
|
1003
|
+
background-color: transparent;
|
|
1004
|
+
color: var(--fd-error);
|
|
1005
|
+
font-size: var(--fd-text-xs);
|
|
1006
|
+
font-weight: 500;
|
|
1007
|
+
cursor: pointer;
|
|
1008
|
+
transition: all var(--fd-transition-fast);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
.form-autocomplete__retry:hover {
|
|
1012
|
+
background-color: var(--fd-error-muted);
|
|
1013
|
+
}
|
|
1014
|
+
</style>
|