extonlawrence commited on
Commit
a02aa32
·
1 Parent(s): 577860c

Add ComboboxField component

Browse files
src/lib/components/ComboboxField.svelte ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CarbonChevronDown from "~icons/carbon/chevron-down";
3
+
4
+ interface Props {
5
+ label: string;
6
+ value: string;
7
+ suggestions: string[];
8
+ disabled?: boolean;
9
+ maxlength?: number;
10
+ placeholder?: string;
11
+ onChange: (value: string) => void;
12
+ }
13
+
14
+ let { label, value, suggestions, disabled = false, maxlength, placeholder = "Type or select...", onChange }: Props = $props();
15
+
16
+ let showSuggestions = $state(false);
17
+ let inputValue = $state(value);
18
+ let focusedIndex = $state(-1);
19
+
20
+ // Update inputValue when value prop changes (e.g., when switching personas)
21
+ $effect(() => {
22
+ inputValue = value;
23
+ });
24
+
25
+ let filteredSuggestions = $derived(
26
+ suggestions.filter(s =>
27
+ s.toLowerCase().includes(inputValue.toLowerCase())
28
+ ).slice(0, 10) // Limit to 10 suggestions
29
+ );
30
+
31
+ function handleInput(e: Event) {
32
+ const target = e.target as HTMLInputElement;
33
+ inputValue = target.value;
34
+ showSuggestions = true;
35
+ focusedIndex = -1;
36
+ }
37
+
38
+ function selectSuggestion(suggestion: string) {
39
+ inputValue = suggestion;
40
+ showSuggestions = false;
41
+ onChange(suggestion);
42
+ }
43
+
44
+ function handleBlur() {
45
+ // Delay to allow click on suggestion
46
+ setTimeout(() => {
47
+ showSuggestions = false;
48
+ if (inputValue !== value) {
49
+ onChange(inputValue);
50
+ }
51
+ }, 200);
52
+ }
53
+
54
+ function handleFocus() {
55
+ if (filteredSuggestions.length > 0) {
56
+ showSuggestions = true;
57
+ }
58
+ }
59
+
60
+ function handleKeydown(e: KeyboardEvent) {
61
+ if (!showSuggestions || filteredSuggestions.length === 0) return;
62
+
63
+ if (e.key === 'ArrowDown') {
64
+ e.preventDefault();
65
+ focusedIndex = Math.min(focusedIndex + 1, filteredSuggestions.length - 1);
66
+ } else if (e.key === 'ArrowUp') {
67
+ e.preventDefault();
68
+ focusedIndex = Math.max(focusedIndex - 1, -1);
69
+ } else if (e.key === 'Enter' && focusedIndex >= 0) {
70
+ e.preventDefault();
71
+ selectSuggestion(filteredSuggestions[focusedIndex]);
72
+ } else if (e.key === 'Escape') {
73
+ showSuggestions = false;
74
+ }
75
+ }
76
+ </script>
77
+
78
+ <div class="flex flex-col gap-2 relative">
79
+ <label for={label.toLowerCase().replace(/\s+/g, '-')} class="text-sm font-medium text-gray-700 dark:text-gray-300">
80
+ {label}
81
+ </label>
82
+
83
+ <div class="relative">
84
+ <input
85
+ id={label.toLowerCase().replace(/\s+/g, '-')}
86
+ type="text"
87
+ value={inputValue}
88
+ {disabled}
89
+ {maxlength}
90
+ {placeholder}
91
+ oninput={handleInput}
92
+ onblur={handleBlur}
93
+ onfocus={handleFocus}
94
+ onkeydown={handleKeydown}
95
+ class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 pr-10 text-sm transition-colors focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 disabled:cursor-not-allowed disabled:opacity-60 dark:border-gray-600 dark:bg-gray-800"
96
+ />
97
+ {#if !disabled}
98
+ <CarbonChevronDown
99
+ class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 transition-transform dark:text-gray-500 {showSuggestions ? 'rotate-180' : ''}"
100
+ />
101
+ {/if}
102
+ </div>
103
+
104
+ {#if showSuggestions && filteredSuggestions.length > 0 && !disabled}
105
+ <div class="absolute top-full left-0 right-0 z-40 mt-1 max-h-60 overflow-auto rounded-md border border-gray-300 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800">
106
+ {#each filteredSuggestions as suggestion, index}
107
+ <button
108
+ type="button"
109
+ class="w-full px-3 py-2 text-left text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 {index === focusedIndex ? 'bg-gray-100 dark:bg-gray-700' : ''}"
110
+ onclick={() => selectSuggestion(suggestion)}
111
+ >
112
+ {suggestion}
113
+ </button>
114
+ {/each}
115
+ </div>
116
+ {/if}
117
+ </div>
118
+