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.
Files changed (212) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +150 -0
  3. package/dist/App/App.svelte +54 -0
  4. package/dist/App/App.svelte.d.ts +65 -0
  5. package/dist/Section.svelte +25 -0
  6. package/dist/Section.svelte.d.ts +34 -0
  7. package/dist/actions/clickOutside.d.ts +9 -0
  8. package/dist/actions/clickOutside.js +19 -0
  9. package/dist/actions/index.d.ts +1 -0
  10. package/dist/actions/index.js +1 -0
  11. package/dist/components/Accordion/Accordion.svelte +75 -0
  12. package/dist/components/Accordion/Accordion.svelte.d.ts +39 -0
  13. package/dist/components/Accordion/AccordionItem.svelte +150 -0
  14. package/dist/components/Accordion/AccordionItem.svelte.d.ts +30 -0
  15. package/dist/components/App/App.story.md +8 -0
  16. package/dist/components/App/App.story.svelte +170 -0
  17. package/dist/components/App/App.story.svelte.d.ts +22 -0
  18. package/dist/components/App/App.svelte +77 -0
  19. package/dist/components/App/App.svelte.d.ts +66 -0
  20. package/dist/components/App/Split.svelte +346 -0
  21. package/dist/components/App/Split.svelte.d.ts +54 -0
  22. package/dist/components/App/index.d.ts +2 -0
  23. package/dist/components/App/index.js +3 -0
  24. package/dist/components/AppHeader/AppHeader.svelte +439 -0
  25. package/dist/components/AppHeader/AppHeader.svelte.d.ts +24 -0
  26. package/dist/components/Avatar/Avatar.svelte +300 -0
  27. package/dist/components/Avatar/Avatar.svelte.d.ts +48 -0
  28. package/dist/components/Avatar/AvatarGroup.svelte +185 -0
  29. package/dist/components/Avatar/AvatarGroup.svelte.d.ts +46 -0
  30. package/dist/components/Badge/Badge.svelte +186 -0
  31. package/dist/components/Badge/Badge.svelte.d.ts +51 -0
  32. package/dist/components/BottomBar/BottomBar.svelte +146 -0
  33. package/dist/components/BottomBar/BottomBar.svelte.d.ts +38 -0
  34. package/dist/components/Breadcrumb/Breadcrumb.svelte +77 -0
  35. package/dist/components/Breadcrumb/Breadcrumb.svelte.d.ts +42 -0
  36. package/dist/components/Breadcrumb/BreadcrumbItem.svelte +171 -0
  37. package/dist/components/Breadcrumb/BreadcrumbItem.svelte.d.ts +38 -0
  38. package/dist/components/Button/Button.svelte +252 -0
  39. package/dist/components/Button/Button.svelte.d.ts +80 -0
  40. package/dist/components/Button/ButtonGroup.svelte +127 -0
  41. package/dist/components/Button/ButtonGroup.svelte.d.ts +44 -0
  42. package/dist/components/Card/Card.svelte +152 -0
  43. package/dist/components/Card/Card.svelte.d.ts +55 -0
  44. package/dist/components/Carousel/Carousel.svelte +461 -0
  45. package/dist/components/Carousel/Carousel.svelte.d.ts +79 -0
  46. package/dist/components/Carousel/CarouselItem.svelte +149 -0
  47. package/dist/components/Carousel/CarouselItem.svelte.d.ts +35 -0
  48. package/dist/components/Chip/Chip.svelte +288 -0
  49. package/dist/components/Chip/Chip.svelte.d.ts +71 -0
  50. package/dist/components/Chip/ChipGroup.svelte +190 -0
  51. package/dist/components/Chip/ChipGroup.svelte.d.ts +71 -0
  52. package/dist/components/CodeBlock/CodeBlock.svelte +356 -0
  53. package/dist/components/CodeBlock/CodeBlock.svelte.d.ts +44 -0
  54. package/dist/components/CodeBlock/index.d.ts +1 -0
  55. package/dist/components/CodeBlock/index.js +1 -0
  56. package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte +145 -0
  57. package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte.d.ts +44 -0
  58. package/dist/components/CodeEditor/CodeEditor.svelte +229 -0
  59. package/dist/components/CodeEditor/CodeEditor.svelte.d.ts +23 -0
  60. package/dist/components/Combobox/Combobox.svelte +279 -0
  61. package/dist/components/Combobox/Combobox.svelte.d.ts +34 -0
  62. package/dist/components/Container/Container.svelte +45 -0
  63. package/dist/components/Container/Container.svelte.d.ts +36 -0
  64. package/dist/components/DataTable/DataTable.svelte +879 -0
  65. package/dist/components/DataTable/DataTable.svelte.d.ts +102 -0
  66. package/dist/components/Form/AutoComplete.svelte +357 -0
  67. package/dist/components/Form/AutoComplete.svelte.d.ts +73 -0
  68. package/dist/components/Form/Calendar.svelte +429 -0
  69. package/dist/components/Form/Calendar.svelte.d.ts +53 -0
  70. package/dist/components/Form/Checkbox.svelte +196 -0
  71. package/dist/components/Form/Checkbox.svelte.d.ts +50 -0
  72. package/dist/components/Form/ColorPicker.svelte +396 -0
  73. package/dist/components/Form/ColorPicker.svelte.d.ts +43 -0
  74. package/dist/components/Form/Combobox.svelte +645 -0
  75. package/dist/components/Form/Combobox.svelte.d.ts +93 -0
  76. package/dist/components/Form/Dropdown.svelte +773 -0
  77. package/dist/components/Form/Dropdown.svelte.d.ts +81 -0
  78. package/dist/components/Form/FileUpload.svelte +796 -0
  79. package/dist/components/Form/FileUpload.svelte.d.ts +78 -0
  80. package/dist/components/Form/FloatLabel.svelte +245 -0
  81. package/dist/components/Form/FloatLabel.svelte.d.ts +44 -0
  82. package/dist/components/Form/Form.svelte +281 -0
  83. package/dist/components/Form/Form.svelte.d.ts +54 -0
  84. package/dist/components/Form/FormField.svelte +218 -0
  85. package/dist/components/Form/FormField.svelte.d.ts +47 -0
  86. package/dist/components/Form/Input.svelte +340 -0
  87. package/dist/components/Form/Input.svelte.d.ts +79 -0
  88. package/dist/components/Form/InputSwitch.svelte +189 -0
  89. package/dist/components/Form/InputSwitch.svelte.d.ts +46 -0
  90. package/dist/components/Form/InvalidState.svelte +97 -0
  91. package/dist/components/Form/InvalidState.svelte.d.ts +37 -0
  92. package/dist/components/Form/Knob.svelte +537 -0
  93. package/dist/components/Form/Knob.svelte.d.ts +78 -0
  94. package/dist/components/Form/ListInput.svelte +469 -0
  95. package/dist/components/Form/ListInput.svelte.d.ts +70 -0
  96. package/dist/components/Form/Listbox.svelte +513 -0
  97. package/dist/components/Form/Listbox.svelte.d.ts +74 -0
  98. package/dist/components/Form/NumberInput.svelte +452 -0
  99. package/dist/components/Form/NumberInput.svelte.d.ts +82 -0
  100. package/dist/components/Form/Radio.svelte +192 -0
  101. package/dist/components/Form/Radio.svelte.d.ts +53 -0
  102. package/dist/components/Form/RadioGroup.svelte +155 -0
  103. package/dist/components/Form/RadioGroup.svelte.d.ts +48 -0
  104. package/dist/components/Form/Rating.svelte +380 -0
  105. package/dist/components/Form/Rating.svelte.d.ts +64 -0
  106. package/dist/components/Form/Select.svelte +436 -0
  107. package/dist/components/Form/Select.svelte.d.ts +49 -0
  108. package/dist/components/Form/SelectGroup.svelte +34 -0
  109. package/dist/components/Form/SelectGroup.svelte.d.ts +33 -0
  110. package/dist/components/Form/Slider.svelte +622 -0
  111. package/dist/components/Form/Slider.svelte.d.ts +73 -0
  112. package/dist/components/Form/Switch.svelte +192 -0
  113. package/dist/components/Form/Switch.svelte.d.ts +46 -0
  114. package/dist/components/Form/TextInput.svelte +274 -0
  115. package/dist/components/Form/TextInput.svelte.d.ts +74 -0
  116. package/dist/components/Form/Textarea.svelte +207 -0
  117. package/dist/components/Form/Textarea.svelte.d.ts +62 -0
  118. package/dist/components/Icon/Icon.svelte +140 -0
  119. package/dist/components/Icon/Icon.svelte.d.ts +25 -0
  120. package/dist/components/Icon/index.d.ts +1 -0
  121. package/dist/components/Icon/index.js +1 -0
  122. package/dist/components/Lazy/Lazy.svelte +158 -0
  123. package/dist/components/Lazy/Lazy.svelte.d.ts +42 -0
  124. package/dist/components/Masonry/Masonry.svelte +299 -0
  125. package/dist/components/Masonry/Masonry.svelte.d.ts +55 -0
  126. package/dist/components/Menu/Menu/Menu.svelte +65 -0
  127. package/dist/components/Menu/Menu/Menu.svelte.d.ts +17 -0
  128. package/dist/components/Menu/Menu/MenuItem.svelte +90 -0
  129. package/dist/components/Menu/Menu/MenuItem.svelte.d.ts +27 -0
  130. package/dist/components/Modal/Modal.svelte +334 -0
  131. package/dist/components/Modal/Modal.svelte.d.ts +55 -0
  132. package/dist/components/Panel/Card.svelte +141 -0
  133. package/dist/components/Panel/Card.svelte.d.ts +52 -0
  134. package/dist/components/Panel/Hero/Hero.story.md +9 -0
  135. package/dist/components/Panel/Hero/Hero.story.svelte +49 -0
  136. package/dist/components/Panel/Hero/Hero.story.svelte.d.ts +21 -0
  137. package/dist/components/Panel/Hero/Hero.svelte +24 -0
  138. package/dist/components/Panel/Hero/Hero.svelte.d.ts +32 -0
  139. package/dist/components/Panel/LazyPanel.svelte +110 -0
  140. package/dist/components/Panel/LazyPanel.svelte.d.ts +46 -0
  141. package/dist/components/Panel/Panel.svelte +205 -0
  142. package/dist/components/Panel/Panel.svelte.d.ts +23 -0
  143. package/dist/components/Progress/Progress.svelte +220 -0
  144. package/dist/components/Progress/Progress.svelte.d.ts +61 -0
  145. package/dist/components/Separator/Separator.svelte +109 -0
  146. package/dist/components/Separator/Separator.svelte.d.ts +35 -0
  147. package/dist/components/Sidebar/Sidebar.svelte +213 -0
  148. package/dist/components/Sidebar/Sidebar.svelte.d.ts +60 -0
  149. package/dist/components/Skeleton/Skeleton.svelte +170 -0
  150. package/dist/components/Skeleton/Skeleton.svelte.d.ts +48 -0
  151. package/dist/components/Stepper/Stepper.svelte +111 -0
  152. package/dist/components/Stepper/Stepper.svelte.d.ts +54 -0
  153. package/dist/components/Stepper/StepperStep.svelte +369 -0
  154. package/dist/components/Stepper/StepperStep.svelte.d.ts +63 -0
  155. package/dist/components/Table/Table.svelte +167 -0
  156. package/dist/components/Table/Table.svelte.d.ts +56 -0
  157. package/dist/components/Table/TableBody.svelte +41 -0
  158. package/dist/components/Table/TableBody.svelte.d.ts +33 -0
  159. package/dist/components/Table/TableCell.svelte +76 -0
  160. package/dist/components/Table/TableCell.svelte.d.ts +36 -0
  161. package/dist/components/Table/TableHead.svelte +41 -0
  162. package/dist/components/Table/TableHead.svelte.d.ts +32 -0
  163. package/dist/components/Table/TableHeader.svelte +148 -0
  164. package/dist/components/Table/TableHeader.svelte.d.ts +42 -0
  165. package/dist/components/Table/TableRow.svelte +99 -0
  166. package/dist/components/Table/TableRow.svelte.d.ts +40 -0
  167. package/dist/components/Tabs/Tab.svelte +145 -0
  168. package/dist/components/Tabs/Tab.svelte.d.ts +36 -0
  169. package/dist/components/Tabs/TabList.svelte +60 -0
  170. package/dist/components/Tabs/TabList.svelte.d.ts +32 -0
  171. package/dist/components/Tabs/TabPanel.svelte +118 -0
  172. package/dist/components/Tabs/TabPanel.svelte.d.ts +38 -0
  173. package/dist/components/Tabs/Tabs.svelte +287 -0
  174. package/dist/components/Tabs/Tabs.svelte.d.ts +50 -0
  175. package/dist/components/Tag/Tag.svelte +260 -0
  176. package/dist/components/Tag/Tag.svelte.d.ts +54 -0
  177. package/dist/components/Tag/TagGroup.svelte +147 -0
  178. package/dist/components/Tag/TagGroup.svelte.d.ts +62 -0
  179. package/dist/components/ThemeToggle/ThemeToggle.svelte +93 -0
  180. package/dist/components/ThemeToggle/ThemeToggle.svelte.d.ts +12 -0
  181. package/dist/components/Timeline/Timeline.svelte +144 -0
  182. package/dist/components/Timeline/Timeline.svelte.d.ts +48 -0
  183. package/dist/components/Timeline/TimelineItem.svelte +391 -0
  184. package/dist/components/Timeline/TimelineItem.svelte.d.ts +63 -0
  185. package/dist/components/Toast/Toast.svelte +313 -0
  186. package/dist/components/Toast/Toast.svelte.d.ts +44 -0
  187. package/dist/components/Toast/toastStore.d.ts +40 -0
  188. package/dist/components/Toast/toastStore.js +293 -0
  189. package/dist/components/Tooltip/Tooltip.svelte +282 -0
  190. package/dist/components/Tooltip/Tooltip.svelte.d.ts +55 -0
  191. package/dist/components/Tree/Tree.svelte +129 -0
  192. package/dist/components/Tree/Tree.svelte.d.ts +61 -0
  193. package/dist/components/Tree/TreeNode.svelte +332 -0
  194. package/dist/components/Tree/TreeNode.svelte.d.ts +55 -0
  195. package/dist/components/icons/TwintrinsicLogo.svelte +73 -0
  196. package/dist/components/icons/TwintrinsicLogo.svelte.d.ts +17 -0
  197. package/dist/components/icons/twintrinsic-source.svg +73 -0
  198. package/dist/components/icons/twintrinsic.svg +38 -0
  199. package/dist/docs/EventsTable.svelte +86 -0
  200. package/dist/docs/EventsTable.svelte.d.ts +27 -0
  201. package/dist/docs/PropsTable.svelte +103 -0
  202. package/dist/docs/PropsTable.svelte.d.ts +28 -0
  203. package/dist/docs/index.d.ts +2 -0
  204. package/dist/docs/index.js +2 -0
  205. package/dist/helpers/detectLanguage.d.ts +6 -0
  206. package/dist/helpers/detectLanguage.js +60 -0
  207. package/dist/helpers/index.d.ts +1 -0
  208. package/dist/helpers/index.js +1 -0
  209. package/dist/index.d.ts +86 -0
  210. package/dist/index.js +94 -0
  211. package/dist/twintrinsic.css +347 -0
  212. 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>