@sabrenski/spire-ui 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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/index.d.ts +4981 -0
  4. package/dist/spire-ui.css +1 -0
  5. package/dist/spire-ui.es.js +18403 -0
  6. package/dist/spire-ui.umd.js +45 -0
  7. package/package.json +83 -0
  8. package/src/components/Accordion/Accordion.test.ts +218 -0
  9. package/src/components/Accordion/AccordionContent.vue +112 -0
  10. package/src/components/Accordion/AccordionItem.vue +87 -0
  11. package/src/components/Accordion/AccordionRoot.vue +111 -0
  12. package/src/components/Accordion/AccordionTrigger.vue +125 -0
  13. package/src/components/Accordion/index.ts +11 -0
  14. package/src/components/Accordion/keys.ts +23 -0
  15. package/src/components/Avatar/Avatar.test.ts +181 -0
  16. package/src/components/Avatar/Avatar.vue +150 -0
  17. package/src/components/Avatar/index.ts +2 -0
  18. package/src/components/Badge/Badge.test.ts +141 -0
  19. package/src/components/Badge/Badge.vue +133 -0
  20. package/src/components/Badge/index.ts +2 -0
  21. package/src/components/BadgeContainer/BadgeContainer.test.ts +150 -0
  22. package/src/components/BadgeContainer/BadgeContainer.vue +90 -0
  23. package/src/components/BadgeContainer/index.ts +2 -0
  24. package/src/components/Breadcrumb/Breadcrumb.test.ts +342 -0
  25. package/src/components/Breadcrumb/BreadcrumbEllipsis.vue +96 -0
  26. package/src/components/Breadcrumb/BreadcrumbItem.vue +16 -0
  27. package/src/components/Breadcrumb/BreadcrumbLink.vue +67 -0
  28. package/src/components/Breadcrumb/BreadcrumbList.vue +20 -0
  29. package/src/components/Breadcrumb/BreadcrumbPage.vue +25 -0
  30. package/src/components/Breadcrumb/BreadcrumbRoot.vue +41 -0
  31. package/src/components/Breadcrumb/BreadcrumbSeparator.vue +63 -0
  32. package/src/components/Breadcrumb/index.ts +13 -0
  33. package/src/components/Breadcrumb/keys.ts +7 -0
  34. package/src/components/Button/Button.test.ts +231 -0
  35. package/src/components/Button/Button.vue +349 -0
  36. package/src/components/Button/index.ts +2 -0
  37. package/src/components/Callout/Callout.test.ts +260 -0
  38. package/src/components/Callout/Callout.vue +341 -0
  39. package/src/components/Callout/index.ts +2 -0
  40. package/src/components/Card/Card.test.ts +565 -0
  41. package/src/components/Card/Card.vue +209 -0
  42. package/src/components/Card/CardContent.vue +57 -0
  43. package/src/components/Card/CardFooter.vue +72 -0
  44. package/src/components/Card/CardHeader.vue +111 -0
  45. package/src/components/Card/CardImage.vue +124 -0
  46. package/src/components/Card/index.ts +14 -0
  47. package/src/components/Chart/BarChart.vue +208 -0
  48. package/src/components/Chart/BaseChart.vue +444 -0
  49. package/src/components/Chart/Chart.test.ts +359 -0
  50. package/src/components/Chart/DonutChart.vue +283 -0
  51. package/src/components/Chart/LineChart.vue +211 -0
  52. package/src/components/Chart/index.ts +20 -0
  53. package/src/components/Chart/useChartTheme.ts +192 -0
  54. package/src/components/Checkbox/Checkbox.test.ts +209 -0
  55. package/src/components/Checkbox/Checkbox.vue +285 -0
  56. package/src/components/Checkbox/index.ts +2 -0
  57. package/src/components/ChoiceChip/ChoiceChip.test.ts +142 -0
  58. package/src/components/ChoiceChip/ChoiceChip.vue +218 -0
  59. package/src/components/ChoiceChip/index.ts +2 -0
  60. package/src/components/ChoiceChipGroup/ChoiceChipGroup.test.ts +151 -0
  61. package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +70 -0
  62. package/src/components/ChoiceChipGroup/index.ts +2 -0
  63. package/src/components/ColorPicker/ColorArea.vue +159 -0
  64. package/src/components/ColorPicker/ColorPicker.test.ts +250 -0
  65. package/src/components/ColorPicker/ColorPicker.vue +339 -0
  66. package/src/components/ColorPicker/ColorSlider.vue +191 -0
  67. package/src/components/ColorPicker/index.ts +7 -0
  68. package/src/components/Combobox/Combobox.test.ts +891 -0
  69. package/src/components/Combobox/Combobox.vue +934 -0
  70. package/src/components/Combobox/index.ts +2 -0
  71. package/src/components/DataTable/DataTable.test.ts +1221 -0
  72. package/src/components/DataTable/DataTable.vue +1415 -0
  73. package/src/components/DataTable/index.ts +10 -0
  74. package/src/components/DatePicker/DatePicker.test.ts +625 -0
  75. package/src/components/DatePicker/DatePicker.vue +1586 -0
  76. package/src/components/DatePicker/index.ts +2 -0
  77. package/src/components/Drawer/Drawer.test.ts +336 -0
  78. package/src/components/Drawer/Drawer.vue +466 -0
  79. package/src/components/Drawer/index.ts +2 -0
  80. package/src/components/Dropdown/Dropdown.test.ts +607 -0
  81. package/src/components/Dropdown/Dropdown.vue +807 -0
  82. package/src/components/Dropdown/DropdownItem.vue +227 -0
  83. package/src/components/Dropdown/DropdownSeparator.vue +14 -0
  84. package/src/components/Dropdown/DropdownSub.vue +104 -0
  85. package/src/components/Dropdown/DropdownSubContent.vue +187 -0
  86. package/src/components/Dropdown/DropdownSubTrigger.vue +151 -0
  87. package/src/components/Dropdown/index.ts +14 -0
  88. package/src/components/EmptyState/EmptyState.test.ts +180 -0
  89. package/src/components/EmptyState/EmptyState.vue +137 -0
  90. package/src/components/EmptyState/index.ts +2 -0
  91. package/src/components/FileUpload/FileUpload.test.ts +1151 -0
  92. package/src/components/FileUpload/FileUpload.vue +1042 -0
  93. package/src/components/FileUpload/index.ts +2 -0
  94. package/src/components/Heading/Heading.test.ts +107 -0
  95. package/src/components/Heading/Heading.vue +67 -0
  96. package/src/components/Heading/index.ts +2 -0
  97. package/src/components/Icon/Icon.test.ts +157 -0
  98. package/src/components/Icon/Icon.vue +86 -0
  99. package/src/components/Icon/index.ts +2 -0
  100. package/src/components/Input/Input.test.ts +273 -0
  101. package/src/components/Input/Input.vue +388 -0
  102. package/src/components/Input/index.ts +2 -0
  103. package/src/components/Layout/Container.vue +67 -0
  104. package/src/components/Layout/Grid.vue +159 -0
  105. package/src/components/Layout/GridItem.vue +154 -0
  106. package/src/components/Layout/Layout.test.ts +202 -0
  107. package/src/components/Layout/Stack.vue +128 -0
  108. package/src/components/Layout/index.ts +9 -0
  109. package/src/components/Layout/keys.ts +7 -0
  110. package/src/components/Modal/Modal.test.ts +311 -0
  111. package/src/components/Modal/Modal.vue +336 -0
  112. package/src/components/Modal/index.ts +2 -0
  113. package/src/components/Pagination/Pagination.test.ts +303 -0
  114. package/src/components/Pagination/Pagination.vue +212 -0
  115. package/src/components/Pagination/index.ts +3 -0
  116. package/src/components/Pagination/utils.ts +86 -0
  117. package/src/components/Popover/Popover.test.ts +285 -0
  118. package/src/components/Popover/Popover.vue +441 -0
  119. package/src/components/Popover/index.ts +2 -0
  120. package/src/components/Progress/Progress.test.ts +361 -0
  121. package/src/components/Progress/Progress.vue +363 -0
  122. package/src/components/Progress/index.ts +7 -0
  123. package/src/components/Radio/Radio.test.ts +216 -0
  124. package/src/components/Radio/Radio.vue +214 -0
  125. package/src/components/Radio/index.ts +2 -0
  126. package/src/components/Rating/Rating.test.ts +319 -0
  127. package/src/components/Rating/Rating.vue +247 -0
  128. package/src/components/Rating/index.ts +2 -0
  129. package/src/components/SegmentedControl/SegmentedControl.test.ts +292 -0
  130. package/src/components/SegmentedControl/SegmentedControl.vue +288 -0
  131. package/src/components/SegmentedControl/index.ts +2 -0
  132. package/src/components/Select/Select.test.ts +589 -0
  133. package/src/components/Select/Select.vue +666 -0
  134. package/src/components/Select/index.ts +2 -0
  135. package/src/components/Sidebar/Sidebar.test.ts +301 -0
  136. package/src/components/Sidebar/SidebarGroup.vue +103 -0
  137. package/src/components/Sidebar/SidebarItem.vue +196 -0
  138. package/src/components/Sidebar/SidebarLayout.vue +42 -0
  139. package/src/components/Sidebar/SidebarRoot.vue +122 -0
  140. package/src/components/Sidebar/index.ts +11 -0
  141. package/src/components/Sidebar/keys.ts +14 -0
  142. package/src/components/Skeleton/Skeleton.test.ts +130 -0
  143. package/src/components/Skeleton/Skeleton.vue +104 -0
  144. package/src/components/Skeleton/index.ts +2 -0
  145. package/src/components/Slider/Slider.test.ts +416 -0
  146. package/src/components/Slider/Slider.vue +435 -0
  147. package/src/components/Slider/index.ts +2 -0
  148. package/src/components/Slider/utils.ts +91 -0
  149. package/src/components/Spinner/Spinner.test.ts +79 -0
  150. package/src/components/Spinner/Spinner.vue +159 -0
  151. package/src/components/Spinner/index.ts +2 -0
  152. package/src/components/SpireProvider/SpireProvider.vue +71 -0
  153. package/src/components/SpireProvider/index.ts +11 -0
  154. package/src/components/Stepper/Stepper.test.ts +221 -0
  155. package/src/components/Stepper/StepperContent.vue +51 -0
  156. package/src/components/Stepper/StepperItem.vue +89 -0
  157. package/src/components/Stepper/StepperRoot.vue +101 -0
  158. package/src/components/Stepper/StepperSeparator.vue +52 -0
  159. package/src/components/Stepper/StepperTrigger.vue +144 -0
  160. package/src/components/Stepper/index.ts +11 -0
  161. package/src/components/Stepper/keys.ts +27 -0
  162. package/src/components/Switch/Switch.test.ts +214 -0
  163. package/src/components/Switch/Switch.vue +235 -0
  164. package/src/components/Switch/index.ts +2 -0
  165. package/src/components/Tabs/Tabs.test.ts +363 -0
  166. package/src/components/Tabs/Tabs.vue +318 -0
  167. package/src/components/Tabs/index.ts +2 -0
  168. package/src/components/Text/Text.test.ts +154 -0
  169. package/src/components/Text/Text.vue +100 -0
  170. package/src/components/Text/index.ts +2 -0
  171. package/src/components/Textarea/Textarea.test.ts +432 -0
  172. package/src/components/Textarea/Textarea.vue +411 -0
  173. package/src/components/Textarea/index.ts +2 -0
  174. package/src/components/TimePicker/TimePicker.test.ts +352 -0
  175. package/src/components/TimePicker/TimePicker.vue +569 -0
  176. package/src/components/TimePicker/index.ts +2 -0
  177. package/src/components/Timeline/Timeline.test.ts +193 -0
  178. package/src/components/Timeline/Timeline.vue +111 -0
  179. package/src/components/Timeline/TimelineItem.vue +167 -0
  180. package/src/components/Timeline/index.ts +13 -0
  181. package/src/components/Timeline/keys.ts +21 -0
  182. package/src/components/Toast/ToastItem.test.ts +289 -0
  183. package/src/components/Toast/ToastItem.vue +370 -0
  184. package/src/components/Toast/ToastProvider.test.ts +158 -0
  185. package/src/components/Toast/ToastProvider.vue +181 -0
  186. package/src/components/Toast/index.ts +83 -0
  187. package/src/components/Toast/toastState.test.ts +165 -0
  188. package/src/components/Toast/toastState.ts +161 -0
  189. package/src/components/ToggleButton/ToggleButton.test.ts +166 -0
  190. package/src/components/ToggleButton/ToggleButton.vue +197 -0
  191. package/src/components/ToggleButton/index.ts +2 -0
  192. package/src/components/ToggleGroup/ToggleGroup.test.ts +181 -0
  193. package/src/components/ToggleGroup/ToggleGroup.vue +130 -0
  194. package/src/components/ToggleGroup/index.ts +2 -0
  195. package/src/components/Tooltip/Tooltip.test.ts +238 -0
  196. package/src/components/Tooltip/Tooltip.vue +217 -0
  197. package/src/components/Tooltip/index.ts +2 -0
  198. package/src/components/TreeView/TreeView.test.ts +357 -0
  199. package/src/components/TreeView/TreeView.vue +251 -0
  200. package/src/components/TreeView/TreeViewItem.vue +288 -0
  201. package/src/components/TreeView/index.ts +11 -0
  202. package/src/components/TreeView/keys.ts +35 -0
  203. package/src/composables/index.ts +12 -0
  204. package/src/composables/useClickOutside.ts +36 -0
  205. package/src/composables/useClipboard.ts +35 -0
  206. package/src/composables/useEventListener.ts +48 -0
  207. package/src/composables/useFocusTrap.ts +58 -0
  208. package/src/composables/useHoverReveal.ts +98 -0
  209. package/src/composables/useId.ts +10 -0
  210. package/src/composables/useMagnetic.ts +171 -0
  211. package/src/composables/useRelativePosition.ts +127 -0
  212. package/src/composables/useRipple.ts +146 -0
  213. package/src/composables/useScrollLock.ts +25 -0
  214. package/src/composables/useSpireConfig.ts +27 -0
  215. package/src/composables/useStagger.ts +224 -0
  216. package/src/config/icons.test.ts +115 -0
  217. package/src/config/icons.ts +170 -0
  218. package/src/index.ts +361 -0
  219. package/src/styles/depth.css +129 -0
  220. package/src/styles/effects.css +169 -0
  221. package/src/styles/fallback.css +152 -0
  222. package/src/styles/main.css +25 -0
  223. package/src/styles/mood.css +211 -0
  224. package/src/styles/motion.css +159 -0
  225. package/src/styles/reset.css +97 -0
  226. package/src/styles/theme.css +708 -0
  227. package/src/styles/tokens.css +183 -0
  228. package/src/utils/.gitkeep +0 -0
  229. package/src/utils/color.ts +277 -0
  230. package/src/utils/date.test.ts +522 -0
  231. package/src/utils/date.ts +380 -0
  232. package/src/utils/index.ts +23 -0
  233. package/src/utils/object.test.ts +80 -0
  234. package/src/utils/object.ts +25 -0
  235. package/src/utils/string.test.ts +64 -0
  236. package/src/utils/string.ts +32 -0
  237. package/src/utils/time.ts +156 -0
