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,773 @@
1
+ <!--
2
+ @component
3
+ Dropdown - A component for selecting one or multiple options from a dropdown menu.
4
+ Supports icons, groups, cascading menus, and keyboard navigation.
5
+
6
+ Usage:
7
+ ```svelte
8
+ <Dropdown
9
+ name="country"
10
+ options={countries}
11
+ placeholder="Select a country"
12
+ />
13
+
14
+ <Dropdown
15
+ name="skills"
16
+ options={skills}
17
+ multiple
18
+ placeholder="Select skills"
19
+ />
20
+
21
+ <Dropdown
22
+ name="category"
23
+ options={categories}
24
+ optionLabel="name"
25
+ optionValue="id"
26
+ optionIcon="icon"
27
+ />
28
+ ```
29
+ -->
30
+ <script>
31
+ import { getContext } from "svelte"
32
+ import { slide } from "svelte/transition"
33
+ import { clickOutside } from "../../actions/clickOutside.js"
34
+
35
+ const {
36
+ /** @type {string} - Additional CSS classes */
37
+ class: className = "",
38
+
39
+ /** @type {string} - HTML id for accessibility */
40
+ id = crypto.randomUUID(),
41
+
42
+ /** @type {string} - Input name */
43
+ name,
44
+
45
+ /** @type {Array} - Options to display */
46
+ options = [],
47
+
48
+ /** @type {any} - Selected value(s) */
49
+ value,
50
+
51
+ /** @type {string} - Placeholder text */
52
+ placeholder = "Select an option",
53
+
54
+ /** @type {boolean} - Whether multiple selection is allowed */
55
+ multiple = false,
56
+
57
+ /** @type {string} - Property name for option label */
58
+ optionLabel = "label",
59
+
60
+ /** @type {string} - Property name for option value */
61
+ optionValue = "value",
62
+
63
+ /** @type {string} - Property name for option icon */
64
+ optionIcon,
65
+
66
+ /** @type {string} - Property name for option children (for cascading) */
67
+ optionChildren = "items",
68
+
69
+ /** @type {boolean} - Whether the dropdown is disabled */
70
+ disabled = false,
71
+
72
+ /** @type {boolean} - Whether the dropdown is required */
73
+ required = false,
74
+
75
+ /** @type {boolean} - Whether to filter options by typing */
76
+ filter = false,
77
+
78
+ /** @type {string} - Size of the dropdown (sm, md, lg) */
79
+ size = "md",
80
+
81
+ /** @type {boolean} - Whether to show a clear button */
82
+ clearable = false,
83
+
84
+ /** @type {string} - ARIA label for accessibility */
85
+ ariaLabel,
86
+
87
+ /** @type {(event: CustomEvent) => void} - Change event handler */
88
+ onchange,
89
+ /** @type {(event: CustomEvent) => void} - Clear event handler */
90
+ onclear,
91
+ /** @type {(event: CustomEvent) => void} - Open event handler */
92
+ onopen,
93
+ /** @type {(event: CustomEvent) => void} - Close event handler */
94
+ onclose,
95
+ /** @type {(event: CustomEvent) => void} - Filter event handler */
96
+ onfilter,
97
+ } = $props()
98
+
99
+ // Get form context if available
100
+ const formContext = getContext("form")
101
+
102
+ // Component state
103
+ let isOpen = $state(false)
104
+ let selectedValues = $state(multiple ? [] : null)
105
+ let filterValue = $state("")
106
+ let highlightedIndex = $state(0)
107
+ let dropdownElement = $state()
108
+ let inputElement = $state()
109
+ let menuElement = $state()
110
+ let activeSubmenu = $state(null)
111
+
112
+ // Register with form if available
113
+ let fieldApi = $state()
114
+
115
+ $effect(() => {
116
+ if (formContext && name) {
117
+ fieldApi = formContext.registerField(name, value)
118
+ }
119
+ })
120
+
121
+ // Update value when form field changes
122
+ $effect(() => {
123
+ if (fieldApi) {
124
+ const formValue = fieldApi.getValue()
125
+ if (formValue !== undefined && JSON.stringify(formValue) !== JSON.stringify(selectedValues)) {
126
+ selectedValues = formValue
127
+ }
128
+ }
129
+ })
130
+
131
+ // Initialize selected values from prop
132
+ $effect(() => {
133
+ if (value !== undefined) {
134
+ selectedValues = value
135
+ }
136
+ })
137
+
138
+ /**
139
+ * Gets the display label for an option
140
+ * @param {Object|string} option - Option to get label for
141
+ * @returns {string} - Display label
142
+ */
143
+ function getOptionLabel(option) {
144
+ if (!option) return ""
145
+
146
+ if (typeof option === "object") {
147
+ return option[optionLabel] || ""
148
+ }
149
+
150
+ return option.toString()
151
+ }
152
+
153
+ /**
154
+ * Gets the value for an option
155
+ * @param {Object|string} option - Option to get value for
156
+ * @returns {any} - Option value
157
+ */
158
+ function getOptionValue(option) {
159
+ if (!option) return null
160
+
161
+ if (typeof option === "object") {
162
+ return option[optionValue]
163
+ }
164
+
165
+ return option
166
+ }
167
+
168
+ /**
169
+ * Gets the icon for an option
170
+ * @param {Object} option - Option to get icon for
171
+ * @returns {string} - Icon HTML
172
+ */
173
+ function getOptionIcon(option) {
174
+ if (!option || !optionIcon || typeof option !== "object") return null
175
+
176
+ return option[optionIcon]
177
+ }
178
+
179
+ /**
180
+ * Gets the children for an option (for cascading)
181
+ * @param {Object} option - Option to get children for
182
+ * @returns {Array} - Child options
183
+ */
184
+ function getOptionChildren(option) {
185
+ if (!option || typeof option !== "object") return null
186
+
187
+ return option[optionChildren] || null
188
+ }
189
+
190
+ /**
191
+ * Checks if an option is selected
192
+ * @param {Object|string} option - Option to check
193
+ * @returns {boolean} - Whether the option is selected
194
+ */
195
+ function isOptionSelected(option) {
196
+ const value = getOptionValue(option)
197
+
198
+ if (multiple) {
199
+ return (
200
+ Array.isArray(selectedValues) &&
201
+ selectedValues.some((v) => (typeof v === "object" ? v[optionValue] === value : v === value))
202
+ )
203
+ }
204
+
205
+ return (
206
+ selectedValues === value ||
207
+ (typeof selectedValues === "object" && selectedValues && selectedValues[optionValue] === value)
208
+ )
209
+ }
210
+
211
+ /**
212
+ * Filters options based on input value
213
+ * @param {Array} opts - Options to filter
214
+ * @returns {Array} - Filtered options
215
+ */
216
+ function filterOptions(opts) {
217
+ if (!filter || !filterValue) return opts
218
+
219
+ return opts.filter((option) => {
220
+ const label = getOptionLabel(option).toLowerCase()
221
+ return label.includes(filterValue.toLowerCase())
222
+ })
223
+ }
224
+
225
+ /**
226
+ * Gets the display value for the input
227
+ * @returns {string} - Display value
228
+ */
229
+ function getDisplayValue() {
230
+ if (!selectedValues || (Array.isArray(selectedValues) && selectedValues.length === 0)) {
231
+ return ""
232
+ }
233
+
234
+ if (multiple) {
235
+ if (Array.isArray(selectedValues)) {
236
+ return selectedValues
237
+ .map((v) =>
238
+ typeof v === "object"
239
+ ? getOptionLabel(v)
240
+ : getOptionLabel(options.find((o) => getOptionValue(o) === v))
241
+ )
242
+ .join(", ")
243
+ }
244
+ return ""
245
+ }
246
+
247
+ if (typeof selectedValues === "object") {
248
+ return getOptionLabel(selectedValues)
249
+ }
250
+
251
+ const selectedOption = options.find((o) => getOptionValue(o) === selectedValues)
252
+ return selectedOption ? getOptionLabel(selectedOption) : ""
253
+ }
254
+
255
+ /**
256
+ * Selects an option
257
+ * @param {Object|string} option - Option to select
258
+ */
259
+ function selectOption(option, evt) {
260
+ evt.stopPropagation()
261
+ const value = getOptionValue(option)
262
+
263
+ if (multiple) {
264
+ if (isOptionSelected(option)) {
265
+ // Remove from selection
266
+ selectedValues = Array.isArray(selectedValues)
267
+ ? selectedValues.filter((v) =>
268
+ typeof v === "object" ? v[optionValue] !== value : v !== value
269
+ )
270
+ : []
271
+ } else {
272
+ // Add to selection
273
+ selectedValues = Array.isArray(selectedValues) ? [...selectedValues, value] : [value]
274
+ }
275
+ } else {
276
+ // Single selection
277
+ selectedValues = value
278
+ closeDropdown()
279
+ }
280
+
281
+ // Update form field if available
282
+ if (fieldApi) {
283
+ fieldApi.setValue(selectedValues)
284
+ }
285
+
286
+ onchange?.(new CustomEvent("change", { detail: { value: selectedValues } }))
287
+ }
288
+
289
+ /**
290
+ * Clears the selection
291
+ */
292
+ function clearSelection(evt) {
293
+ evt.stopPropagation()
294
+ selectedValues = multiple ? [] : null
295
+
296
+ // Update form field if available
297
+ if (fieldApi) {
298
+ fieldApi.setValue(selectedValues)
299
+ }
300
+
301
+ onchange?.(new CustomEvent("change", { detail: { value: selectedValues } }))
302
+ onclear?.(new CustomEvent("clear"))
303
+ }
304
+
305
+ /**
306
+ * Opens the dropdown
307
+ */
308
+ function openDropdown() {
309
+ if (disabled) return
310
+
311
+ isOpen = true
312
+ highlightedIndex = 0
313
+ activeSubmenu = null
314
+
315
+ // Focus the filter input if filtering is enabled
316
+ if (filter) {
317
+ setTimeout(() => {
318
+ inputElement?.focus()
319
+ }, 0)
320
+ }
321
+
322
+ onopen?.(new CustomEvent("open"))
323
+ }
324
+
325
+ /**
326
+ * Closes the dropdown
327
+ */
328
+ function closeDropdown() {
329
+ isOpen = false
330
+ filterValue = ""
331
+ activeSubmenu = null
332
+
333
+ onclose?.(new CustomEvent("close"))
334
+ }
335
+
336
+ /**
337
+ * Toggles the dropdown
338
+ */
339
+ function toggleDropdown() {
340
+ if (isOpen) {
341
+ closeDropdown()
342
+ } else {
343
+ openDropdown()
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Opens a submenu
349
+ * @param {Object} option - Parent option
350
+ * @param {Event} event - Mouse event
351
+ */
352
+ function openSubmenu(option, event) {
353
+ if (event) {
354
+ event.stopPropagation()
355
+ }
356
+
357
+ activeSubmenu = option
358
+ }
359
+
360
+ /**
361
+ * Handles keydown events
362
+ * @param {KeyboardEvent} event - Keydown event
363
+ */
364
+ function handleKeydown(event) {
365
+ if (disabled) return
366
+
367
+ const filteredOptions = filterOptions(options)
368
+
369
+ switch (event.key) {
370
+ case "ArrowDown":
371
+ event.preventDefault()
372
+ if (!isOpen) {
373
+ openDropdown()
374
+ } else {
375
+ highlightedIndex = (highlightedIndex + 1) % filteredOptions.length
376
+ scrollOptionIntoView()
377
+ }
378
+ break
379
+
380
+ case "ArrowUp":
381
+ event.preventDefault()
382
+ if (!isOpen) {
383
+ openDropdown()
384
+ } else {
385
+ highlightedIndex = (highlightedIndex - 1 + filteredOptions.length) % filteredOptions.length
386
+ scrollOptionIntoView()
387
+ }
388
+ break
389
+
390
+ case "Enter":
391
+ event.preventDefault()
392
+ if (isOpen) {
393
+ if (filteredOptions[highlightedIndex]) {
394
+ selectOption(filteredOptions[highlightedIndex])
395
+ }
396
+ } else {
397
+ openDropdown()
398
+ }
399
+ break
400
+
401
+ case "Escape":
402
+ event.preventDefault()
403
+ closeDropdown()
404
+ break
405
+
406
+ case "Tab":
407
+ if (isOpen) {
408
+ closeDropdown()
409
+ }
410
+ break
411
+
412
+ case " ":
413
+ if (!filter) {
414
+ event.preventDefault()
415
+ if (!isOpen) {
416
+ openDropdown()
417
+ }
418
+ }
419
+ break
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Scrolls the highlighted option into view
425
+ */
426
+ function scrollOptionIntoView() {
427
+ if (!menuElement) return
428
+
429
+ const highlightedOption = menuElement.querySelector(`[data-index="${highlightedIndex}"]`)
430
+ if (highlightedOption) {
431
+ highlightedOption.scrollIntoView({ block: "nearest" })
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Handles filter input
437
+ * @param {Event} event - Input event
438
+ */
439
+ function handleFilterInput(event) {
440
+ filterValue = event.target.value
441
+ highlightedIndex = 0
442
+
443
+ onfilter?.(new CustomEvent("filter", { detail: { filter: filterValue } }))
444
+ }
445
+
446
+ // Determine size classes
447
+ const sizeClasses = $derived(
448
+ {
449
+ sm: "h-8 text-sm",
450
+ md: "h-10 text-base",
451
+ lg: "h-12 text-lg",
452
+ }[size] || "h-10 text-base"
453
+ )
454
+
455
+ // Computed display value
456
+ const displayValue = $derived(getDisplayValue())
457
+ </script>
458
+
459
+ <div
460
+ class="
461
+ dropdown
462
+ {isOpen ? 'dropdown-open' : ''}
463
+ {disabled ? 'dropdown-disabled' : ''}
464
+ {className}
465
+ "
466
+ bind:this={dropdownElement}
467
+ use:clickOutside={() => isOpen && closeDropdown()}
468
+ >
469
+ <div
470
+ class="dropdown-control {sizeClasses}"
471
+ onclick={toggleDropdown}
472
+ onkeydown={handleKeydown}
473
+ tabindex={disabled ? undefined : 0}
474
+ role="combobox"
475
+ aria-expanded={isOpen}
476
+ aria-haspopup="listbox"
477
+ aria-controls={`${id}-menu`}
478
+ aria-label={ariaLabel || name}
479
+ aria-disabled={disabled ? 'true' : undefined}
480
+ >
481
+ {#if filter && isOpen}
482
+ <input
483
+ type="text"
484
+ class="dropdown-filter"
485
+ placeholder={placeholder}
486
+ value={filterValue}
487
+ oninput={handleFilterInput}
488
+ onkeydown={handleKeydown}
489
+ bind:this={inputElement}
490
+ {disabled}
491
+ />
492
+ {:else}
493
+ <div class="dropdown-value">
494
+ {#if displayValue}
495
+ {displayValue}
496
+ {:else}
497
+ <span class="dropdown-placeholder">{placeholder}</span>
498
+ {/if}
499
+ </div>
500
+ {/if}
501
+
502
+ <div class="dropdown-indicators">
503
+ {#if clearable && (selectedValues !== null && (!Array.isArray(selectedValues) || selectedValues.length > 0))}
504
+ <button
505
+ type="button"
506
+ class="dropdown-clear-button"
507
+ aria-label="Clear selection"
508
+ onclick={clearSelection}
509
+ tabindex="-1"
510
+ >
511
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
512
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
513
+ </svg>
514
+ </button>
515
+ {/if}
516
+
517
+ <div class="dropdown-arrow">
518
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
519
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
520
+ </svg>
521
+ </div>
522
+ </div>
523
+ </div>
524
+
525
+ {#if isOpen}
526
+ <div
527
+ id="{id}-menu"
528
+ class="dropdown-menu"
529
+ role="listbox"
530
+ aria-multiselectable={multiple ? 'true' : undefined}
531
+ bind:this={menuElement}
532
+ transition:slide={{ duration: 200 }}
533
+ >
534
+ <ul class="dropdown-options">
535
+ {#each filterOptions(options) as option, index}
536
+ {@const hasChildren = getOptionChildren(option) && getOptionChildren(option).length > 0}
537
+ {@const isHighlighted = index === highlightedIndex}
538
+ {@const isSelected = isOptionSelected(option)}
539
+
540
+ <li
541
+ class="
542
+ dropdown-option
543
+ {isHighlighted ? 'dropdown-option-highlighted' : ''}
544
+ {isSelected ? 'dropdown-option-selected' : ''}
545
+ {hasChildren ? 'dropdown-option-parent' : ''}
546
+ "
547
+ role="option"
548
+ aria-selected={isSelected ? 'true' : 'false'}
549
+ data-index={index}
550
+ onclick={() => selectOption(option)}
551
+ onmouseenter={() => {
552
+ highlightedIndex = index;
553
+ if (hasChildren) {
554
+ openSubmenu(option);
555
+ } else {
556
+ activeSubmenu = null;
557
+ }
558
+ }}
559
+ >
560
+ <div class="dropdown-option-content">
561
+ {#if multiple}
562
+ <div class="dropdown-checkbox">
563
+ {#if isSelected}
564
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
565
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
566
+ </svg>
567
+ {/if}
568
+ </div>
569
+ {/if}
570
+
571
+ {#if optionIcon && getOptionIcon(option)}
572
+ <div class="dropdown-option-icon">
573
+ {@html getOptionIcon(option)}
574
+ </div>
575
+ {/if}
576
+
577
+ <div class="dropdown-option-label">
578
+ {getOptionLabel(option)}
579
+ </div>
580
+
581
+ {#if hasChildren}
582
+ <div class="dropdown-submenu-arrow">
583
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
584
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
585
+ </svg>
586
+ </div>
587
+ {/if}
588
+ </div>
589
+
590
+ {#if hasChildren && activeSubmenu === option}
591
+ <div class="dropdown-submenu">
592
+ <ul class="dropdown-options">
593
+ {#each getOptionChildren(option) as childOption, childIndex}
594
+ {@const isChildSelected = isOptionSelected(childOption)}
595
+
596
+ <li
597
+ class="
598
+ dropdown-option
599
+ {isChildSelected ? 'dropdown-option-selected' : ''}
600
+ "
601
+ role="option"
602
+ aria-selected={isChildSelected ? 'true' : 'false'}
603
+ onclick={() => selectOption(childOption)}
604
+ >
605
+ <div class="dropdown-option-content">
606
+ {#if multiple}
607
+ <div class="dropdown-checkbox">
608
+ {#if isChildSelected}
609
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
610
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
611
+ </svg>
612
+ {/if}
613
+ </div>
614
+ {/if}
615
+
616
+ {#if optionIcon && getOptionIcon(childOption)}
617
+ <div class="dropdown-option-icon">
618
+ {@html getOptionIcon(childOption)}
619
+ </div>
620
+ {/if}
621
+
622
+ <div class="dropdown-option-label">
623
+ {getOptionLabel(childOption)}
624
+ </div>
625
+ </div>
626
+ </li>
627
+ {/each}
628
+ </ul>
629
+ </div>
630
+ {/if}
631
+ </li>
632
+ {:else}
633
+ <li class="dropdown-empty">No options available</li>
634
+ {/each}
635
+ </ul>
636
+ </div>
637
+ {/if}
638
+
639
+ <!-- Hidden input for form submission -->
640
+ <input
641
+ type="hidden"
642
+ {id}
643
+ {name}
644
+ value={JSON.stringify(selectedValues)}
645
+ {required}
646
+ {disabled}
647
+ />
648
+ </div>
649
+
650
+ <style>
651
+ @reference "../../twintrinsic.css";
652
+
653
+ .dropdown {
654
+ @apply relative w-full;
655
+ }
656
+
657
+ .dropdown-control {
658
+ @apply flex items-center justify-between;
659
+ @apply bg-background dark:bg-background;
660
+ @apply border border-border dark:border-border rounded-md;
661
+ @apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 focus:border-primary-500 dark:focus:border-primary-400;
662
+ @apply transition-colors duration-200;
663
+ @apply px-3 cursor-pointer;
664
+ }
665
+
666
+ .dropdown-disabled .dropdown-control {
667
+ @apply opacity-50 cursor-not-allowed;
668
+ }
669
+
670
+ .dropdown-value {
671
+ @apply flex-grow truncate text-text dark:text-text;
672
+ }
673
+
674
+ .dropdown-placeholder {
675
+ @apply text-muted dark:text-muted;
676
+ }
677
+
678
+ .dropdown-indicators {
679
+ @apply flex items-center ml-2;
680
+ }
681
+
682
+ .dropdown-arrow {
683
+ @apply flex-shrink-0 text-muted dark:text-muted transition-transform duration-200;
684
+ }
685
+
686
+ .dropdown-open .dropdown-arrow {
687
+ @apply rotate-180;
688
+ }
689
+
690
+ .dropdown-clear-button {
691
+ @apply p-1 mr-1 rounded-full text-muted dark:text-muted;
692
+ @apply hover:bg-hover dark:hover:bg-hover hover:text-text dark:hover:text-text;
693
+ @apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400;
694
+ }
695
+
696
+ .dropdown-filter {
697
+ @apply w-full bg-transparent border-none outline-none;
698
+ @apply text-text dark:text-text placeholder:text-muted dark:placeholder:text-muted;
699
+ }
700
+
701
+ .dropdown-menu {
702
+ @apply absolute z-50 w-full mt-1;
703
+ @apply bg-background dark:bg-background;
704
+ @apply border border-border dark:border-border rounded-md;
705
+ @apply shadow-lg;
706
+ @apply max-h-60 overflow-y-auto;
707
+ }
708
+
709
+ .dropdown-options {
710
+ @apply py-1;
711
+ }
712
+
713
+ .dropdown-option {
714
+ @apply relative px-3 py-2 cursor-pointer;
715
+ @apply text-text dark:text-text;
716
+ @apply hover:bg-hover dark:hover:bg-hover;
717
+ @apply transition-colors duration-150;
718
+ }
719
+
720
+ .dropdown-option-content {
721
+ @apply flex items-center gap-2;
722
+ }
723
+
724
+ .dropdown-option-highlighted {
725
+ @apply bg-hover dark:bg-hover;
726
+ }
727
+
728
+ .dropdown-option-selected {
729
+ @apply bg-primary-50 dark:bg-primary-900/20;
730
+ @apply text-primary-700 dark:text-primary-300;
731
+ }
732
+
733
+ .dropdown-option-parent {
734
+ @apply pr-8;
735
+ }
736
+
737
+ .dropdown-checkbox {
738
+ @apply w-4 h-4 flex-shrink-0;
739
+ @apply border border-border dark:border-border rounded;
740
+ @apply flex items-center justify-center;
741
+ @apply bg-background dark:bg-background;
742
+ }
743
+
744
+ .dropdown-option-selected .dropdown-checkbox {
745
+ @apply bg-primary-500 dark:bg-primary-400 border-primary-500 dark:border-primary-400;
746
+ @apply text-white dark:text-white;
747
+ }
748
+
749
+ .dropdown-option-icon {
750
+ @apply flex-shrink-0 w-5 h-5;
751
+ }
752
+
753
+ .dropdown-option-label {
754
+ @apply flex-grow truncate;
755
+ }
756
+
757
+ .dropdown-submenu-arrow {
758
+ @apply absolute right-3 top-1/2 -translate-y-1/2;
759
+ @apply text-muted dark:text-muted;
760
+ }
761
+
762
+ .dropdown-submenu {
763
+ @apply absolute left-full top-0 ml-1;
764
+ @apply bg-background dark:bg-background;
765
+ @apply border border-border dark:border-border rounded-md;
766
+ @apply shadow-lg;
767
+ @apply min-w-[200px] max-h-60 overflow-y-auto;
768
+ }
769
+
770
+ .dropdown-empty {
771
+ @apply px-3 py-2 text-muted dark:text-muted text-center;
772
+ }
773
+ </style>