twintrinsic 0.0.1
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/LICENSE +674 -0
- package/README.md +150 -0
- package/dist/App/App.svelte +54 -0
- package/dist/App/App.svelte.d.ts +65 -0
- package/dist/Section.svelte +25 -0
- package/dist/Section.svelte.d.ts +34 -0
- package/dist/actions/clickOutside.d.ts +9 -0
- package/dist/actions/clickOutside.js +19 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/components/Accordion/Accordion.svelte +75 -0
- package/dist/components/Accordion/Accordion.svelte.d.ts +39 -0
- package/dist/components/Accordion/AccordionItem.svelte +150 -0
- package/dist/components/Accordion/AccordionItem.svelte.d.ts +30 -0
- package/dist/components/App/App.story.md +8 -0
- package/dist/components/App/App.story.svelte +170 -0
- package/dist/components/App/App.story.svelte.d.ts +22 -0
- package/dist/components/App/App.svelte +77 -0
- package/dist/components/App/App.svelte.d.ts +66 -0
- package/dist/components/App/Split.svelte +346 -0
- package/dist/components/App/Split.svelte.d.ts +54 -0
- package/dist/components/App/index.d.ts +2 -0
- package/dist/components/App/index.js +3 -0
- package/dist/components/AppHeader/AppHeader.svelte +439 -0
- package/dist/components/AppHeader/AppHeader.svelte.d.ts +24 -0
- package/dist/components/Avatar/Avatar.svelte +300 -0
- package/dist/components/Avatar/Avatar.svelte.d.ts +48 -0
- package/dist/components/Avatar/AvatarGroup.svelte +185 -0
- package/dist/components/Avatar/AvatarGroup.svelte.d.ts +46 -0
- package/dist/components/Badge/Badge.svelte +186 -0
- package/dist/components/Badge/Badge.svelte.d.ts +51 -0
- package/dist/components/BottomBar/BottomBar.svelte +146 -0
- package/dist/components/BottomBar/BottomBar.svelte.d.ts +38 -0
- package/dist/components/Breadcrumb/Breadcrumb.svelte +77 -0
- package/dist/components/Breadcrumb/Breadcrumb.svelte.d.ts +42 -0
- package/dist/components/Breadcrumb/BreadcrumbItem.svelte +171 -0
- package/dist/components/Breadcrumb/BreadcrumbItem.svelte.d.ts +38 -0
- package/dist/components/Button/Button.svelte +252 -0
- package/dist/components/Button/Button.svelte.d.ts +80 -0
- package/dist/components/Button/ButtonGroup.svelte +127 -0
- package/dist/components/Button/ButtonGroup.svelte.d.ts +44 -0
- package/dist/components/Card/Card.svelte +152 -0
- package/dist/components/Card/Card.svelte.d.ts +55 -0
- package/dist/components/Carousel/Carousel.svelte +461 -0
- package/dist/components/Carousel/Carousel.svelte.d.ts +79 -0
- package/dist/components/Carousel/CarouselItem.svelte +149 -0
- package/dist/components/Carousel/CarouselItem.svelte.d.ts +35 -0
- package/dist/components/Chip/Chip.svelte +288 -0
- package/dist/components/Chip/Chip.svelte.d.ts +71 -0
- package/dist/components/Chip/ChipGroup.svelte +190 -0
- package/dist/components/Chip/ChipGroup.svelte.d.ts +71 -0
- package/dist/components/CodeBlock/CodeBlock.svelte +356 -0
- package/dist/components/CodeBlock/CodeBlock.svelte.d.ts +44 -0
- package/dist/components/CodeBlock/index.d.ts +1 -0
- package/dist/components/CodeBlock/index.js +1 -0
- package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte +145 -0
- package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte.d.ts +44 -0
- package/dist/components/CodeEditor/CodeEditor.svelte +229 -0
- package/dist/components/CodeEditor/CodeEditor.svelte.d.ts +23 -0
- package/dist/components/Combobox/Combobox.svelte +279 -0
- package/dist/components/Combobox/Combobox.svelte.d.ts +34 -0
- package/dist/components/Container/Container.svelte +45 -0
- package/dist/components/Container/Container.svelte.d.ts +36 -0
- package/dist/components/DataTable/DataTable.svelte +879 -0
- package/dist/components/DataTable/DataTable.svelte.d.ts +102 -0
- package/dist/components/Form/AutoComplete.svelte +357 -0
- package/dist/components/Form/AutoComplete.svelte.d.ts +73 -0
- package/dist/components/Form/Calendar.svelte +429 -0
- package/dist/components/Form/Calendar.svelte.d.ts +53 -0
- package/dist/components/Form/Checkbox.svelte +196 -0
- package/dist/components/Form/Checkbox.svelte.d.ts +50 -0
- package/dist/components/Form/ColorPicker.svelte +396 -0
- package/dist/components/Form/ColorPicker.svelte.d.ts +43 -0
- package/dist/components/Form/Combobox.svelte +645 -0
- package/dist/components/Form/Combobox.svelte.d.ts +93 -0
- package/dist/components/Form/Dropdown.svelte +773 -0
- package/dist/components/Form/Dropdown.svelte.d.ts +81 -0
- package/dist/components/Form/FileUpload.svelte +796 -0
- package/dist/components/Form/FileUpload.svelte.d.ts +78 -0
- package/dist/components/Form/FloatLabel.svelte +245 -0
- package/dist/components/Form/FloatLabel.svelte.d.ts +44 -0
- package/dist/components/Form/Form.svelte +281 -0
- package/dist/components/Form/Form.svelte.d.ts +54 -0
- package/dist/components/Form/FormField.svelte +218 -0
- package/dist/components/Form/FormField.svelte.d.ts +47 -0
- package/dist/components/Form/Input.svelte +340 -0
- package/dist/components/Form/Input.svelte.d.ts +79 -0
- package/dist/components/Form/InputSwitch.svelte +189 -0
- package/dist/components/Form/InputSwitch.svelte.d.ts +46 -0
- package/dist/components/Form/InvalidState.svelte +97 -0
- package/dist/components/Form/InvalidState.svelte.d.ts +37 -0
- package/dist/components/Form/Knob.svelte +537 -0
- package/dist/components/Form/Knob.svelte.d.ts +78 -0
- package/dist/components/Form/ListInput.svelte +469 -0
- package/dist/components/Form/ListInput.svelte.d.ts +70 -0
- package/dist/components/Form/Listbox.svelte +513 -0
- package/dist/components/Form/Listbox.svelte.d.ts +74 -0
- package/dist/components/Form/NumberInput.svelte +452 -0
- package/dist/components/Form/NumberInput.svelte.d.ts +82 -0
- package/dist/components/Form/Radio.svelte +192 -0
- package/dist/components/Form/Radio.svelte.d.ts +53 -0
- package/dist/components/Form/RadioGroup.svelte +155 -0
- package/dist/components/Form/RadioGroup.svelte.d.ts +48 -0
- package/dist/components/Form/Rating.svelte +380 -0
- package/dist/components/Form/Rating.svelte.d.ts +64 -0
- package/dist/components/Form/Select.svelte +436 -0
- package/dist/components/Form/Select.svelte.d.ts +49 -0
- package/dist/components/Form/SelectGroup.svelte +34 -0
- package/dist/components/Form/SelectGroup.svelte.d.ts +33 -0
- package/dist/components/Form/Slider.svelte +622 -0
- package/dist/components/Form/Slider.svelte.d.ts +73 -0
- package/dist/components/Form/Switch.svelte +192 -0
- package/dist/components/Form/Switch.svelte.d.ts +46 -0
- package/dist/components/Form/TextInput.svelte +274 -0
- package/dist/components/Form/TextInput.svelte.d.ts +74 -0
- package/dist/components/Form/Textarea.svelte +207 -0
- package/dist/components/Form/Textarea.svelte.d.ts +62 -0
- package/dist/components/Icon/Icon.svelte +140 -0
- package/dist/components/Icon/Icon.svelte.d.ts +25 -0
- package/dist/components/Icon/index.d.ts +1 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Lazy/Lazy.svelte +158 -0
- package/dist/components/Lazy/Lazy.svelte.d.ts +42 -0
- package/dist/components/Masonry/Masonry.svelte +299 -0
- package/dist/components/Masonry/Masonry.svelte.d.ts +55 -0
- package/dist/components/Menu/Menu/Menu.svelte +65 -0
- package/dist/components/Menu/Menu/Menu.svelte.d.ts +17 -0
- package/dist/components/Menu/Menu/MenuItem.svelte +90 -0
- package/dist/components/Menu/Menu/MenuItem.svelte.d.ts +27 -0
- package/dist/components/Modal/Modal.svelte +334 -0
- package/dist/components/Modal/Modal.svelte.d.ts +55 -0
- package/dist/components/Panel/Card.svelte +141 -0
- package/dist/components/Panel/Card.svelte.d.ts +52 -0
- package/dist/components/Panel/Hero/Hero.story.md +9 -0
- package/dist/components/Panel/Hero/Hero.story.svelte +49 -0
- package/dist/components/Panel/Hero/Hero.story.svelte.d.ts +21 -0
- package/dist/components/Panel/Hero/Hero.svelte +24 -0
- package/dist/components/Panel/Hero/Hero.svelte.d.ts +32 -0
- package/dist/components/Panel/LazyPanel.svelte +110 -0
- package/dist/components/Panel/LazyPanel.svelte.d.ts +46 -0
- package/dist/components/Panel/Panel.svelte +205 -0
- package/dist/components/Panel/Panel.svelte.d.ts +23 -0
- package/dist/components/Progress/Progress.svelte +220 -0
- package/dist/components/Progress/Progress.svelte.d.ts +61 -0
- package/dist/components/Separator/Separator.svelte +109 -0
- package/dist/components/Separator/Separator.svelte.d.ts +35 -0
- package/dist/components/Sidebar/Sidebar.svelte +213 -0
- package/dist/components/Sidebar/Sidebar.svelte.d.ts +60 -0
- package/dist/components/Skeleton/Skeleton.svelte +170 -0
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +48 -0
- package/dist/components/Stepper/Stepper.svelte +111 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +54 -0
- package/dist/components/Stepper/StepperStep.svelte +369 -0
- package/dist/components/Stepper/StepperStep.svelte.d.ts +63 -0
- package/dist/components/Table/Table.svelte +167 -0
- package/dist/components/Table/Table.svelte.d.ts +56 -0
- package/dist/components/Table/TableBody.svelte +41 -0
- package/dist/components/Table/TableBody.svelte.d.ts +33 -0
- package/dist/components/Table/TableCell.svelte +76 -0
- package/dist/components/Table/TableCell.svelte.d.ts +36 -0
- package/dist/components/Table/TableHead.svelte +41 -0
- package/dist/components/Table/TableHead.svelte.d.ts +32 -0
- package/dist/components/Table/TableHeader.svelte +148 -0
- package/dist/components/Table/TableHeader.svelte.d.ts +42 -0
- package/dist/components/Table/TableRow.svelte +99 -0
- package/dist/components/Table/TableRow.svelte.d.ts +40 -0
- package/dist/components/Tabs/Tab.svelte +145 -0
- package/dist/components/Tabs/Tab.svelte.d.ts +36 -0
- package/dist/components/Tabs/TabList.svelte +60 -0
- package/dist/components/Tabs/TabList.svelte.d.ts +32 -0
- package/dist/components/Tabs/TabPanel.svelte +118 -0
- package/dist/components/Tabs/TabPanel.svelte.d.ts +38 -0
- package/dist/components/Tabs/Tabs.svelte +287 -0
- package/dist/components/Tabs/Tabs.svelte.d.ts +50 -0
- package/dist/components/Tag/Tag.svelte +260 -0
- package/dist/components/Tag/Tag.svelte.d.ts +54 -0
- package/dist/components/Tag/TagGroup.svelte +147 -0
- package/dist/components/Tag/TagGroup.svelte.d.ts +62 -0
- package/dist/components/ThemeToggle/ThemeToggle.svelte +93 -0
- package/dist/components/ThemeToggle/ThemeToggle.svelte.d.ts +12 -0
- package/dist/components/Timeline/Timeline.svelte +144 -0
- package/dist/components/Timeline/Timeline.svelte.d.ts +48 -0
- package/dist/components/Timeline/TimelineItem.svelte +391 -0
- package/dist/components/Timeline/TimelineItem.svelte.d.ts +63 -0
- package/dist/components/Toast/Toast.svelte +313 -0
- package/dist/components/Toast/Toast.svelte.d.ts +44 -0
- package/dist/components/Toast/toastStore.d.ts +40 -0
- package/dist/components/Toast/toastStore.js +293 -0
- package/dist/components/Tooltip/Tooltip.svelte +282 -0
- package/dist/components/Tooltip/Tooltip.svelte.d.ts +55 -0
- package/dist/components/Tree/Tree.svelte +129 -0
- package/dist/components/Tree/Tree.svelte.d.ts +61 -0
- package/dist/components/Tree/TreeNode.svelte +332 -0
- package/dist/components/Tree/TreeNode.svelte.d.ts +55 -0
- package/dist/components/icons/TwintrinsicLogo.svelte +73 -0
- package/dist/components/icons/TwintrinsicLogo.svelte.d.ts +17 -0
- package/dist/components/icons/twintrinsic-source.svg +73 -0
- package/dist/components/icons/twintrinsic.svg +38 -0
- package/dist/docs/EventsTable.svelte +86 -0
- package/dist/docs/EventsTable.svelte.d.ts +27 -0
- package/dist/docs/PropsTable.svelte +103 -0
- package/dist/docs/PropsTable.svelte.d.ts +28 -0
- package/dist/docs/index.d.ts +2 -0
- package/dist/docs/index.js +2 -0
- package/dist/helpers/detectLanguage.d.ts +6 -0
- package/dist/helpers/detectLanguage.js +60 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +94 -0
- package/dist/twintrinsic.css +347 -0
- package/package.json +98 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Combobox - A component that combines a text input with a dropdown list.
|
|
4
|
+
Provides autocomplete functionality with keyboard navigation and accessibility features.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
```svelte
|
|
8
|
+
<Combobox
|
|
9
|
+
options={['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']}
|
|
10
|
+
placeholder="Select a fruit"
|
|
11
|
+
onchange={(e) => console.log(e.detail.value)}
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
<Combobox
|
|
15
|
+
options={users}
|
|
16
|
+
optionLabel="name"
|
|
17
|
+
optionValue="id"
|
|
18
|
+
placeholder="Select a user"
|
|
19
|
+
onchange={(e) => console.log(e.detail.value)}
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
<Combobox
|
|
23
|
+
options={countries}
|
|
24
|
+
optionLabel="name"
|
|
25
|
+
optionValue="code"
|
|
26
|
+
placeholder="Select a country"
|
|
27
|
+
searchable
|
|
28
|
+
clearable
|
|
29
|
+
let:option
|
|
30
|
+
>
|
|
31
|
+
<div class="flex items-center">
|
|
32
|
+
<img src={option.flag} alt={option.name} class="w-5 h-5 mr-2" />
|
|
33
|
+
{option.name}
|
|
34
|
+
</div>
|
|
35
|
+
</Combobox>
|
|
36
|
+
```
|
|
37
|
+
-->
|
|
38
|
+
<script>
|
|
39
|
+
import { tick } from "svelte"
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
/** @type {string} - Additional CSS classes */
|
|
43
|
+
class: className = "",
|
|
44
|
+
|
|
45
|
+
/** @type {string} - HTML id for accessibility */
|
|
46
|
+
id = crypto.randomUUID(),
|
|
47
|
+
|
|
48
|
+
/** @type {string} - Name attribute for the input */
|
|
49
|
+
name,
|
|
50
|
+
|
|
51
|
+
/** @type {Array} - Options to display in the dropdown */
|
|
52
|
+
options = [],
|
|
53
|
+
|
|
54
|
+
/** @type {any} - Current value */
|
|
55
|
+
value = null,
|
|
56
|
+
|
|
57
|
+
/** @type {string} - Placeholder text */
|
|
58
|
+
placeholder = "Select an option",
|
|
59
|
+
|
|
60
|
+
/** @type {string} - Property name for option labels */
|
|
61
|
+
optionLabel = "label",
|
|
62
|
+
|
|
63
|
+
/** @type {string} - Property name for option values */
|
|
64
|
+
optionValue = "value",
|
|
65
|
+
|
|
66
|
+
/** @type {boolean} - Whether the combobox is disabled */
|
|
67
|
+
disabled = false,
|
|
68
|
+
|
|
69
|
+
/** @type {boolean} - Whether the combobox is readonly */
|
|
70
|
+
readonly = false,
|
|
71
|
+
|
|
72
|
+
/** @type {boolean} - Whether the combobox is required */
|
|
73
|
+
required = false,
|
|
74
|
+
|
|
75
|
+
/** @type {boolean} - Whether to allow searching */
|
|
76
|
+
searchable = true,
|
|
77
|
+
|
|
78
|
+
/** @type {boolean} - Whether to allow clearing the selection */
|
|
79
|
+
clearable = true,
|
|
80
|
+
|
|
81
|
+
/** @type {boolean} - Whether to show a loading indicator */
|
|
82
|
+
loading = false,
|
|
83
|
+
|
|
84
|
+
/** @type {boolean} - Whether to automatically select the first option */
|
|
85
|
+
autoSelect = false,
|
|
86
|
+
|
|
87
|
+
/** @type {boolean} - Whether to open the dropdown on focus */
|
|
88
|
+
openOnFocus = true,
|
|
89
|
+
|
|
90
|
+
/** @type {number} - Maximum height of the dropdown in pixels */
|
|
91
|
+
maxHeight = 250,
|
|
92
|
+
|
|
93
|
+
/** @type {string} - ARIA label for the combobox */
|
|
94
|
+
ariaLabel,
|
|
95
|
+
|
|
96
|
+
/** @type {Function} - Custom filter function */
|
|
97
|
+
filter,
|
|
98
|
+
|
|
99
|
+
/** @type {Function} - Custom template for options */
|
|
100
|
+
optionTemplate,
|
|
101
|
+
|
|
102
|
+
/** @type {Function} - Custom template for selected value */
|
|
103
|
+
valueTemplate,
|
|
104
|
+
|
|
105
|
+
/** @type {(event: CustomEvent) => void} - Change event handler */
|
|
106
|
+
onchange,
|
|
107
|
+
/** @type {(event: CustomEvent) => void} - Input event handler */
|
|
108
|
+
oninput,
|
|
109
|
+
|
|
110
|
+
option,
|
|
111
|
+
} = $props()
|
|
112
|
+
|
|
113
|
+
// Component state
|
|
114
|
+
let inputElement = $state()
|
|
115
|
+
let dropdownElement = $state()
|
|
116
|
+
let isOpen = $state(false)
|
|
117
|
+
let inputValue = $state("")
|
|
118
|
+
let selectedOption = $state(null)
|
|
119
|
+
let highlightedIndex = $state(-1)
|
|
120
|
+
let filteredOptions = $state([])
|
|
121
|
+
let inputWidth = $state(0)
|
|
122
|
+
|
|
123
|
+
// Update selected option when value prop changes
|
|
124
|
+
$effect(() => {
|
|
125
|
+
if (value !== undefined && value !== null) {
|
|
126
|
+
const option = findOptionByValue(value)
|
|
127
|
+
selectedOption = option
|
|
128
|
+
inputValue = option ? getOptionLabel(option) : ""
|
|
129
|
+
} else {
|
|
130
|
+
selectedOption = null
|
|
131
|
+
if (!isOpen) {
|
|
132
|
+
inputValue = ""
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Update filtered options when input value changes
|
|
138
|
+
$effect(() => {
|
|
139
|
+
if (searchable && isOpen) {
|
|
140
|
+
filteredOptions = filterOptions(inputValue)
|
|
141
|
+
highlightedIndex = autoSelect && filteredOptions.length > 0 ? 0 : -1
|
|
142
|
+
} else {
|
|
143
|
+
filteredOptions = [...options]
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Update input width when element is mounted
|
|
148
|
+
$effect(() => {
|
|
149
|
+
if (inputElement) {
|
|
150
|
+
inputWidth = inputElement.offsetWidth
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Finds an option by its value
|
|
156
|
+
* @param {any} value - Value to find
|
|
157
|
+
* @returns {Object|null} - Found option or null
|
|
158
|
+
*/
|
|
159
|
+
function findOptionByValue(value) {
|
|
160
|
+
if (value === null || value === undefined) return null
|
|
161
|
+
|
|
162
|
+
return options.find((option) => {
|
|
163
|
+
const optionValue = getOptionValue(option)
|
|
164
|
+
return optionValue === value
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gets the label for an option
|
|
170
|
+
* @param {Object|string} option - Option to get label for
|
|
171
|
+
* @returns {string} - Option label
|
|
172
|
+
*/
|
|
173
|
+
function getOptionLabel(option) {
|
|
174
|
+
if (!option) return ""
|
|
175
|
+
|
|
176
|
+
if (typeof option === "string" || typeof option === "number") {
|
|
177
|
+
return String(option)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return option[optionLabel] || ""
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Gets the value for an option
|
|
185
|
+
* @param {Object|string} option - Option to get value for
|
|
186
|
+
* @returns {any} - Option value
|
|
187
|
+
*/
|
|
188
|
+
function getOptionValue(option) {
|
|
189
|
+
if (!option) return null
|
|
190
|
+
|
|
191
|
+
if (typeof option === "string" || typeof option === "number") {
|
|
192
|
+
return option
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return option[optionValue]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Filters options based on input value
|
|
200
|
+
* @param {string} query - Query to filter by
|
|
201
|
+
* @returns {Array} - Filtered options
|
|
202
|
+
*/
|
|
203
|
+
function filterOptions(query) {
|
|
204
|
+
if (!query) return [...options]
|
|
205
|
+
|
|
206
|
+
if (filter) {
|
|
207
|
+
return filter(options, query)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return options.filter((option) => {
|
|
211
|
+
const label = getOptionLabel(option).toLowerCase()
|
|
212
|
+
return label.includes(query.toLowerCase())
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handles input focus
|
|
218
|
+
*/
|
|
219
|
+
function handleFocus() {
|
|
220
|
+
if (disabled || readonly) return
|
|
221
|
+
|
|
222
|
+
if (openOnFocus) {
|
|
223
|
+
openDropdown()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Handles input blur
|
|
229
|
+
*/
|
|
230
|
+
function handleBlur(event) {
|
|
231
|
+
// Close dropdown after a short delay to allow for click events
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
if (
|
|
234
|
+
document.activeElement !== inputElement &&
|
|
235
|
+
!dropdownElement?.contains(document.activeElement)
|
|
236
|
+
) {
|
|
237
|
+
closeDropdown()
|
|
238
|
+
|
|
239
|
+
// Reset input value if no option is selected
|
|
240
|
+
if (!selectedOption) {
|
|
241
|
+
inputValue = ""
|
|
242
|
+
} else {
|
|
243
|
+
inputValue = getOptionLabel(selectedOption)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}, 100)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Handles input change
|
|
251
|
+
* @param {Event} event - Input event
|
|
252
|
+
*/
|
|
253
|
+
function handleInput(event) {
|
|
254
|
+
if (disabled || readonly) return
|
|
255
|
+
|
|
256
|
+
inputValue = event.target.value
|
|
257
|
+
|
|
258
|
+
if (!isOpen) {
|
|
259
|
+
openDropdown()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// If input is cleared, clear selection
|
|
263
|
+
if (!inputValue && selectedOption) {
|
|
264
|
+
selectedOption = null
|
|
265
|
+
onchange?.(new CustomEvent("change", { detail: { value: null } }))
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Handles keydown events
|
|
271
|
+
* @param {KeyboardEvent} event - Keydown event
|
|
272
|
+
*/
|
|
273
|
+
async function handleKeydown(event) {
|
|
274
|
+
if (disabled || readonly) return
|
|
275
|
+
|
|
276
|
+
switch (event.key) {
|
|
277
|
+
case "ArrowDown":
|
|
278
|
+
event.preventDefault()
|
|
279
|
+
if (!isOpen) {
|
|
280
|
+
openDropdown()
|
|
281
|
+
} else {
|
|
282
|
+
highlightedIndex = Math.min(highlightedIndex + 1, filteredOptions.length - 1)
|
|
283
|
+
await scrollToHighlighted()
|
|
284
|
+
}
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
case "ArrowUp":
|
|
288
|
+
event.preventDefault()
|
|
289
|
+
if (!isOpen) {
|
|
290
|
+
openDropdown()
|
|
291
|
+
} else {
|
|
292
|
+
highlightedIndex = Math.max(highlightedIndex - 1, 0)
|
|
293
|
+
await scrollToHighlighted()
|
|
294
|
+
}
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
case "Enter":
|
|
298
|
+
event.preventDefault()
|
|
299
|
+
if (isOpen && highlightedIndex >= 0) {
|
|
300
|
+
selectOption(filteredOptions[highlightedIndex])
|
|
301
|
+
} else if (!isOpen) {
|
|
302
|
+
openDropdown()
|
|
303
|
+
}
|
|
304
|
+
break
|
|
305
|
+
|
|
306
|
+
case "Escape":
|
|
307
|
+
event.preventDefault()
|
|
308
|
+
if (isOpen) {
|
|
309
|
+
closeDropdown()
|
|
310
|
+
// Reset input value to selected option
|
|
311
|
+
inputValue = selectedOption ? getOptionLabel(selectedOption) : ""
|
|
312
|
+
}
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
case "Tab":
|
|
316
|
+
if (isOpen) {
|
|
317
|
+
closeDropdown()
|
|
318
|
+
// Reset input value to selected option
|
|
319
|
+
inputValue = selectedOption ? getOptionLabel(selectedOption) : ""
|
|
320
|
+
}
|
|
321
|
+
break
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Scrolls to the highlighted option
|
|
327
|
+
*/
|
|
328
|
+
async function scrollToHighlighted() {
|
|
329
|
+
await tick()
|
|
330
|
+
|
|
331
|
+
if (dropdownElement && highlightedIndex >= 0) {
|
|
332
|
+
const highlightedEl = dropdownElement.querySelector(`[data-index="${highlightedIndex}"]`)
|
|
333
|
+
if (highlightedEl) {
|
|
334
|
+
const containerRect = dropdownElement.getBoundingClientRect()
|
|
335
|
+
const optionRect = highlightedEl.getBoundingClientRect()
|
|
336
|
+
|
|
337
|
+
if (optionRect.bottom > containerRect.bottom) {
|
|
338
|
+
dropdownElement.scrollTop += optionRect.bottom - containerRect.bottom
|
|
339
|
+
} else if (optionRect.top < containerRect.top) {
|
|
340
|
+
dropdownElement.scrollTop -= containerRect.top - optionRect.top
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Opens the dropdown
|
|
348
|
+
*/
|
|
349
|
+
function openDropdown() {
|
|
350
|
+
if (disabled || readonly) return
|
|
351
|
+
|
|
352
|
+
isOpen = true
|
|
353
|
+
filteredOptions = filterOptions(inputValue)
|
|
354
|
+
highlightedIndex = autoSelect && filteredOptions.length > 0 ? 0 : -1
|
|
355
|
+
|
|
356
|
+
// Focus input if not already focused
|
|
357
|
+
if (document.activeElement !== inputElement) {
|
|
358
|
+
inputElement.focus()
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Closes the dropdown
|
|
364
|
+
*/
|
|
365
|
+
function closeDropdown() {
|
|
366
|
+
isOpen = false
|
|
367
|
+
highlightedIndex = -1
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Selects an option
|
|
372
|
+
* @param {Object|string} option - Option to select
|
|
373
|
+
*/
|
|
374
|
+
function selectOption(option) {
|
|
375
|
+
selectedOption = option
|
|
376
|
+
inputValue = getOptionLabel(option)
|
|
377
|
+
closeDropdown()
|
|
378
|
+
|
|
379
|
+
const value = getOptionValue(option)
|
|
380
|
+
onchange?.(new CustomEvent("change", { detail: { value, option } }))
|
|
381
|
+
oninput?.(new CustomEvent("input", { detail: { value, option } }))
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Clears the selection
|
|
386
|
+
* @param {Event} event - Click event
|
|
387
|
+
*/
|
|
388
|
+
function clearSelection(event) {
|
|
389
|
+
event.stopPropagation()
|
|
390
|
+
|
|
391
|
+
if (disabled || readonly) return
|
|
392
|
+
|
|
393
|
+
selectedOption = null
|
|
394
|
+
inputValue = ""
|
|
395
|
+
|
|
396
|
+
onchange?.(new CustomEvent("change", { detail: { value: null } }))
|
|
397
|
+
oninput?.(new CustomEvent("input", { detail: { value: null } }))
|
|
398
|
+
|
|
399
|
+
// Focus input after clearing
|
|
400
|
+
inputElement.focus()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Toggles the dropdown
|
|
405
|
+
*/
|
|
406
|
+
function toggleDropdown() {
|
|
407
|
+
if (disabled || readonly) return
|
|
408
|
+
|
|
409
|
+
if (isOpen) {
|
|
410
|
+
closeDropdown()
|
|
411
|
+
} else {
|
|
412
|
+
openDropdown()
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
</script>
|
|
416
|
+
|
|
417
|
+
<div
|
|
418
|
+
class="
|
|
419
|
+
combobox
|
|
420
|
+
{disabled ? 'combobox-disabled' : ''}
|
|
421
|
+
{readonly ? 'combobox-readonly' : ''}
|
|
422
|
+
{isOpen ? 'combobox-open' : ''}
|
|
423
|
+
{className}
|
|
424
|
+
"
|
|
425
|
+
>
|
|
426
|
+
<div class="combobox-input-container">
|
|
427
|
+
<input
|
|
428
|
+
{id}
|
|
429
|
+
{name}
|
|
430
|
+
type="text"
|
|
431
|
+
class="combobox-input"
|
|
432
|
+
placeholder={placeholder}
|
|
433
|
+
value={inputValue}
|
|
434
|
+
aria-label={ariaLabel || placeholder}
|
|
435
|
+
aria-autocomplete="list"
|
|
436
|
+
aria-controls={`${id}-listbox`}
|
|
437
|
+
aria-expanded={isOpen}
|
|
438
|
+
aria-activedescendant={highlightedIndex >= 0 ? `${id}-option-${highlightedIndex}` : undefined}
|
|
439
|
+
role="combobox"
|
|
440
|
+
autocomplete="off"
|
|
441
|
+
{disabled}
|
|
442
|
+
{readonly}
|
|
443
|
+
{required}
|
|
444
|
+
onfocus={handleFocus}
|
|
445
|
+
onblur={handleBlur}
|
|
446
|
+
oninput={handleInput}
|
|
447
|
+
onkeydown={handleKeydown}
|
|
448
|
+
bind:this={inputElement}
|
|
449
|
+
/>
|
|
450
|
+
|
|
451
|
+
<div class="combobox-actions">
|
|
452
|
+
{#if loading}
|
|
453
|
+
<div class="combobox-loading">
|
|
454
|
+
<svg class="combobox-spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
455
|
+
<circle class="combobox-spinner-track" cx="12" cy="12" r="10" />
|
|
456
|
+
<circle class="combobox-spinner-path" cx="12" cy="12" r="10" />
|
|
457
|
+
</svg>
|
|
458
|
+
</div>
|
|
459
|
+
{/if}
|
|
460
|
+
|
|
461
|
+
{#if clearable && selectedOption && !disabled && !readonly}
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
class="combobox-clear"
|
|
465
|
+
aria-label="Clear selection"
|
|
466
|
+
onclick={clearSelection}
|
|
467
|
+
>
|
|
468
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
469
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
470
|
+
</svg>
|
|
471
|
+
</button>
|
|
472
|
+
{/if}
|
|
473
|
+
|
|
474
|
+
<button
|
|
475
|
+
type="button"
|
|
476
|
+
class="combobox-toggle"
|
|
477
|
+
aria-label={isOpen ? 'Close dropdown' : 'Open dropdown'}
|
|
478
|
+
onclick={toggleDropdown}
|
|
479
|
+
disabled={disabled || readonly}
|
|
480
|
+
>
|
|
481
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
482
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={isOpen ? "M5 15l7-7 7 7" : "M19 9l-7 7-7-7"}></path>
|
|
483
|
+
</svg>
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
{#if isOpen}
|
|
489
|
+
<div
|
|
490
|
+
id={`${id}-listbox`}
|
|
491
|
+
class="combobox-dropdown"
|
|
492
|
+
role="listbox"
|
|
493
|
+
style="max-height: {maxHeight}px; width: {inputWidth}px;"
|
|
494
|
+
bind:this={dropdownElement}
|
|
495
|
+
>
|
|
496
|
+
{#if filteredOptions.length === 0}
|
|
497
|
+
<div class="combobox-empty">
|
|
498
|
+
No options available
|
|
499
|
+
</div>
|
|
500
|
+
{:else}
|
|
501
|
+
{#each filteredOptions as option, i}
|
|
502
|
+
<div
|
|
503
|
+
id={`${id}-option-${i}`}
|
|
504
|
+
class="
|
|
505
|
+
combobox-option
|
|
506
|
+
{i === highlightedIndex ? 'combobox-option-highlighted' : ''}
|
|
507
|
+
{selectedOption && getOptionValue(selectedOption) === getOptionValue(option) ? 'combobox-option-selected' : ''}
|
|
508
|
+
"
|
|
509
|
+
role="option"
|
|
510
|
+
aria-selected={selectedOption && getOptionValue(selectedOption) === getOptionValue(option)}
|
|
511
|
+
data-index={i}
|
|
512
|
+
onclick={() => selectOption(option)}
|
|
513
|
+
onmouseenter={() => highlightedIndex = i}
|
|
514
|
+
>
|
|
515
|
+
{#if optionTemplate}
|
|
516
|
+
{@render optionTemplate(option)}
|
|
517
|
+
{:else if option}
|
|
518
|
+
{@render option?.({ option })}
|
|
519
|
+
{:else}
|
|
520
|
+
{getOptionLabel(option)}
|
|
521
|
+
{/if}
|
|
522
|
+
</div>
|
|
523
|
+
{/each}
|
|
524
|
+
{/if}
|
|
525
|
+
</div>
|
|
526
|
+
{/if}
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<style>
|
|
530
|
+
@reference "../../twintrinsic.css";
|
|
531
|
+
|
|
532
|
+
.combobox {
|
|
533
|
+
@apply relative w-full;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.combobox-disabled {
|
|
537
|
+
@apply opacity-50 cursor-not-allowed;
|
|
538
|
+
@apply pointer-events-none;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.combobox-readonly {
|
|
542
|
+
@apply cursor-default;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.combobox-input-container {
|
|
546
|
+
@apply relative flex items-center;
|
|
547
|
+
@apply w-full;
|
|
548
|
+
@apply bg-background dark:bg-background;
|
|
549
|
+
@apply border border-border dark:border-border;
|
|
550
|
+
@apply rounded-md;
|
|
551
|
+
@apply transition-colors duration-150;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.combobox-open .combobox-input-container {
|
|
555
|
+
@apply border-primary-500 dark:border-primary-500;
|
|
556
|
+
@apply ring-2 ring-primary-500/20 dark:ring-primary-500/20;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.combobox-input {
|
|
560
|
+
@apply w-full py-2 pl-3 pr-10;
|
|
561
|
+
@apply bg-transparent;
|
|
562
|
+
@apply text-text dark:text-text;
|
|
563
|
+
@apply border-none;
|
|
564
|
+
@apply focus:outline-none;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.combobox-actions {
|
|
568
|
+
@apply absolute right-2;
|
|
569
|
+
@apply flex items-center;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.combobox-loading {
|
|
573
|
+
@apply mr-1;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.combobox-spinner {
|
|
577
|
+
@apply w-4 h-4;
|
|
578
|
+
@apply animate-spin;
|
|
579
|
+
@apply text-muted dark:text-muted;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.combobox-spinner-track {
|
|
583
|
+
@apply opacity-25;
|
|
584
|
+
@apply stroke-current;
|
|
585
|
+
@apply fill-none;
|
|
586
|
+
@apply stroke-2;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.combobox-spinner-path {
|
|
590
|
+
@apply opacity-75;
|
|
591
|
+
@apply stroke-current;
|
|
592
|
+
@apply fill-none;
|
|
593
|
+
@apply stroke-2;
|
|
594
|
+
stroke-dasharray: 60;
|
|
595
|
+
stroke-dashoffset: 45;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.combobox-clear {
|
|
599
|
+
@apply p-1 mr-1;
|
|
600
|
+
@apply text-muted dark:text-muted;
|
|
601
|
+
@apply hover:text-text dark:hover:text-text;
|
|
602
|
+
@apply rounded-full;
|
|
603
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400;
|
|
604
|
+
@apply transition-colors duration-150;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.combobox-toggle {
|
|
608
|
+
@apply p-1;
|
|
609
|
+
@apply text-muted dark:text-muted;
|
|
610
|
+
@apply hover:text-text dark:hover:text-text;
|
|
611
|
+
@apply rounded-full;
|
|
612
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400;
|
|
613
|
+
@apply transition-colors duration-150;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.combobox-dropdown {
|
|
617
|
+
@apply absolute z-50 mt-1;
|
|
618
|
+
@apply overflow-auto;
|
|
619
|
+
@apply bg-background dark:bg-background;
|
|
620
|
+
@apply border border-border dark:border-border;
|
|
621
|
+
@apply rounded-md shadow-lg;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.combobox-empty {
|
|
625
|
+
@apply py-2 px-3;
|
|
626
|
+
@apply text-muted dark:text-muted;
|
|
627
|
+
@apply text-center;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.combobox-option {
|
|
631
|
+
@apply py-2 px-3;
|
|
632
|
+
@apply cursor-pointer;
|
|
633
|
+
@apply text-text dark:text-text;
|
|
634
|
+
@apply hover:bg-hover dark:hover:bg-hover;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.combobox-option-highlighted {
|
|
638
|
+
@apply bg-hover dark:bg-hover;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.combobox-option-selected {
|
|
642
|
+
@apply bg-primary-50 dark:bg-primary-900/20;
|
|
643
|
+
@apply text-primary-700 dark:text-primary-300;
|
|
644
|
+
}
|
|
645
|
+
</style>
|