@@ -0,0 +1,666 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
3
+ import { useId } from '../../composables'
4
+
5
+ export interface SelectOption {
6
+ /** Display text shown to user */
7
+ label: string
8
+ /** Value stored in v-model */
9
+ value: string | number
10
+ /** Disable this option */
11
+ disabled?: boolean
12
+ }
13
+
14
+ export interface SelectProps {
15
+ /** Selected value (v-model) */
16
+ modelValue?: string | number | null
17
+ /** Available options */
18
+ options: SelectOption[]
19
+ /** Label text above select */
20
+ label?: string
21
+ /** Placeholder when no selection */
22
+ placeholder?: string
23
+ /** Helper text below select */
24
+ hint?: string
25
+ /** Error message (also sets error state) */
26
+ error?: string
27
+ /** Select size - matches Input heights */
28
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
29
+ /** Disabled state */
30
+ disabled?: boolean
31
+ /** Required field */
32
+ required?: boolean
33
+ /** Make select full width */
34
+ block?: boolean
35
+ /** HTML id attribute */
36
+ id?: string
37
+ /** HTML name attribute (for forms) */
38
+ name?: string
39
+ }
40
+
41
+ const props = withDefaults(defineProps<SelectProps>(), {
42
+ modelValue: null,
43
+ placeholder: 'Select an option',
44
+ size: 'md',
45
+ disabled: false,
46
+ required: false,
47
+ block: false
48
+ })
49
+
50
+ const emit = defineEmits<{
51
+ (e: 'update:modelValue', value: string | number | null): void
52
+ (e: 'change', value: string | number | null): void
53
+ }>()
54
+
55
+ const triggerRef = ref<HTMLButtonElement | null>(null)
56
+ const listboxRef = ref<HTMLUListElement | null>(null)
57
+
58
+ const isOpen = ref(false)
59
+ const highlightedIndex = ref(-1)
60
+
61
+ const uid = useId('select')
62
+ const selectId = computed(() => props.id || uid)
63
+ const listboxId = computed(() => `${selectId.value}-listbox`)
64
+ const hintId = computed(() => `${selectId.value}-hint`)
65
+ const errorId = computed(() => `${selectId.value}-error`)
66
+
67
+ function getOptionId(index: number) {
68
+ return `${selectId.value}-option-${index}`
69
+ }
70
+
71
+ const describedBy = computed(() => {
72
+ if (props.error) return errorId.value
73
+ if (props.hint) return hintId.value
74
+ return undefined
75
+ })
76
+
77
+ const selectedOption = computed(() => {
78
+ return props.options.find(opt => opt.value === props.modelValue)
79
+ })
80
+
81
+ const displayText = computed(() => {
82
+ return selectedOption.value?.label || ''
83
+ })
84
+
85
+ const activeDescendant = computed(() => {
86
+ if (!isOpen.value || highlightedIndex.value < 0) return undefined
87
+ return getOptionId(highlightedIndex.value)
88
+ })
89
+
90
+ const menuStyle = ref<Record<string, string>>({})
91
+
92
+ /**
93
+ * Calculate menu position (below trigger, matching width)
94
+ */
95
+ function updateMenuPosition() {
96
+ if (!triggerRef.value) return
97
+
98
+ const rect = triggerRef.value.getBoundingClientRect()
99
+ menuStyle.value = {
100
+ position: 'fixed',
101
+ top: `${rect.bottom + 4}px`,
102
+ left: `${rect.left}px`,
103
+ minWidth: `${rect.width}px`,
104
+ maxWidth: `${Math.max(rect.width, 300)}px`
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Open the dropdown
110
+ */
111
+ function open() {
112
+ if (props.disabled || isOpen.value) return
113
+
114
+ isOpen.value = true
115
+ updateMenuPosition()
116
+
117
+ const selectedIdx = props.options.findIndex(opt => opt.value === props.modelValue)
118
+ if (selectedIdx >= 0 && !props.options[selectedIdx].disabled) {
119
+ highlightedIndex.value = selectedIdx
120
+ } else {
121
+ highlightedIndex.value = props.options.findIndex(opt => !opt.disabled)
122
+ }
123
+
124
+ nextTick(() => {
125
+ scrollToHighlighted()
126
+ })
127
+
128
+ document.addEventListener('mousedown', handleClickOutside)
129
+ window.addEventListener('resize', close)
130
+ window.addEventListener('scroll', updateMenuPosition, true)
131
+ }
132
+
133
+ /**
134
+ * Close the dropdown
135
+ */
136
+ function close() {
137
+ if (!isOpen.value) return
138
+
139
+ isOpen.value = false
140
+ highlightedIndex.value = -1
141
+
142
+ document.removeEventListener('mousedown', handleClickOutside)
143
+ window.removeEventListener('resize', close)
144
+ window.removeEventListener('scroll', updateMenuPosition, true)
145
+ }
146
+
147
+ /**
148
+ * Toggle open/close
149
+ */
150
+ function toggle() {
151
+ if (isOpen.value) {
152
+ close()
153
+ } else {
154
+ open()
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Handle click outside to close
160
+ */
161
+ function handleClickOutside(event: MouseEvent) {
162
+ const target = event.target as Node
163
+ if (
164
+ triggerRef.value?.contains(target) ||
165
+ listboxRef.value?.contains(target)
166
+ ) {
167
+ return
168
+ }
169
+ close()
170
+ }
171
+
172
+ /**
173
+ * Select an option
174
+ */
175
+ function selectOption(option: SelectOption) {
176
+ if (option.disabled) return
177
+
178
+ emit('update:modelValue', option.value)
179
+ emit('change', option.value)
180
+ close()
181
+ triggerRef.value?.focus()
182
+ }
183
+
184
+ /**
185
+ * Scroll highlighted option into view
186
+ */
187
+ function scrollToHighlighted() {
188
+ if (!listboxRef.value || highlightedIndex.value < 0) return
189
+
190
+ const option = listboxRef.value.children[highlightedIndex.value] as HTMLElement
191
+ if (option) {
192
+ option.scrollIntoView({ block: 'nearest' })
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Move highlight up/down
198
+ */
199
+ function moveHighlight(direction: 1 | -1) {
200
+ const len = props.options.length
201
+ let next = highlightedIndex.value
202
+
203
+ do {
204
+ next = (next + direction + len) % len
205
+ } while (props.options[next].disabled && next !== highlightedIndex.value)
206
+
207
+ if (!props.options[next].disabled) {
208
+ highlightedIndex.value = next
209
+ nextTick(scrollToHighlighted)
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Handle keyboard navigation
215
+ */
216
+ function handleKeydown(event: KeyboardEvent) {
217
+ switch (event.key) {
218
+ case 'Enter':
219
+ case ' ':
220
+ event.preventDefault()
221
+ if (isOpen.value && highlightedIndex.value >= 0) {
222
+ selectOption(props.options[highlightedIndex.value])
223
+ } else {
224
+ open()
225
+ }
226
+ break
227
+
228
+ case 'ArrowDown':
229
+ event.preventDefault()
230
+ if (!isOpen.value) {
231
+ open()
232
+ } else {
233
+ moveHighlight(1)
234
+ }
235
+ break
236
+
237
+ case 'ArrowUp':
238
+ event.preventDefault()
239
+ if (!isOpen.value) {
240
+ open()
241
+ } else {
242
+ moveHighlight(-1)
243
+ }
244
+ break
245
+
246
+ case 'Home':
247
+ event.preventDefault()
248
+ if (isOpen.value) {
249
+ highlightedIndex.value = props.options.findIndex(opt => !opt.disabled)
250
+ nextTick(scrollToHighlighted)
251
+ }
252
+ break
253
+
254
+ case 'End':
255
+ event.preventDefault()
256
+ if (isOpen.value) {
257
+ for (let i = props.options.length - 1; i >= 0; i--) {
258
+ if (!props.options[i].disabled) {
259
+ highlightedIndex.value = i
260
+ break
261
+ }
262
+ }
263
+ nextTick(scrollToHighlighted)
264
+ }
265
+ break
266
+
267
+ case 'Escape':
268
+ event.preventDefault()
269
+ close()
270
+ break
271
+
272
+ case 'Tab':
273
+ close()
274
+ break
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Handle option mouse enter (highlight on hover)
280
+ */
281
+ function handleOptionMouseEnter(index: number) {
282
+ if (!props.options[index].disabled) {
283
+ highlightedIndex.value = index
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Handle option click
289
+ */
290
+ function handleOptionClick(option: SelectOption, event: MouseEvent) {
291
+ event.preventDefault()
292
+ selectOption(option)
293
+ }
294
+
295
+ onBeforeUnmount(() => {
296
+ document.removeEventListener('mousedown', handleClickOutside)
297
+ window.removeEventListener('resize', close)
298
+ window.removeEventListener('scroll', updateMenuPosition, true)
299
+ })
300
+ </script>
301
+
302
+ <template>
303
+ <div
304
+ class="ui-select"
305
+ :class="[
306
+ `ui-select--${size}`,
307
+ {
308
+ 'ui-select--block': block,
309
+ 'ui-select--disabled': disabled,
310
+ 'ui-select--error': error,
311
+ 'ui-select--open': isOpen
312
+ }
313
+ ]"
314
+ >
315
+ <label
316
+ v-if="label"
317
+ :for="selectId"
318
+ class="ui-select__label"
319
+ >
320
+ {{ label }}
321
+ <span v-if="required" class="ui-select__required" aria-hidden="true">*</span>
322
+ </label>
323
+
324
+ <button
325
+ :id="selectId"
326
+ ref="triggerRef"
327
+ type="button"
328
+ role="combobox"
329
+ class="ui-select__trigger"
330
+ :class="[
331
+ `ui-select__trigger--${size}`,
332
+ {
333
+ 'ui-select__trigger--error': error,
334
+ 'ui-select__trigger--placeholder': !selectedOption
335
+ }
336
+ ]"
337
+ :disabled="disabled"
338
+ :aria-expanded="isOpen"
339
+ :aria-controls="listboxId"
340
+ :aria-activedescendant="activeDescendant"
341
+ :aria-invalid="error ? 'true' : undefined"
342
+ :aria-describedby="describedBy"
343
+ :aria-required="required"
344
+ aria-haspopup="listbox"
345
+ @click="toggle"
346
+ @keydown="handleKeydown"
347
+ >
348
+ <span class="ui-select__value">
349
+ {{ displayText || placeholder }}
350
+ </span>
351
+ <svg
352
+ class="ui-select__chevron"
353
+ :class="{ 'ui-select__chevron--open': isOpen }"
354
+ viewBox="0 0 24 24"
355
+ fill="none"
356
+ stroke="currentColor"
357
+ stroke-width="2"
358
+ stroke-linecap="round"
359
+ stroke-linejoin="round"
360
+ aria-hidden="true"
361
+ >
362
+ <path d="M6 9l6 6 6-6" />
363
+ </svg>
364
+ </button>
365
+
366
+ <select
367
+ v-if="name"
368
+ :name="name"
369
+ :value="modelValue ?? ''"
370
+ :required="required"
371
+ :disabled="disabled"
372
+ class="ui-select__native"
373
+ tabindex="-1"
374
+ aria-hidden="true"
375
+ >
376
+ <option value="" disabled>{{ placeholder }}</option>
377
+ <option
378
+ v-for="option in options"
379
+ :key="option.value"
380
+ :value="option.value"
381
+ :disabled="option.disabled"
382
+ >
383
+ {{ option.label }}
384
+ </option>
385
+ </select>
386
+
387
+ <Teleport to="body">
388
+ <Transition name="ui-select-menu">
389
+ <ul
390
+ v-if="isOpen"
391
+ :id="listboxId"
392
+ ref="listboxRef"
393
+ role="listbox"
394
+ class="ui-select__listbox"
395
+ :style="menuStyle"
396
+ :aria-labelledby="selectId"
397
+ >
398
+ <li
399
+ v-for="(option, index) in options"
400
+ :key="option.value"
401
+ :id="getOptionId(index)"
402
+ role="option"
403
+ class="ui-select__option"
404
+ :class="{
405
+ 'ui-select__option--selected': option.value === modelValue,
406
+ 'ui-select__option--highlighted': index === highlightedIndex,
407
+ 'ui-select__option--disabled': option.disabled
408
+ }"
409
+ :aria-selected="option.value === modelValue"
410
+ :aria-disabled="option.disabled"
411
+ @mouseenter="handleOptionMouseEnter(index)"
412
+ @click="handleOptionClick(option, $event)"
413
+ >
414
+ {{ option.label }}
415
+ <svg
416
+ v-if="option.value === modelValue"
417
+ class="ui-select__check"
418
+ viewBox="0 0 24 24"
419
+ fill="none"
420
+ stroke="currentColor"
421
+ stroke-width="2"
422
+ stroke-linecap="round"
423
+ stroke-linejoin="round"
424
+ aria-hidden="true"
425
+ >
426
+ <path d="M5 12l5 5L20 7" />
427
+ </svg>
428
+ </li>
429
+ </ul>
430
+ </Transition>
431
+ </Teleport>
432
+
433
+ <p
434
+ v-if="error"
435
+ :id="errorId"
436
+ class="ui-select__message ui-select__message--error"
437
+ role="alert"
438
+ >
439
+ {{ error }}
440
+ </p>
441
+ <p
442
+ v-else-if="hint"
443
+ :id="hintId"
444
+ class="ui-select__message ui-select__message--hint"
445
+ >
446
+ {{ hint }}
447
+ </p>
448
+ </div>
449
+ </template>
450
+
451
+ <style scoped>
452
+ .ui-select {
453
+ display: flex;
454
+ flex-direction: column;
455
+ gap: var(--space-1);
456
+ font-family: var(--font-sans);
457
+ }
458
+
459
+ .ui-select--block {
460
+ width: 100%;
461
+ }
462
+
463
+ .ui-select__label {
464
+ font-size: var(--text-sm);
465
+ font-weight: var(--font-medium);
466
+ color: var(--input-label);
467
+ line-height: var(--leading-tight);
468
+ }
469
+
470
+ .ui-select__required {
471
+ color: var(--input-error);
472
+ margin-left: var(--space-1);
473
+ }
474
+
475
+ .ui-select__trigger {
476
+ display: inline-flex;
477
+ align-items: center;
478
+ justify-content: space-between;
479
+ gap: var(--space-2);
480
+ width: 100%;
481
+ background-color: var(--input-bg);
482
+ border: 1px solid var(--input-border);
483
+ border-radius: var(--radius-md);
484
+ font-family: inherit;
485
+ font-size: var(--text-sm);
486
+ color: var(--input-text);
487
+ text-align: left;
488
+ cursor: pointer;
489
+ transition:
490
+ border-color var(--duration-fast) var(--ease-default),
491
+ box-shadow var(--duration-fast) var(--ease-default);
492
+ }
493
+
494
+ .ui-select__trigger--xs {
495
+ height: var(--input-height-xs);
496
+ padding: 0 var(--space-1);
497
+ font-size: var(--text-xs);
498
+ }
499
+
500
+ .ui-select__trigger--sm {
501
+ height: var(--input-height-sm);
502
+ padding: 0 var(--space-2);
503
+ font-size: var(--text-sm);
504
+ }
505
+
506
+ .ui-select__trigger--md {
507
+ height: var(--input-height-md);
508
+ padding: 0 var(--space-3);
509
+ font-size: var(--text-sm);
510
+ }
511
+
512
+ .ui-select__trigger--lg {
513
+ height: var(--input-height-lg);
514
+ padding: 0 var(--space-3);
515
+ font-size: var(--text-base);
516
+ }
517
+
518
+ .ui-select__trigger--xl {
519
+ height: var(--input-height-xl);
520
+ padding: 0 var(--space-4);
521
+ font-size: var(--text-base);
522
+ }
523
+
524
+ .ui-select__trigger:hover:not(:disabled) {
525
+ border-color: var(--input-border-hover);
526
+ }
527
+
528
+ .ui-select__trigger:focus-visible {
529
+ outline: none;
530
+ border-color: var(--input-border-focus);
531
+ box-shadow: 0 0 0 3px var(--ring-color);
532
+ }
533
+
534
+ .ui-select--open .ui-select__trigger {
535
+ border-color: var(--input-border-focus);
536
+ box-shadow: 0 0 0 3px var(--ring-color);
537
+ }
538
+
539
+ .ui-select__trigger--error {
540
+ border-color: var(--input-error);
541
+ }
542
+
543
+ .ui-select__trigger--error:focus-visible,
544
+ .ui-select--open .ui-select__trigger--error {
545
+ box-shadow: 0 0 0 3px var(--input-ring-error);
546
+ }
547
+
548
+ .ui-select__trigger:disabled {
549
+ opacity: 0.5;
550
+ cursor: not-allowed;
551
+ background-color: var(--input-bg-disabled);
552
+ }
553
+
554
+ .ui-select__trigger--placeholder {
555
+ color: var(--input-placeholder);
556
+ }
557
+
558
+ .ui-select__value {
559
+ flex: 1;
560
+ overflow: hidden;
561
+ text-overflow: ellipsis;
562
+ white-space: nowrap;
563
+ }
564
+
565
+ .ui-select__chevron {
566
+ flex-shrink: 0;
567
+ width: 1rem;
568
+ height: 1rem;
569
+ color: var(--input-icon);
570
+ transition: transform var(--duration-fast) var(--ease-default);
571
+ }
572
+
573
+ .ui-select__chevron--open {
574
+ transform: rotate(180deg);
575
+ }
576
+
577
+ .ui-select__native {
578
+ position: absolute;
579
+ width: 1px;
580
+ height: 1px;
581
+ padding: 0;
582
+ margin: -1px;
583
+ overflow: hidden;
584
+ clip: rect(0, 0, 0, 0);
585
+ white-space: nowrap;
586
+ border: 0;
587
+ }
588
+
589
+ .ui-select__message {
590
+ font-size: var(--text-xs);
591
+ line-height: var(--leading-normal);
592
+ margin: 0;
593
+ }
594
+
595
+ .ui-select__message--hint {
596
+ color: var(--input-hint);
597
+ }
598
+
599
+ .ui-select__message--error {
600
+ color: var(--input-error);
601
+ }
602
+ </style>
603
+
604
+ <style>
605
+ .ui-select__listbox {
606
+ z-index: 9999;
607
+ margin: 0;
608
+ padding: var(--space-1);
609
+ list-style: none;
610
+ background-color: var(--select-menu-bg);
611
+ border: 1px solid var(--select-menu-border);
612
+ border-radius: var(--radius-lg);
613
+ box-shadow: var(--shadow-lg);
614
+ max-height: 256px;
615
+ overflow-y: auto;
616
+ overscroll-behavior: contain;
617
+ }
618
+
619
+ .ui-select__option {
620
+ display: flex;
621
+ align-items: center;
622
+ justify-content: space-between;
623
+ gap: var(--space-2);
624
+ padding: var(--space-2) var(--space-3);
625
+ border-radius: var(--radius-md);
626
+ font-family: var(--font-sans);
627
+ font-size: var(--text-sm);
628
+ color: var(--select-option-text);
629
+ cursor: pointer;
630
+ transition: background-color var(--duration-fast) var(--ease-default);
631
+ }
632
+
633
+ .ui-select__option--highlighted {
634
+ background-color: var(--select-option-hover);
635
+ }
636
+
637
+ .ui-select__option--selected {
638
+ color: var(--select-option-selected);
639
+ font-weight: var(--font-medium);
640
+ }
641
+
642
+ .ui-select__option--disabled {
643
+ opacity: 0.5;
644
+ cursor: not-allowed;
645
+ }
646
+
647
+ .ui-select__check {
648
+ flex-shrink: 0;
649
+ width: 1rem;
650
+ height: 1rem;
651
+ color: var(--select-option-selected);
652
+ }
653
+
654
+ .ui-select-menu-enter-active,
655
+ .ui-select-menu-leave-active {
656
+ transition:
657
+ opacity var(--duration-fast) var(--ease-default),
658
+ transform var(--duration-fast) var(--ease-default);
659
+ }
660
+
661
+ .ui-select-menu-enter-from,
662
+ .ui-select-menu-leave-to {
663
+ opacity: 0;
664
+ transform: translateY(-4px);
665
+ }
666
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as Select } from './Select.vue'
2
+ export type { SelectProps, SelectOption } from './Select.vue'