@sakoa/ui 0.1.0

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 (345) hide show
  1. package/README.md +171 -0
  2. package/dist/App.d.ts +2 -0
  3. package/dist/cli/index.js +9243 -0
  4. package/dist/components/DemoSection.d.ts +30 -0
  5. package/dist/components/SApiKeyboard.d.ts +22 -0
  6. package/dist/components/SApiSection.d.ts +21 -0
  7. package/dist/components/SApiTable.d.ts +46 -0
  8. package/dist/components/STableOfContents.d.ts +2 -0
  9. package/dist/components/ui/SAlert.d.ts +76 -0
  10. package/dist/components/ui/SBadge.d.ts +56 -0
  11. package/dist/components/ui/SButton.d.ts +67 -0
  12. package/dist/components/ui/SCheckbox.d.ts +64 -0
  13. package/dist/components/ui/SChip.d.ts +43 -0
  14. package/dist/components/ui/SDatePicker.d.ts +77 -0
  15. package/dist/components/ui/SGlassButton.d.ts +70 -0
  16. package/dist/components/ui/SIcon.d.ts +29 -0
  17. package/dist/components/ui/SInput.d.ts +129 -0
  18. package/dist/components/ui/SKbd.d.ts +24 -0
  19. package/dist/components/ui/SKbdShortcut.d.ts +14 -0
  20. package/dist/components/ui/SSelect.d.ts +148 -0
  21. package/dist/components/ui/SSkeleton.d.ts +37 -0
  22. package/dist/components/ui/SSwitch.d.ts +61 -0
  23. package/dist/components/ui/STooltip.d.ts +82 -0
  24. package/dist/components/ui/accordion/SAccordionContent.d.ts +23 -0
  25. package/dist/components/ui/accordion/SAccordionItem.d.ts +70 -0
  26. package/dist/components/ui/accordion/SAccordionTrigger.d.ts +37 -0
  27. package/dist/components/ui/accordion/index.d.ts +4 -0
  28. package/dist/components/ui/avatar/SAvatar.d.ts +36 -0
  29. package/dist/components/ui/avatar/SAvatarFallback.d.ts +26 -0
  30. package/dist/components/ui/avatar/SAvatarGroup.d.ts +30 -0
  31. package/dist/components/ui/avatar/SAvatarImage.d.ts +23 -0
  32. package/dist/components/ui/avatar/index.d.ts +4 -0
  33. package/dist/components/ui/breadcrumb/SBreadcrumb.d.ts +22 -0
  34. package/dist/components/ui/breadcrumb/SBreadcrumbEllipsis.d.ts +17 -0
  35. package/dist/components/ui/breadcrumb/SBreadcrumbItem.d.ts +17 -0
  36. package/dist/components/ui/breadcrumb/SBreadcrumbLink.d.ts +26 -0
  37. package/dist/components/ui/breadcrumb/SBreadcrumbList.d.ts +17 -0
  38. package/dist/components/ui/breadcrumb/SBreadcrumbPage.d.ts +17 -0
  39. package/dist/components/ui/breadcrumb/SBreadcrumbSeparator.d.ts +17 -0
  40. package/dist/components/ui/breadcrumb/index.d.ts +7 -0
  41. package/dist/components/ui/card/SCard.d.ts +103 -0
  42. package/dist/components/ui/card/SCardActions.d.ts +44 -0
  43. package/dist/components/ui/card/SCardContent.d.ts +35 -0
  44. package/dist/components/ui/card/SCardFooter.d.ts +38 -0
  45. package/dist/components/ui/card/SCardHeader.d.ts +53 -0
  46. package/dist/components/ui/card/SCardMedia.d.ts +83 -0
  47. package/dist/components/ui/card/SGlassCard.d.ts +103 -0
  48. package/dist/components/ui/card/SMorphingCardContent.d.ts +18 -0
  49. package/dist/components/ui/card/index.d.ts +24 -0
  50. package/dist/components/ui/carousel/SCarousel.d.ts +166 -0
  51. package/dist/components/ui/carousel/index.d.ts +2 -0
  52. package/dist/components/ui/color-picker/SColorPickerAlphaSlider.d.ts +4 -0
  53. package/dist/components/ui/color-picker/SColorPickerCopy.d.ts +19 -0
  54. package/dist/components/ui/color-picker/SColorPickerEyeDropper.d.ts +17 -0
  55. package/dist/components/ui/color-picker/SColorPickerHueSlider.d.ts +4 -0
  56. package/dist/components/ui/color-picker/SColorPickerInputs.d.ts +2 -0
  57. package/dist/components/ui/color-picker/SColorPickerPresets.d.ts +9 -0
  58. package/dist/components/ui/color-picker/SColorPickerPreview.d.ts +2 -0
  59. package/dist/components/ui/color-picker/SColorPickerRecent.d.ts +7 -0
  60. package/dist/components/ui/color-picker/SColorPickerSpectrum.d.ts +4 -0
  61. package/dist/components/ui/color-picker/index.d.ts +11 -0
  62. package/dist/components/ui/drawer/index.d.ts +11 -0
  63. package/dist/components/ui/dropdown/SDropdownDivider.d.ts +8 -0
  64. package/dist/components/ui/dropdown/SDropdownGroup.d.ts +25 -0
  65. package/dist/components/ui/dropdown/SDropdownItem.d.ts +56 -0
  66. package/dist/components/ui/dropdown/index.d.ts +4 -0
  67. package/dist/components/ui/form/SForm.d.ts +38 -0
  68. package/dist/components/ui/form/SFormField.d.ts +31 -0
  69. package/dist/components/ui/form/index.d.ts +5 -0
  70. package/dist/components/ui/modal/index.d.ts +19 -0
  71. package/dist/components/ui/option/SOption.d.ts +32 -0
  72. package/dist/components/ui/option/SOptionGroup.d.ts +28 -0
  73. package/dist/components/ui/option/index.d.ts +2 -0
  74. package/dist/components/ui/otp/SOTP.d.ts +122 -0
  75. package/dist/components/ui/otp/SOTPGroup.d.ts +23 -0
  76. package/dist/components/ui/otp/SOTPSeparator.d.ts +17 -0
  77. package/dist/components/ui/otp/SOTPSlot.d.ts +49 -0
  78. package/dist/components/ui/otp/index.d.ts +7 -0
  79. package/dist/components/ui/otp/types.d.ts +26 -0
  80. package/dist/components/ui/otp/useOTPContext.d.ts +42 -0
  81. package/dist/components/ui/pagination/SPagination.d.ts +151 -0
  82. package/dist/components/ui/pagination/index.d.ts +2 -0
  83. package/dist/components/ui/progress/SProgress.d.ts +62 -0
  84. package/dist/components/ui/progress/SProgressRange.d.ts +91 -0
  85. package/dist/components/ui/progress/index.d.ts +4 -0
  86. package/dist/components/ui/radio/SRadio.d.ts +58 -0
  87. package/dist/components/ui/radio/SRadioGroup.d.ts +52 -0
  88. package/dist/components/ui/radio/index.d.ts +2 -0
  89. package/dist/components/ui/stepper/SStepper.d.ts +83 -0
  90. package/dist/components/ui/stepper/SStepperContent.d.ts +24 -0
  91. package/dist/components/ui/stepper/SStepperDescription.d.ts +20 -0
  92. package/dist/components/ui/stepper/SStepperIndicator.d.ts +37 -0
  93. package/dist/components/ui/stepper/SStepperItem.d.ts +37 -0
  94. package/dist/components/ui/stepper/SStepperSeparator.d.ts +5 -0
  95. package/dist/components/ui/stepper/SStepperTitle.d.ts +20 -0
  96. package/dist/components/ui/stepper/SStepperTrigger.d.ts +22 -0
  97. package/dist/components/ui/stepper/index.d.ts +11 -0
  98. package/dist/components/ui/table/STableBody.d.ts +27 -0
  99. package/dist/components/ui/table/STableCell.d.ts +55 -0
  100. package/dist/components/ui/table/STableColumn.d.ts +87 -0
  101. package/dist/components/ui/table/STableEmpty.d.ts +54 -0
  102. package/dist/components/ui/table/STableHeader.d.ts +25 -0
  103. package/dist/components/ui/table/STableRow.d.ts +38 -0
  104. package/dist/components/ui/table/STableSkeleton.d.ts +29 -0
  105. package/dist/components/ui/table/index.d.ts +98 -0
  106. package/dist/components/ui/table/useDataTable.d.ts +80 -0
  107. package/dist/components/ui/tabs/STabPane.d.ts +31 -0
  108. package/dist/components/ui/tabs/STabsContent.d.ts +21 -0
  109. package/dist/components/ui/tabs/STabsIndicator.d.ts +9 -0
  110. package/dist/components/ui/tabs/STabsTrigger.d.ts +28 -0
  111. package/dist/components/ui/tabs/index.d.ts +6 -0
  112. package/dist/components/ui/toast/SToast.d.ts +49 -0
  113. package/dist/components/ui/toast/SToastContainer.d.ts +21 -0
  114. package/dist/components/ui/toast/index.d.ts +2 -0
  115. package/dist/composables/useAsync.d.ts +134 -0
  116. package/dist/composables/useClickOutside.d.ts +69 -0
  117. package/dist/composables/useClipboard.d.ts +46 -0
  118. package/dist/composables/useDebounce.d.ts +150 -0
  119. package/dist/composables/useDialog.d.ts +118 -0
  120. package/dist/composables/useForm.d.ts +204 -0
  121. package/dist/composables/useHotkey.d.ts +128 -0
  122. package/dist/composables/useIntersectionObserver.d.ts +156 -0
  123. package/dist/composables/useLocalStorage.d.ts +120 -0
  124. package/dist/composables/useMediaQuery.d.ts +115 -0
  125. package/dist/composables/useTheme.d.ts +8 -0
  126. package/dist/composables/useToast.d.ts +1619 -0
  127. package/dist/index.d.ts +71 -0
  128. package/dist/layouts/UILayout.d.ts +2 -0
  129. package/dist/lib/utils.d.ts +2 -0
  130. package/dist/main.d.ts +0 -0
  131. package/dist/router.d.ts +2 -0
  132. package/dist/saka-ui.css +1 -0
  133. package/dist/saka-ui.js +18513 -0
  134. package/dist/saka-ui.umd.cjs +38 -0
  135. package/dist/views/docs/CustomizationView.d.ts +2 -0
  136. package/dist/views/docs/FormValidationView.d.ts +2 -0
  137. package/dist/views/docs/StylingGuideView.d.ts +2 -0
  138. package/dist/views/docs/UseAsyncView.d.ts +2 -0
  139. package/dist/views/docs/UseClickOutsideView.d.ts +124 -0
  140. package/dist/views/docs/UseClipboardView.d.ts +4 -0
  141. package/dist/views/docs/UseDebounceView.d.ts +2 -0
  142. package/dist/views/docs/UseHotkeyView.d.ts +205 -0
  143. package/dist/views/docs/UseIntersectionObserverView.d.ts +5 -0
  144. package/dist/views/docs/UseLocalStorageView.d.ts +2 -0
  145. package/dist/views/docs/UseMediaQueryView.d.ts +2 -0
  146. package/dist/views/docs/UseThemeView.d.ts +2 -0
  147. package/dist/views/examples/AuthFormView.d.ts +2 -0
  148. package/dist/views/examples/CreditCardFormView.d.ts +6 -0
  149. package/dist/views/examples/FormFieldExampleView.d.ts +2 -0
  150. package/dist/views/examples/ProjectFormView.d.ts +2 -0
  151. package/dist/views/ui/AccordionView.d.ts +2 -0
  152. package/dist/views/ui/AlertView.d.ts +2 -0
  153. package/dist/views/ui/AvatarView.d.ts +2 -0
  154. package/dist/views/ui/BadgeView.d.ts +2 -0
  155. package/dist/views/ui/BreadcrumbView.d.ts +2 -0
  156. package/dist/views/ui/ButtonView.d.ts +2 -0
  157. package/dist/views/ui/CardView.d.ts +2 -0
  158. package/dist/views/ui/CarouselView.d.ts +274 -0
  159. package/dist/views/ui/CheckboxView.d.ts +2 -0
  160. package/dist/views/ui/ChipView.d.ts +2 -0
  161. package/dist/views/ui/ColorPickerView.d.ts +2 -0
  162. package/dist/views/ui/DatePickerView.d.ts +2 -0
  163. package/dist/views/ui/DialogView.d.ts +2 -0
  164. package/dist/views/ui/DrawerView.d.ts +2 -0
  165. package/dist/views/ui/DropdownView.d.ts +2 -0
  166. package/dist/views/ui/GlassButtonView.d.ts +2 -0
  167. package/dist/views/ui/GlassCardView.d.ts +2 -0
  168. package/dist/views/ui/HomeView.d.ts +2 -0
  169. package/dist/views/ui/IconsView.d.ts +2 -0
  170. package/dist/views/ui/InputView.d.ts +2 -0
  171. package/dist/views/ui/KbdView.d.ts +2 -0
  172. package/dist/views/ui/ModalView.d.ts +2 -0
  173. package/dist/views/ui/MorphingCardView.d.ts +2 -0
  174. package/dist/views/ui/MorphingModalView.d.ts +2 -0
  175. package/dist/views/ui/OTPView.d.ts +206 -0
  176. package/dist/views/ui/PaginationView.d.ts +2 -0
  177. package/dist/views/ui/ProgressView.d.ts +2 -0
  178. package/dist/views/ui/RadioView.d.ts +2 -0
  179. package/dist/views/ui/SelectView.d.ts +2 -0
  180. package/dist/views/ui/SkeletonView.d.ts +2 -0
  181. package/dist/views/ui/StepperView.d.ts +2 -0
  182. package/dist/views/ui/SwitchView.d.ts +2 -0
  183. package/dist/views/ui/TableView.d.ts +2 -0
  184. package/dist/views/ui/TabsView.d.ts +2 -0
  185. package/dist/views/ui/ToastView.d.ts +2 -0
  186. package/dist/views/ui/TooltipView.d.ts +2 -0
  187. package/dist/vite.svg +1 -0
  188. package/package.json +64 -0
  189. package/registry/components/accordion.json +19 -0
  190. package/registry/components/alert.json +17 -0
  191. package/registry/components/avatar.json +18 -0
  192. package/registry/components/badge.json +14 -0
  193. package/registry/components/breadcrumb.json +24 -0
  194. package/registry/components/button.json +17 -0
  195. package/registry/components/card.json +23 -0
  196. package/registry/components/carousel.json +19 -0
  197. package/registry/components/checkbox.json +17 -0
  198. package/registry/components/chip.json +17 -0
  199. package/registry/components/color-picker.json +24 -0
  200. package/registry/components/date-picker.json +17 -0
  201. package/registry/components/drawer.json +26 -0
  202. package/registry/components/dropdown.json +21 -0
  203. package/registry/components/form.json +16 -0
  204. package/registry/components/glass-button.json +17 -0
  205. package/registry/components/icon.json +17 -0
  206. package/registry/components/input.json +17 -0
  207. package/registry/components/kbd.json +16 -0
  208. package/registry/components/modal.json +32 -0
  209. package/registry/components/option.json +16 -0
  210. package/registry/components/otp.json +23 -0
  211. package/registry/components/pagination.json +18 -0
  212. package/registry/components/progress.json +16 -0
  213. package/registry/components/radio.json +19 -0
  214. package/registry/components/select.json +17 -0
  215. package/registry/components/skeleton.json +14 -0
  216. package/registry/components/switch.json +17 -0
  217. package/registry/components/table.json +26 -0
  218. package/registry/components/tabs.json +19 -0
  219. package/registry/components/toast.json +19 -0
  220. package/registry/components/tooltip.json +14 -0
  221. package/registry/index.json +4 -0
  222. package/registry/source/components/ui/SAlert.vue +388 -0
  223. package/registry/source/components/ui/SBadge.vue +243 -0
  224. package/registry/source/components/ui/SButton.vue +387 -0
  225. package/registry/source/components/ui/SCheckbox.vue +310 -0
  226. package/registry/source/components/ui/SChip.vue +130 -0
  227. package/registry/source/components/ui/SDatePicker.vue +1290 -0
  228. package/registry/source/components/ui/SGlassButton.vue +547 -0
  229. package/registry/source/components/ui/SIcon.vue +78 -0
  230. package/registry/source/components/ui/SInput.vue +1054 -0
  231. package/registry/source/components/ui/SKbd.vue +96 -0
  232. package/registry/source/components/ui/SKbdShortcut.vue +36 -0
  233. package/registry/source/components/ui/SSelect.vue +1290 -0
  234. package/registry/source/components/ui/SSkeleton.vue +185 -0
  235. package/registry/source/components/ui/SSwitch.vue +275 -0
  236. package/registry/source/components/ui/STooltip.vue +491 -0
  237. package/registry/source/components/ui/accordion/SAccordion.vue +248 -0
  238. package/registry/source/components/ui/accordion/SAccordionItem.vue +353 -0
  239. package/registry/source/components/ui/accordion/index.ts +5 -0
  240. package/registry/source/components/ui/avatar/SAvatar.vue +169 -0
  241. package/registry/source/components/ui/avatar/SAvatarFallback.vue +66 -0
  242. package/registry/source/components/ui/avatar/SAvatarGroup.vue +69 -0
  243. package/registry/source/components/ui/avatar/SAvatarImage.vue +92 -0
  244. package/registry/source/components/ui/avatar/index.ts +5 -0
  245. package/registry/source/components/ui/breadcrumb/SBreadcrumb.vue +23 -0
  246. package/registry/source/components/ui/breadcrumb/SBreadcrumbEllipsis.vue +17 -0
  247. package/registry/source/components/ui/breadcrumb/SBreadcrumbItem.vue +14 -0
  248. package/registry/source/components/ui/breadcrumb/SBreadcrumbLink.vue +46 -0
  249. package/registry/source/components/ui/breadcrumb/SBreadcrumbList.vue +17 -0
  250. package/registry/source/components/ui/breadcrumb/SBreadcrumbPage.vue +15 -0
  251. package/registry/source/components/ui/breadcrumb/SBreadcrumbSeparator.vue +18 -0
  252. package/registry/source/components/ui/breadcrumb/index.ts +7 -0
  253. package/registry/source/components/ui/card/SCard.vue +517 -0
  254. package/registry/source/components/ui/card/SCardActions.vue +129 -0
  255. package/registry/source/components/ui/card/SCardContent.vue +117 -0
  256. package/registry/source/components/ui/card/SCardFooter.vue +103 -0
  257. package/registry/source/components/ui/card/SCardHeader.vue +163 -0
  258. package/registry/source/components/ui/card/SCardMedia.vue +312 -0
  259. package/registry/source/components/ui/card/index.ts +34 -0
  260. package/registry/source/components/ui/carousel/SCarousel.vue +1069 -0
  261. package/registry/source/components/ui/carousel/SCarouselSlide.vue +107 -0
  262. package/registry/source/components/ui/carousel/index.ts +3 -0
  263. package/registry/source/components/ui/color-picker/SColorPicker.vue +772 -0
  264. package/registry/source/components/ui/color-picker/SColorPickerAlphaSlider.vue +158 -0
  265. package/registry/source/components/ui/color-picker/SColorPickerCopy.vue +76 -0
  266. package/registry/source/components/ui/color-picker/SColorPickerEyeDropper.vue +68 -0
  267. package/registry/source/components/ui/color-picker/SColorPickerHueSlider.vue +138 -0
  268. package/registry/source/components/ui/color-picker/SColorPickerInputs.vue +227 -0
  269. package/registry/source/components/ui/color-picker/SColorPickerPresets.vue +87 -0
  270. package/registry/source/components/ui/color-picker/SColorPickerPreview.vue +46 -0
  271. package/registry/source/components/ui/color-picker/SColorPickerRecent.vue +74 -0
  272. package/registry/source/components/ui/color-picker/SColorPickerSpectrum.vue +149 -0
  273. package/registry/source/components/ui/color-picker/index.ts +11 -0
  274. package/registry/source/components/ui/drawer/SDrawer.vue +797 -0
  275. package/registry/source/components/ui/drawer/SDrawerClose.vue +64 -0
  276. package/registry/source/components/ui/drawer/SDrawerContent.vue +81 -0
  277. package/registry/source/components/ui/drawer/SDrawerDescription.vue +40 -0
  278. package/registry/source/components/ui/drawer/SDrawerFooter.vue +97 -0
  279. package/registry/source/components/ui/drawer/SDrawerHandle.vue +79 -0
  280. package/registry/source/components/ui/drawer/SDrawerHeader.vue +117 -0
  281. package/registry/source/components/ui/drawer/SDrawerTitle.vue +40 -0
  282. package/registry/source/components/ui/drawer/SDrawerTrigger.vue +51 -0
  283. package/registry/source/components/ui/drawer/index.ts +20 -0
  284. package/registry/source/components/ui/dropdown/SDropdown.vue +843 -0
  285. package/registry/source/components/ui/dropdown/SDropdownDivider.vue +23 -0
  286. package/registry/source/components/ui/dropdown/SDropdownGroup.vue +53 -0
  287. package/registry/source/components/ui/dropdown/SDropdownItem.vue +179 -0
  288. package/registry/source/components/ui/dropdown/index.ts +5 -0
  289. package/registry/source/components/ui/form/SForm.vue +84 -0
  290. package/registry/source/components/ui/form/SFormField.vue +78 -0
  291. package/registry/source/components/ui/form/index.ts +8 -0
  292. package/registry/source/components/ui/modal/SModal.vue +648 -0
  293. package/registry/source/components/ui/modal/SModalClose.vue +49 -0
  294. package/registry/source/components/ui/modal/SModalContent.vue +74 -0
  295. package/registry/source/components/ui/modal/SModalDescription.vue +39 -0
  296. package/registry/source/components/ui/modal/SModalFooter.vue +84 -0
  297. package/registry/source/components/ui/modal/SModalHeader.vue +107 -0
  298. package/registry/source/components/ui/modal/SModalTitle.vue +39 -0
  299. package/registry/source/components/ui/modal/SModalTrigger.vue +61 -0
  300. package/registry/source/components/ui/modal/SMorphingModal.vue +429 -0
  301. package/registry/source/components/ui/modal/SMorphingModalClose.vue +42 -0
  302. package/registry/source/components/ui/modal/SMorphingModalDescription.vue +49 -0
  303. package/registry/source/components/ui/modal/SMorphingModalImage.vue +44 -0
  304. package/registry/source/components/ui/modal/SMorphingModalSubtitle.vue +29 -0
  305. package/registry/source/components/ui/modal/SMorphingModalTitle.vue +34 -0
  306. package/registry/source/components/ui/modal/SMorphingModalTrigger.vue +95 -0
  307. package/registry/source/components/ui/modal/index.ts +32 -0
  308. package/registry/source/components/ui/option/SOption.vue +180 -0
  309. package/registry/source/components/ui/option/SOptionGroup.vue +77 -0
  310. package/registry/source/components/ui/option/index.ts +3 -0
  311. package/registry/source/components/ui/otp/SOTP.vue +843 -0
  312. package/registry/source/components/ui/otp/SOTPGroup.vue +29 -0
  313. package/registry/source/components/ui/otp/SOTPSeparator.vue +15 -0
  314. package/registry/source/components/ui/otp/SOTPSlot.vue +462 -0
  315. package/registry/source/components/ui/otp/index.ts +7 -0
  316. package/registry/source/components/ui/otp/types.ts +27 -0
  317. package/registry/source/components/ui/otp/useOTPContext.ts +62 -0
  318. package/registry/source/components/ui/pagination/SPagination.vue +923 -0
  319. package/registry/source/components/ui/pagination/index.ts +8 -0
  320. package/registry/source/components/ui/progress/SProgress.vue +635 -0
  321. package/registry/source/components/ui/progress/SProgressRange.vue +715 -0
  322. package/registry/source/components/ui/progress/index.ts +4 -0
  323. package/registry/source/components/ui/radio/SRadio.vue +407 -0
  324. package/registry/source/components/ui/radio/SRadioGroup.vue +200 -0
  325. package/registry/source/components/ui/radio/index.ts +3 -0
  326. package/registry/source/components/ui/table/SDataTable.vue +828 -0
  327. package/registry/source/components/ui/table/STableBody.vue +70 -0
  328. package/registry/source/components/ui/table/STableCell.vue +147 -0
  329. package/registry/source/components/ui/table/STableColumn.vue +120 -0
  330. package/registry/source/components/ui/table/STableEmpty.vue +159 -0
  331. package/registry/source/components/ui/table/STableHeader.vue +132 -0
  332. package/registry/source/components/ui/table/STableRow.vue +106 -0
  333. package/registry/source/components/ui/table/STableSkeleton.vue +208 -0
  334. package/registry/source/components/ui/table/index.ts +126 -0
  335. package/registry/source/components/ui/table/useDataTable.ts +519 -0
  336. package/registry/source/components/ui/tabs/STabPane.vue +130 -0
  337. package/registry/source/components/ui/tabs/STabs.vue +467 -0
  338. package/registry/source/components/ui/tabs/index.ts +7 -0
  339. package/registry/source/components/ui/toast/SToast.vue +261 -0
  340. package/registry/source/components/ui/toast/SToastContainer.vue +209 -0
  341. package/registry/source/components/ui/toast/index.ts +2 -0
  342. package/registry/source/composables/useForm.ts +960 -0
  343. package/registry/source/composables/useTheme.ts +86 -0
  344. package/registry/source/composables/useToast.ts +440 -0
  345. package/registry/source/lib/utils.ts +6 -0
@@ -0,0 +1,1290 @@
1
+ <script setup lang="ts">
2
+ defineOptions({ inheritAttrs: false })
3
+
4
+ import { ref, computed, watch, provide, onMounted, onBeforeUnmount, nextTick, type CSSProperties } from 'vue'
5
+ import { cn } from '../../lib/utils'
6
+
7
+ export interface SelectOption {
8
+ value: any
9
+ label?: string
10
+ disabled?: boolean
11
+ icon?: string
12
+ image?: string
13
+ description?: string
14
+ color?: string
15
+ group?: string
16
+ }
17
+
18
+ export interface Props {
19
+ modelValue?: any | any[]
20
+ options?: SelectOption[]
21
+ multiple?: boolean
22
+ searchable?: boolean
23
+ clearable?: boolean
24
+ disabled?: boolean
25
+ loading?: boolean
26
+ placeholder?: string
27
+ size?: 'small' | 'medium' | 'large'
28
+ color?: string
29
+ variant?: 'outlined' | 'filled' | 'underlined'
30
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full'
31
+ maxHeight?: string
32
+ closeOnSelect?: boolean
33
+ tagLimit?: number
34
+ noOptionsText?: string
35
+ noResultsText?: string
36
+ label?: string
37
+ error?: string
38
+ hint?: string
39
+ required?: boolean
40
+ teleport?: boolean | string
41
+ placement?: 'bottom' | 'top' | 'auto'
42
+ // New props
43
+ arrowIcon?: string
44
+ menuWidth?: string | number
45
+ menuAlign?: 'start' | 'end' | 'center'
46
+ creatable?: boolean
47
+ createText?: string
48
+ labelPlacement?: 'top' | 'top-left' | 'top-center' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-center' | 'bottom-right' | 'left' | 'left-top' | 'left-center' | 'left-bottom' | 'right' | 'right-top' | 'right-center' | 'right-bottom'
49
+ // Vuesax-inspired props
50
+ labelPlaceholder?: string // Float label that animates from inside to top
51
+ filter?: boolean // Inline filter - type directly in trigger
52
+ }
53
+
54
+ const props = withDefaults(defineProps<Props>(), {
55
+ modelValue: null,
56
+ options: () => [],
57
+ multiple: false,
58
+ searchable: false,
59
+ clearable: false,
60
+ disabled: false,
61
+ loading: false,
62
+ placeholder: 'Select...',
63
+ size: 'medium',
64
+ color: undefined,
65
+ variant: 'outlined',
66
+ rounded: 'md',
67
+ maxHeight: '280px',
68
+ closeOnSelect: undefined,
69
+ tagLimit: 3,
70
+ noOptionsText: 'No options available',
71
+ noResultsText: 'No results found',
72
+ label: undefined,
73
+ error: undefined,
74
+ hint: undefined,
75
+ required: false,
76
+ teleport: true,
77
+ placement: 'auto',
78
+ // New props defaults
79
+ arrowIcon: 'chevron-down',
80
+ menuWidth: undefined,
81
+ menuAlign: 'start',
82
+ creatable: false,
83
+ createText: 'Create "{query}"',
84
+ labelPlacement: 'top',
85
+ // Vuesax-inspired props defaults
86
+ labelPlaceholder: undefined,
87
+ filter: false
88
+ })
89
+
90
+ const emit = defineEmits<{
91
+ 'update:modelValue': [value: any | any[]]
92
+ 'change': [value: any | any[], event?: Event]
93
+ 'open': []
94
+ 'close': []
95
+ 'search': [query: string]
96
+ 'focus': [event: FocusEvent]
97
+ 'blur': [event: FocusEvent]
98
+ 'create': [value: string]
99
+ }>()
100
+
101
+ // Refs
102
+ const triggerRef = ref<HTMLElement | null>(null)
103
+ const dropdownRef = ref<HTMLElement | null>(null)
104
+ const searchInputRef = ref<HTMLInputElement | null>(null)
105
+ const filterInputRef = ref<HTMLInputElement | null>(null) // For inline filter
106
+ const isOpen = ref(false)
107
+ const isFocused = ref(false)
108
+ const searchQuery = ref('')
109
+ const filterQuery = ref('') // For inline filter mode
110
+ const isFilterActive = ref(false) // Track if user is typing in filter mode
111
+ const highlightedIndex = ref(-1)
112
+ const registeredOptions = ref<SelectOption[]>([])
113
+ const dropdownPosition = ref<{ top?: number; bottom?: number; left: number; width: number; placement: 'top' | 'bottom' }>({ top: 0, left: 0, width: 0, placement: 'bottom' })
114
+
115
+ // Computed
116
+ const shouldCloseOnSelect = computed(() => {
117
+ if (props.closeOnSelect !== undefined) return props.closeOnSelect
118
+ return !props.multiple
119
+ })
120
+
121
+ const selectedValues = computed(() => {
122
+ if (props.modelValue === null || props.modelValue === undefined) return []
123
+ return props.multiple ? (Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue]) : [props.modelValue]
124
+ })
125
+
126
+ const selectedOptions = computed(() => {
127
+ return selectedValues.value.map(val => {
128
+ const fromOptions = props.options.find(o => o.value === val)
129
+ if (fromOptions) return fromOptions
130
+ const fromRegistered = registeredOptions.value.find(o => o.value === val)
131
+ if (fromRegistered) return fromRegistered
132
+ return { value: val, label: String(val) }
133
+ })
134
+ })
135
+
136
+ const displayValue = computed(() => {
137
+ if (selectedOptions.value.length === 0) return ''
138
+ if (props.multiple) {
139
+ return selectedOptions.value.map(o => o.label ?? o.value).join(', ')
140
+ }
141
+ return selectedOptions.value[0]?.label ?? selectedOptions.value[0]?.value ?? ''
142
+ })
143
+
144
+ // Active query - uses filterQuery for inline filter mode, searchQuery for searchable mode
145
+ const activeQuery = computed(() => {
146
+ if (props.filter) return filterQuery.value
147
+ return searchQuery.value
148
+ })
149
+
150
+ const filteredOptions = computed(() => {
151
+ if (!activeQuery.value) return props.options
152
+ const query = activeQuery.value.toLowerCase()
153
+ return props.options.filter(option => {
154
+ const label = (option.label ?? String(option.value)).toLowerCase()
155
+ return label.includes(query)
156
+ })
157
+ })
158
+
159
+ // Float label should show above when focused/open or has value
160
+ const showFloatLabel = computed(() => {
161
+ return props.labelPlaceholder && (isFocused.value || isOpen.value || hasValue.value || filterQuery.value)
162
+ })
163
+
164
+ // Group options by their group property
165
+ const groupedOptions = computed(() => {
166
+ const groups: Map<string | undefined, SelectOption[]> = new Map()
167
+
168
+ for (const option of filteredOptions.value) {
169
+ const group = option.group
170
+ if (!groups.has(group)) {
171
+ groups.set(group, [])
172
+ }
173
+ groups.get(group)!.push(option)
174
+ }
175
+
176
+ return groups
177
+ })
178
+
179
+ const hasGroups = computed(() => {
180
+ return filteredOptions.value.some(o => o.group)
181
+ })
182
+
183
+ // Show create option when creatable is enabled and query doesn't match any option
184
+ const showCreateOption = computed(() => {
185
+ if (!props.creatable || !activeQuery.value.trim()) return false
186
+ const query = activeQuery.value.toLowerCase().trim()
187
+ return !props.options.some(o =>
188
+ (o.label ?? String(o.value)).toLowerCase() === query
189
+ )
190
+ })
191
+
192
+ const createOptionLabel = computed(() => {
193
+ return props.createText.replace('{query}', activeQuery.value.trim())
194
+ })
195
+
196
+ const hasValue = computed(() => {
197
+ if (props.multiple) {
198
+ return Array.isArray(props.modelValue) && props.modelValue.length > 0
199
+ }
200
+ return props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== ''
201
+ })
202
+
203
+ const visibleTags = computed(() => {
204
+ if (!props.multiple) return []
205
+ return selectedOptions.value.slice(0, props.tagLimit)
206
+ })
207
+
208
+ const hiddenTagCount = computed(() => {
209
+ if (!props.multiple) return 0
210
+ return Math.max(0, selectedOptions.value.length - props.tagLimit)
211
+ })
212
+
213
+ // Methods
214
+ const isSelected = (value: any) => selectedValues.value.includes(value)
215
+
216
+ const registerOption = (option: { value: any; label: string; disabled: boolean }) => {
217
+ const existingIndex = registeredOptions.value.findIndex(o => o.value === option.value)
218
+ if (existingIndex >= 0) {
219
+ return existingIndex
220
+ }
221
+ registeredOptions.value.push(option)
222
+ return registeredOptions.value.length - 1
223
+ }
224
+
225
+ const selectOption = (value: any) => {
226
+ if (props.disabled || props.loading) return
227
+
228
+ let newValue: any
229
+
230
+ if (props.multiple) {
231
+ const arr = Array.isArray(props.modelValue) ? [...props.modelValue] : []
232
+ const index = arr.indexOf(value)
233
+ if (index >= 0) {
234
+ arr.splice(index, 1)
235
+ } else {
236
+ arr.push(value)
237
+ }
238
+ newValue = arr
239
+ } else {
240
+ newValue = value
241
+ }
242
+
243
+ emit('update:modelValue', newValue)
244
+ emit('change', newValue)
245
+
246
+ // Clear filter state after selection
247
+ filterQuery.value = ''
248
+ isFilterActive.value = false
249
+
250
+ if (shouldCloseOnSelect.value) {
251
+ close()
252
+ }
253
+ }
254
+
255
+ const createOption = () => {
256
+ if (!props.creatable || !activeQuery.value.trim()) return
257
+ const newValue = activeQuery.value.trim()
258
+ emit('create', newValue)
259
+ selectOption(newValue)
260
+ searchQuery.value = ''
261
+ filterQuery.value = ''
262
+ isFilterActive.value = false
263
+ }
264
+
265
+ const removeTag = (value: any, event?: Event) => {
266
+ event?.stopPropagation()
267
+ if (props.disabled || props.loading) return
268
+
269
+ if (props.multiple && Array.isArray(props.modelValue)) {
270
+ const newValue = props.modelValue.filter(v => v !== value)
271
+ emit('update:modelValue', newValue)
272
+ emit('change', newValue)
273
+ }
274
+ }
275
+
276
+ const clear = (event?: Event) => {
277
+ event?.stopPropagation()
278
+ if (props.disabled || props.loading) return
279
+
280
+ const newValue = props.multiple ? [] : null
281
+ emit('update:modelValue', newValue)
282
+ emit('change', newValue)
283
+ searchQuery.value = ''
284
+ }
285
+
286
+ const updateDropdownPosition = () => {
287
+ if (!triggerRef.value) return
288
+
289
+ const rect = triggerRef.value.getBoundingClientRect()
290
+ const viewportHeight = window.innerHeight
291
+ const spaceBelow = viewportHeight - rect.bottom
292
+ const spaceAbove = rect.top
293
+ const dropdownHeight = parseInt(props.maxHeight) || 280
294
+
295
+ let placement: 'top' | 'bottom' = 'bottom'
296
+
297
+ if (props.placement === 'auto') {
298
+ placement = spaceBelow < dropdownHeight && spaceAbove > spaceBelow ? 'top' : 'bottom'
299
+ } else {
300
+ placement = props.placement === 'top' ? 'top' : 'bottom'
301
+ }
302
+
303
+ dropdownPosition.value = {
304
+ top: placement === 'bottom' ? rect.bottom + 2 : undefined,
305
+ bottom: placement === 'top' ? window.innerHeight - rect.top + 2 : undefined,
306
+ left: rect.left,
307
+ width: rect.width,
308
+ placement
309
+ }
310
+ }
311
+
312
+ const open = () => {
313
+ if (props.disabled || props.loading || isOpen.value) return
314
+
315
+ isOpen.value = true
316
+ highlightedIndex.value = -1
317
+ updateDropdownPosition()
318
+ emit('open')
319
+
320
+ nextTick(() => {
321
+ // Focus appropriate input based on mode
322
+ if (props.filter && filterInputRef.value) {
323
+ filterInputRef.value.focus()
324
+ isFilterActive.value = true
325
+ } else if (props.searchable && searchInputRef.value) {
326
+ searchInputRef.value.focus()
327
+ }
328
+ // Find first selected option to highlight
329
+ if (selectedValues.value.length > 0) {
330
+ const allOptions = props.options.length > 0 ? props.options : registeredOptions.value
331
+ const selectedIndex = allOptions.findIndex(o => o.value === selectedValues.value[0])
332
+ if (selectedIndex >= 0) {
333
+ highlightedIndex.value = selectedIndex
334
+ }
335
+ }
336
+ })
337
+ }
338
+
339
+ const close = () => {
340
+ if (!isOpen.value) return
341
+
342
+ isOpen.value = false
343
+ searchQuery.value = ''
344
+ filterQuery.value = ''
345
+ isFilterActive.value = false
346
+ highlightedIndex.value = -1
347
+ emit('close')
348
+ }
349
+
350
+ const toggle = () => {
351
+ if (isOpen.value) {
352
+ close()
353
+ } else {
354
+ open()
355
+ }
356
+ }
357
+
358
+ const handleFocus = (event: FocusEvent) => {
359
+ isFocused.value = true
360
+ emit('focus', event)
361
+ }
362
+
363
+ const handleBlur = (event: FocusEvent) => {
364
+ // Check if focus moved to dropdown
365
+ const relatedTarget = event.relatedTarget as HTMLElement
366
+ if (dropdownRef.value?.contains(relatedTarget)) return
367
+
368
+ isFocused.value = false
369
+ emit('blur', event)
370
+ }
371
+
372
+ const handleKeydown = (event: KeyboardEvent) => {
373
+ if (props.disabled || props.loading) return
374
+
375
+ const allOptions = filteredOptions.value.length > 0 ? filteredOptions.value : registeredOptions.value
376
+ const enabledOptions = allOptions.filter(o => !o.disabled)
377
+
378
+ switch (event.key) {
379
+ case 'Enter':
380
+ event.preventDefault()
381
+ if (!isOpen.value) {
382
+ open()
383
+ } else if (highlightedIndex.value >= 0 && allOptions[highlightedIndex.value]) {
384
+ selectOption(allOptions[highlightedIndex.value].value)
385
+ } else if (props.creatable && activeQuery.value.trim()) {
386
+ createOption()
387
+ }
388
+ break
389
+
390
+ case ' ':
391
+ // In filter mode, space should type a space, not toggle
392
+ if (props.filter && isFilterActive.value) {
393
+ return // Let the space character be typed
394
+ }
395
+ event.preventDefault()
396
+ if (!isOpen.value) {
397
+ open()
398
+ } else if (highlightedIndex.value >= 0 && allOptions[highlightedIndex.value]) {
399
+ selectOption(allOptions[highlightedIndex.value].value)
400
+ }
401
+ break
402
+
403
+ case 'Escape':
404
+ event.preventDefault()
405
+ close()
406
+ if (props.filter) {
407
+ filterInputRef.value?.blur()
408
+ }
409
+ triggerRef.value?.focus()
410
+ break
411
+
412
+ case 'ArrowDown':
413
+ event.preventDefault()
414
+ if (!isOpen.value) {
415
+ open()
416
+ } else {
417
+ // Find next enabled option
418
+ let nextIndex = highlightedIndex.value + 1
419
+ while (nextIndex < allOptions.length && allOptions[nextIndex]?.disabled) {
420
+ nextIndex++
421
+ }
422
+ if (nextIndex < allOptions.length) {
423
+ highlightedIndex.value = nextIndex
424
+ }
425
+ }
426
+ break
427
+
428
+ case 'ArrowUp':
429
+ event.preventDefault()
430
+ if (!isOpen.value) {
431
+ open()
432
+ } else {
433
+ // Find previous enabled option
434
+ let prevIndex = highlightedIndex.value - 1
435
+ while (prevIndex >= 0 && allOptions[prevIndex]?.disabled) {
436
+ prevIndex--
437
+ }
438
+ if (prevIndex >= 0) {
439
+ highlightedIndex.value = prevIndex
440
+ }
441
+ }
442
+ break
443
+
444
+ case 'Home':
445
+ event.preventDefault()
446
+ if (isOpen.value) {
447
+ highlightedIndex.value = enabledOptions.length > 0 ? allOptions.indexOf(enabledOptions[0]) : 0
448
+ }
449
+ break
450
+
451
+ case 'End':
452
+ event.preventDefault()
453
+ if (isOpen.value) {
454
+ highlightedIndex.value = enabledOptions.length > 0
455
+ ? allOptions.indexOf(enabledOptions[enabledOptions.length - 1])
456
+ : allOptions.length - 1
457
+ }
458
+ break
459
+
460
+ case 'Tab':
461
+ close()
462
+ break
463
+ }
464
+ }
465
+
466
+ const handleSearchInput = (event: Event) => {
467
+ const target = event.target as HTMLInputElement
468
+ searchQuery.value = target.value
469
+ emit('search', target.value)
470
+ highlightedIndex.value = 0
471
+ }
472
+
473
+ // Handle inline filter input
474
+ const handleFilterInput = (event: Event) => {
475
+ const target = event.target as HTMLInputElement
476
+ filterQuery.value = target.value
477
+ isFilterActive.value = true
478
+ emit('search', target.value)
479
+ highlightedIndex.value = 0
480
+
481
+ // Auto-open dropdown when typing
482
+ if (!isOpen.value && target.value) {
483
+ open()
484
+ }
485
+ }
486
+
487
+ // Handle filter input focus
488
+ const handleFilterFocus = (event: FocusEvent) => {
489
+ isFocused.value = true
490
+ isFilterActive.value = true
491
+ emit('focus', event)
492
+ }
493
+
494
+ // Handle filter input blur
495
+ const handleFilterBlur = (event: FocusEvent) => {
496
+ const relatedTarget = event.relatedTarget as HTMLElement
497
+ if (dropdownRef.value?.contains(relatedTarget)) return
498
+ if (triggerRef.value?.contains(relatedTarget)) return
499
+
500
+ isFocused.value = false
501
+ isFilterActive.value = false
502
+
503
+ // Reset filter query when blurring without selecting
504
+ if (!isOpen.value) {
505
+ filterQuery.value = ''
506
+ }
507
+ emit('blur', event)
508
+ }
509
+
510
+ const handleClickOutside = (event: MouseEvent) => {
511
+ const target = event.target as HTMLElement
512
+ if (
513
+ !triggerRef.value?.contains(target) &&
514
+ !dropdownRef.value?.contains(target)
515
+ ) {
516
+ close()
517
+ }
518
+ }
519
+
520
+ // Watchers
521
+ watch(isOpen, (val) => {
522
+ if (val) {
523
+ window.addEventListener('scroll', updateDropdownPosition, true)
524
+ window.addEventListener('resize', updateDropdownPosition)
525
+ } else {
526
+ window.removeEventListener('scroll', updateDropdownPosition, true)
527
+ window.removeEventListener('resize', updateDropdownPosition)
528
+ }
529
+ })
530
+
531
+ // Lifecycle
532
+ onMounted(() => {
533
+ document.addEventListener('mousedown', handleClickOutside)
534
+ })
535
+
536
+ onBeforeUnmount(() => {
537
+ document.removeEventListener('mousedown', handleClickOutside)
538
+ window.removeEventListener('scroll', updateDropdownPosition, true)
539
+ window.removeEventListener('resize', updateDropdownPosition)
540
+ })
541
+
542
+ // Provide context to child options
543
+ provide('s-select-context', {
544
+ modelValue: computed(() => props.modelValue),
545
+ multiple: computed(() => props.multiple),
546
+ highlightedIndex,
547
+ registerOption,
548
+ selectOption,
549
+ isSelected,
550
+ color: computed(() => props.color),
551
+ size: computed(() => props.size)
552
+ })
553
+
554
+ // Size configurations
555
+ const sizeConfig = computed(() => {
556
+ const sizes = {
557
+ small: {
558
+ trigger: 'min-h-8 text-xs',
559
+ padding: 'px-2.5 py-0.5',
560
+ icon: 'text-sm',
561
+ tag: 'text-xs px-1.5 py-0.5',
562
+ label: 'text-xs',
563
+ option: 'py-0.5 px-3 my-0.5 text-xs'
564
+ },
565
+ medium: {
566
+ trigger: 'min-h-10 text-sm',
567
+ padding: 'px-3 py-0.5',
568
+ icon: 'text-base',
569
+ tag: 'text-xs px-2 py-0.5',
570
+ label: 'text-sm',
571
+ option: 'py-1 px-3 my-0.5 text-sm'
572
+ },
573
+ large: {
574
+ trigger: 'min-h-12 text-base',
575
+ padding: 'px-4 py-0.5',
576
+ icon: 'text-lg',
577
+ tag: 'text-sm px-2.5 py-0.5',
578
+ label: 'text-base',
579
+ option: 'py-1.5 px-4 my-0.5 text-base'
580
+ }
581
+ }
582
+ return sizes[props.size]
583
+ })
584
+
585
+ // Rounded classes
586
+ const roundedConfig = computed(() => {
587
+ const radii = {
588
+ none: 'rounded-none',
589
+ sm: 'rounded',
590
+ md: 'rounded-lg',
591
+ lg: 'rounded-xl',
592
+ full: 'rounded-full'
593
+ }
594
+ return radii[props.rounded]
595
+ })
596
+
597
+ // Dropdown rounded classes (max at xl to prevent oval shape)
598
+ const dropdownRoundedConfig = computed(() => {
599
+ const radii = {
600
+ none: 'rounded-none',
601
+ sm: 'rounded',
602
+ md: 'rounded-lg',
603
+ lg: 'rounded-xl',
604
+ full: 'rounded-xl' // Cap at xl for dropdown
605
+ }
606
+ return radii[props.rounded]
607
+ })
608
+
609
+ // Variant styles
610
+ const variantClasses = computed(() => {
611
+ const base = {
612
+ outlined: 'border bg-background border-border hover:border-input',
613
+ filled: 'border-transparent bg-accent',
614
+ underlined: 'border-b border-t-0 border-l-0 border-r-0 rounded-none bg-transparent border-border'
615
+ }
616
+ return base[props.variant]
617
+ })
618
+
619
+ const focusClasses = computed(() => {
620
+ if (!isFocused.value && !isOpen.value) return ''
621
+ if (props.color) return '' // handled by inline style
622
+ return props.variant === 'underlined'
623
+ ? 'border-primary'
624
+ : 'ring-2 ring-ring/20 border-primary'
625
+ })
626
+
627
+ // Focus inline style when custom color is set
628
+ const focusStyle = computed<CSSProperties | undefined>(() => {
629
+ if ((!isFocused.value && !isOpen.value) || !props.color) return undefined
630
+ if (props.variant === 'underlined') {
631
+ return { borderColor: props.color }
632
+ }
633
+ return {
634
+ borderColor: props.color,
635
+ boxShadow: `0 0 0 2px color-mix(in srgb, ${props.color} 20%, transparent)`
636
+ }
637
+ })
638
+
639
+ const teleportTarget = computed(() => {
640
+ if (props.teleport === true) return 'body'
641
+ if (typeof props.teleport === 'string') return props.teleport
642
+ return undefined
643
+ })
644
+
645
+ // Label placement layout classes
646
+ // Label placement layout classes
647
+ const labelLayoutClasses = computed(() => {
648
+ const [side, align] = props.labelPlacement.split('-') as [string, string | undefined]
649
+ const classes = ['flex', 'w-full'] // Make sure wrapper is full width
650
+
651
+ // Vertical placement (Top / Bottom)
652
+ if (side === 'top' || side === 'bottom') {
653
+ classes.push('gap-1.5') // Gap for vertical
654
+ if (side === 'bottom') classes.push('flex-col-reverse')
655
+ else classes.push('flex-col')
656
+ // Vertical always stretches to keep input full width
657
+ // Text alignment is handled on the label itself
658
+ }
659
+ // Horizontal placement (Left / Right)
660
+ else {
661
+ classes.push('gap-3') // Gap for horizontal
662
+ if (side === 'right') classes.push('flex-row-reverse')
663
+ else classes.push('flex-row') // left
664
+
665
+ // Alignment (Vertical axis for row)
666
+ if (align === 'center') classes.push('items-center')
667
+ else if (align === 'bottom') classes.push('items-end')
668
+ else classes.push('items-start') // default top/start
669
+ }
670
+
671
+ return classes.join(' ')
672
+ })
673
+
674
+ const labelClasses = computed(() => {
675
+ const base = 'font-medium text-muted-foreground'
676
+ const [side, align] = props.labelPlacement.split('-')
677
+
678
+ let alignClass = ''
679
+ // Only apply text alignment for vertical layouts
680
+ if (side === 'top' || side === 'bottom') {
681
+ if (align === 'center') alignClass = 'text-center'
682
+ else if (align === 'right') alignClass = 'text-right'
683
+ else alignClass = 'text-left'
684
+ }
685
+
686
+ return `${base} ${alignClass} ${sizeConfig.value.label}`
687
+ })
688
+
689
+ // Float label background based on variant
690
+ const floatLabelBackground = computed(() => {
691
+ switch (props.variant) {
692
+ case 'filled':
693
+ return 'var(--s-accent)'
694
+ case 'underlined':
695
+ return 'transparent'
696
+ default: // outlined
697
+ return 'var(--s-background)'
698
+ }
699
+ })
700
+
701
+ // Resolved color for inline styles - falls back to CSS variable
702
+ const resolvedColor = computed(() => props.color ?? 'var(--s-primary)')
703
+ </script>
704
+
705
+ <template>
706
+ <div v-bind="$attrs" :class="cn('s-select relative w-full', labelLayoutClasses, $attrs.class as string)">
707
+ <!-- Static Label (traditional placement) -->
708
+ <label
709
+ v-if="label && !labelPlaceholder"
710
+ class="shrink-0"
711
+ :class="labelClasses"
712
+ >
713
+ {{ label }}
714
+ <span v-if="required" class="text-red-500 ml-0.5">*</span>
715
+ </label>
716
+
717
+ <!-- Select wrapper -->
718
+ <div class="flex-1 min-w-0">
719
+
720
+ <!-- Trigger -->
721
+ <div
722
+ ref="triggerRef"
723
+ role="combobox"
724
+ :aria-expanded="isOpen"
725
+ :aria-haspopup="true"
726
+ :aria-disabled="disabled || loading"
727
+ :tabindex="filter ? -1 : 0"
728
+ class="s-select-trigger relative flex items-center gap-2 cursor-pointer transition-all duration-250 outline-none"
729
+ :class="[
730
+ sizeConfig.trigger,
731
+ sizeConfig.padding,
732
+ roundedConfig,
733
+ variantClasses,
734
+ focusClasses,
735
+ {
736
+ 'opacity-50 cursor-not-allowed': disabled,
737
+ 'cursor-wait': loading,
738
+ 's-select-trigger--float-label': labelPlaceholder,
739
+ 's-select-trigger--elevated': (isFocused || isOpen) && !disabled
740
+ }
741
+ ]"
742
+ :style="focusStyle"
743
+ @click="!filter && toggle()"
744
+ @keydown="!filter && handleKeydown($event)"
745
+ @focus="!filter && handleFocus($event)"
746
+ @blur="!filter && handleBlur($event)"
747
+ >
748
+ <!-- Float Label (Vuesax-style label-placeholder) -->
749
+ <label
750
+ v-if="labelPlaceholder"
751
+ class="s-float-label absolute left-2.5 transition-all duration-250 ease-out pointer-events-none z-10"
752
+ :class="[
753
+ showFloatLabel
754
+ ? 's-float-label--active text-xs px-1.5 opacity-100'
755
+ : 'text-sm translate-y-0 opacity-40',
756
+ { 'text-muted-foreground': !showFloatLabel }
757
+ ]"
758
+ :style="showFloatLabel ? { color: resolvedColor, backgroundColor: floatLabelBackground } : {}"
759
+ >
760
+ {{ labelPlaceholder }}
761
+ <span v-if="required" class="text-red-500 ml-0.5">*</span>
762
+ </label>
763
+
764
+ <!-- Prefix slot -->
765
+ <slot name="prefix" />
766
+
767
+ <!-- Inline Filter Input (Vuesax-style filter) -->
768
+ <template v-if="filter && !multiple">
769
+ <input
770
+ ref="filterInputRef"
771
+ type="text"
772
+ :value="isFilterActive ? filterQuery : (hasValue ? displayValue : '')"
773
+ :placeholder="hasValue ? '' : (labelPlaceholder || placeholder)"
774
+ :disabled="disabled || loading"
775
+ class="s-filter-input flex-1 min-w-0 bg-transparent outline-none text-foreground placeholder:text-muted-foreground"
776
+ :class="{ 'cursor-not-allowed': disabled }"
777
+ @input="handleFilterInput"
778
+ @focus="handleFilterFocus"
779
+ @blur="handleFilterBlur"
780
+ @keydown="handleKeydown"
781
+ @click.stop="!isOpen && open()"
782
+ />
783
+ </template>
784
+
785
+ <!-- Selected value display (when not in filter mode or in multiple mode) -->
786
+ <div v-else class="flex-1 flex items-center gap-1.5 min-w-0 overflow-hidden">
787
+ <!-- Multiple: Tags -->
788
+ <template v-if="multiple && visibleTags.length > 0">
789
+ <TransitionGroup
790
+ enter-active-class="transition-all duration-200 ease-out"
791
+ enter-from-class="scale-90 opacity-0"
792
+ enter-to-class="scale-100 opacity-100"
793
+ leave-active-class="transition-all duration-150 ease-in absolute"
794
+ leave-from-class="scale-100 opacity-100"
795
+ leave-to-class="scale-90 opacity-0"
796
+ tag="div"
797
+ class="flex flex-wrap gap-1"
798
+ >
799
+ <slot
800
+ v-for="option in visibleTags"
801
+ :key="option.value"
802
+ name="tag"
803
+ :option="option"
804
+ :remove="() => removeTag(option.value)"
805
+ >
806
+ <span
807
+ class="inline-flex items-center gap-1 rounded-md transition-colors"
808
+ :class="sizeConfig.tag"
809
+ :style="{
810
+ backgroundColor: `color-mix(in srgb, ${resolvedColor} 15%, transparent)`,
811
+ color: resolvedColor
812
+ }"
813
+ >
814
+ <span class="truncate max-w-24">{{ option.label ?? option.value }}</span>
815
+ <button
816
+ type="button"
817
+ class="mdi mdi-close text-xs opacity-70 hover:opacity-100 transition-opacity"
818
+ @click="removeTag(option.value, $event)"
819
+ />
820
+ </span>
821
+ </slot>
822
+ <span
823
+ v-if="hiddenTagCount > 0"
824
+ :key="'more'"
825
+ class="text-muted-foreground"
826
+ :class="sizeConfig.tag"
827
+ >
828
+ +{{ hiddenTagCount }}
829
+ </span>
830
+ </TransitionGroup>
831
+
832
+ <!-- Inline filter input for multiple mode -->
833
+ <input
834
+ v-if="filter"
835
+ ref="filterInputRef"
836
+ type="text"
837
+ :value="filterQuery"
838
+ :placeholder="visibleTags.length === 0 ? (labelPlaceholder || placeholder) : ''"
839
+ :disabled="disabled || loading"
840
+ class="s-filter-input flex-1 min-w-[60px] bg-transparent outline-none text-foreground placeholder:text-muted-foreground text-sm"
841
+ @input="handleFilterInput"
842
+ @focus="handleFilterFocus"
843
+ @blur="handleFilterBlur"
844
+ @keydown="handleKeydown"
845
+ @click.stop="!isOpen && open()"
846
+ />
847
+ </template>
848
+
849
+ <!-- Multiple with no tags but filter enabled -->
850
+ <template v-else-if="multiple && filter && visibleTags.length === 0">
851
+ <input
852
+ ref="filterInputRef"
853
+ type="text"
854
+ :value="filterQuery"
855
+ :placeholder="labelPlaceholder || placeholder"
856
+ :disabled="disabled || loading"
857
+ class="s-filter-input flex-1 min-w-0 bg-transparent outline-none text-foreground placeholder:text-muted-foreground"
858
+ @input="handleFilterInput"
859
+ @focus="handleFilterFocus"
860
+ @blur="handleFilterBlur"
861
+ @keydown="handleKeydown"
862
+ @click.stop="!isOpen && open()"
863
+ />
864
+ </template>
865
+
866
+ <!-- Single: Display value -->
867
+ <template v-else-if="hasValue && !multiple">
868
+ <slot name="selected" :option="selectedOptions[0]">
869
+ <img
870
+ v-if="selectedOptions[0]?.image"
871
+ :src="selectedOptions[0].image"
872
+ :alt="selectedOptions[0].label"
873
+ class="w-5 h-5 rounded-full object-cover shrink-0"
874
+ />
875
+ <span v-else-if="selectedOptions[0]?.icon" :class="['mdi', `mdi-${selectedOptions[0].icon}`, 'text-muted-foreground']" />
876
+ <span class="truncate text-foreground">{{ displayValue }}</span>
877
+ </slot>
878
+ </template>
879
+
880
+ <!-- Placeholder (only show if not using labelPlaceholder) -->
881
+ <span v-else-if="!labelPlaceholder" class="text-muted-foreground truncate">
882
+ {{ placeholder }}
883
+ </span>
884
+
885
+ <!-- Empty space when using labelPlaceholder without value -->
886
+ <span v-else class="opacity-0">{{ placeholder }}</span>
887
+ </div>
888
+
889
+ <!-- Suffix slot -->
890
+ <slot name="suffix" />
891
+
892
+ <!-- Loading spinner -->
893
+ <span
894
+ v-if="loading"
895
+ class="mdi mdi-loading animate-spin text-muted-foreground"
896
+ :class="sizeConfig.icon"
897
+ />
898
+
899
+ <!-- Clear button -->
900
+ <button
901
+ v-else-if="clearable && hasValue && !disabled"
902
+ type="button"
903
+ class="mdi mdi-close-circle text-muted-foreground hover:text-muted-foreground transition-colors shrink-0"
904
+ :class="sizeConfig.icon"
905
+ @click="clear"
906
+ />
907
+
908
+ <!-- Dropdown arrow -->
909
+ <slot name="arrow" :is-open="isOpen">
910
+ <span
911
+ v-if="!loading"
912
+ class="text-muted-foreground transition-transform duration-200 shrink-0"
913
+ :class="['mdi', `mdi-${arrowIcon}`, sizeConfig.icon, { 'rotate-180': isOpen }]"
914
+ />
915
+ </slot>
916
+ </div>
917
+
918
+ <!-- Error / Hint -->
919
+ <p
920
+ v-if="error"
921
+ class="mt-1.5 text-xs text-red-500 flex items-center gap-1"
922
+ >
923
+ <span class="mdi mdi-alert-circle" />
924
+ {{ error }}
925
+ </p>
926
+ <p
927
+ v-else-if="hint"
928
+ class="mt-1.5 text-xs text-muted-foreground"
929
+ >
930
+ {{ hint }}
931
+ </p>
932
+
933
+ <!-- Dropdown -->
934
+ <Teleport v-if="teleportTarget" :to="teleportTarget" :disabled="!teleportTarget">
935
+ <Transition
936
+ enter-active-class="transition-all duration-200 ease-out"
937
+ :enter-from-class="dropdownPosition.placement === 'top' ? 'opacity-0 translate-y-2' : 'opacity-0 -translate-y-2'"
938
+ enter-to-class="opacity-100 translate-y-0"
939
+ leave-active-class="transition-all duration-150 ease-in"
940
+ leave-from-class="opacity-100 translate-y-0"
941
+ :leave-to-class="dropdownPosition.placement === 'top' ? 'opacity-0 translate-y-2' : 'opacity-0 -translate-y-2'"
942
+ >
943
+ <div
944
+ v-if="isOpen"
945
+ ref="dropdownRef"
946
+ role="listbox"
947
+ :aria-multiselectable="multiple"
948
+ class="s-select-dropdown fixed z-[100] overflow-hidden border border-border shadow-xl"
949
+ :class="dropdownRoundedConfig"
950
+ :style="{
951
+ top: dropdownPosition.top ? `${dropdownPosition.top}px` : 'auto',
952
+ bottom: dropdownPosition.bottom ? `${dropdownPosition.bottom}px` : 'auto',
953
+ left: `${dropdownPosition.left}px`,
954
+ width: menuWidth ? (typeof menuWidth === 'number' ? `${menuWidth}px` : menuWidth) : `${dropdownPosition.width}px`,
955
+ maxHeight: maxHeight
956
+ }"
957
+ >
958
+ <!-- Glassmorphism background -->
959
+ <div class="absolute inset-0 bg-background/95 backdrop-blur-xl" />
960
+
961
+ <!-- Content -->
962
+ <div class="relative">
963
+ <!-- Search input -->
964
+ <div v-if="searchable" class="p-2 border-b border-border">
965
+ <div class="relative">
966
+ <span class="absolute left-3 top-1/2 -translate-y-1/2 mdi mdi-magnify text-muted-foreground" />
967
+ <input
968
+ ref="searchInputRef"
969
+ type="text"
970
+ :value="searchQuery"
971
+ placeholder="Search..."
972
+ class="w-full pl-9 pr-3 py-2 text-sm bg-muted border border-border rounded-lg outline-none focus:border-primary focus:ring-2 focus:ring-ring/20 transition-all text-foreground placeholder:text-muted-foreground"
973
+ @input="handleSearchInput"
974
+ @keydown="handleKeydown"
975
+ />
976
+ </div>
977
+ </div>
978
+
979
+ <!-- Header slot -->
980
+ <slot name="header" />
981
+
982
+ <!-- Options list -->
983
+ <div
984
+ class="overflow-y-auto overscroll-contain py-0.5"
985
+ :style="{ maxHeight: searchable ? `calc(${maxHeight} - 60px)` : maxHeight }"
986
+ >
987
+ <!-- Using options prop -->
988
+ <template v-if="options.length > 0">
989
+ <template v-if="filteredOptions.length > 0 || showCreateOption">
990
+ <!-- Grouped options rendering -->
991
+ <template v-if="hasGroups">
992
+ <template v-for="[groupName, groupOptions] in groupedOptions" :key="groupName ?? 'ungrouped'">
993
+ <!-- Group header -->
994
+ <div
995
+ v-if="groupName"
996
+ class="flex items-center gap-2 px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground sticky top-0 bg-background/95 backdrop-blur-sm mt-2 first:mt-0 border-t border-border first:border-t-0 z-20"
997
+ >
998
+ {{ groupName }}
999
+ </div>
1000
+ <!-- Group options -->
1001
+ <div
1002
+ v-for="option in groupOptions"
1003
+ :key="option.value"
1004
+ role="option"
1005
+ :aria-selected="isSelected(option.value)"
1006
+ :aria-disabled="option.disabled"
1007
+ class="s-option relative flex items-center cursor-pointer transition-all duration-150 select-none"
1008
+ :class="[
1009
+ sizeConfig.option,
1010
+ {
1011
+ 'opacity-50 cursor-not-allowed': option.disabled,
1012
+ 'text-foreground': isSelected(option.value),
1013
+ 'text-muted-foreground hover:text-foreground hover:bg-accent/50': !isSelected(option.value) && !option.disabled
1014
+ }
1015
+ ]"
1016
+ @click="!option.disabled && selectOption(option.value)"
1017
+ >
1018
+ <!-- Highlight background for selected -->
1019
+ <div
1020
+ v-if="isSelected(option.value)"
1021
+ class="absolute inset-0 transition-all duration-150 rounded-lg mx-1"
1022
+ :style="{ backgroundColor: `color-mix(in srgb, ${option.color ?? resolvedColor} 15%, transparent)` }"
1023
+ />
1024
+
1025
+ <!-- Image -->
1026
+ <img
1027
+ v-if="option.image"
1028
+ :src="option.image"
1029
+ :alt="option.label"
1030
+ class="relative z-10 w-6 h-6 rounded-full object-cover shrink-0 mr-2.5"
1031
+ />
1032
+ <!-- Icon -->
1033
+ <span
1034
+ v-else-if="option.icon"
1035
+ class="relative z-10 shrink-0 mr-2.5"
1036
+ :class="['mdi', `mdi-${option.icon}`, sizeConfig.icon]"
1037
+ :style="isSelected(option.value) ? { color: option.color ?? resolvedColor } : {}"
1038
+ />
1039
+
1040
+ <!-- Content -->
1041
+ <div class="relative z-10 flex-1 min-w-0">
1042
+ <slot name="option" :option="option" :selected="isSelected(option.value)">
1043
+ <span class="truncate block">{{ option.label ?? option.value }}</span>
1044
+ <p v-if="option.description" class="text-xs text-muted-foreground truncate mt-0.5">
1045
+ {{ option.description }}
1046
+ </p>
1047
+ </slot>
1048
+ </div>
1049
+
1050
+ <!-- Check mark -->
1051
+ <span
1052
+ v-if="isSelected(option.value)"
1053
+ class="relative z-10 mdi mdi-check shrink-0 ml-2"
1054
+ :class="sizeConfig.icon"
1055
+ :style="{ color: option.color ?? resolvedColor }"
1056
+ />
1057
+ </div>
1058
+ </template>
1059
+ </template>
1060
+
1061
+ <!-- Non-grouped options (flat list) -->
1062
+ <template v-else>
1063
+ <div
1064
+ v-for="(option, index) in filteredOptions"
1065
+ :key="option.value"
1066
+ role="option"
1067
+ :aria-selected="isSelected(option.value)"
1068
+ :aria-disabled="option.disabled"
1069
+ class="s-option relative flex items-center cursor-pointer transition-all duration-150 select-none"
1070
+ :class="[
1071
+ sizeConfig.option,
1072
+ {
1073
+ 'opacity-50 cursor-not-allowed': option.disabled,
1074
+ 'text-foreground': highlightedIndex === index || isSelected(option.value),
1075
+ 'text-muted-foreground hover:text-foreground': highlightedIndex !== index && !isSelected(option.value) && !option.disabled
1076
+ }
1077
+ ]"
1078
+ @click="!option.disabled && selectOption(option.value)"
1079
+ @mouseenter="highlightedIndex = index"
1080
+ >
1081
+ <!-- Highlight background -->
1082
+ <div
1083
+ v-if="highlightedIndex === index || isSelected(option.value)"
1084
+ class="absolute inset-0 transition-all duration-150 rounded-lg mx-1"
1085
+ :class="isSelected(option.value) ? 'opacity-100' : 'opacity-60'"
1086
+ :style="{
1087
+ backgroundColor: isSelected(option.value)
1088
+ ? `color-mix(in srgb, ${option.color ?? resolvedColor} 15%, transparent)`
1089
+ : 'var(--s-accent)'
1090
+ }"
1091
+ />
1092
+
1093
+ <!-- Image -->
1094
+ <img
1095
+ v-if="option.image"
1096
+ :src="option.image"
1097
+ :alt="option.label"
1098
+ class="relative z-10 w-6 h-6 rounded-full object-cover shrink-0 mr-2.5"
1099
+ />
1100
+ <!-- Icon -->
1101
+ <span
1102
+ v-else-if="option.icon"
1103
+ class="relative z-10 shrink-0 mr-2.5"
1104
+ :class="['mdi', `mdi-${option.icon}`, sizeConfig.icon]"
1105
+ :style="isSelected(option.value) ? { color: option.color ?? resolvedColor } : {}"
1106
+ />
1107
+
1108
+ <!-- Content -->
1109
+ <div class="relative z-10 flex-1 min-w-0">
1110
+ <slot name="option" :option="option" :selected="isSelected(option.value)" :highlighted="highlightedIndex === index">
1111
+ <span class="truncate block">{{ option.label ?? option.value }}</span>
1112
+ <p v-if="option.description" class="text-xs text-muted-foreground truncate mt-0.5">
1113
+ {{ option.description }}
1114
+ </p>
1115
+ </slot>
1116
+ </div>
1117
+
1118
+ <!-- Check mark -->
1119
+ <Transition
1120
+ enter-active-class="transition-all duration-150 ease-out"
1121
+ enter-from-class="scale-0 opacity-0"
1122
+ enter-to-class="scale-100 opacity-100"
1123
+ leave-active-class="transition-all duration-100 ease-in"
1124
+ leave-from-class="scale-100 opacity-100"
1125
+ leave-to-class="scale-0 opacity-0"
1126
+ >
1127
+ <span
1128
+ v-if="isSelected(option.value)"
1129
+ class="relative z-10 mdi mdi-check shrink-0 ml-2"
1130
+ :class="sizeConfig.icon"
1131
+ :style="{ color: option.color ?? resolvedColor }"
1132
+ />
1133
+ </Transition>
1134
+ </div>
1135
+ </template>
1136
+
1137
+ <!-- Creatable option -->
1138
+ <div
1139
+ v-if="showCreateOption"
1140
+ class="s-option relative flex items-center cursor-pointer transition-all duration-150 select-none border-t border-border mt-1 pt-1"
1141
+ :class="sizeConfig.option"
1142
+ @click="createOption"
1143
+ >
1144
+ <span class="mdi mdi-plus-circle mr-2.5" :class="sizeConfig.icon" :style="{ color: resolvedColor }" />
1145
+ <span class="text-muted-foreground">{{ createOptionLabel }}</span>
1146
+ </div>
1147
+ </template>
1148
+
1149
+ <!-- No results -->
1150
+ <div v-else class="px-4 py-8 text-center">
1151
+ <slot name="empty">
1152
+ <span class="mdi mdi-magnify-close text-3xl text-muted-foreground mb-2 block" />
1153
+ <p class="text-sm text-muted-foreground">{{ noResultsText }}</p>
1154
+ </slot>
1155
+ </div>
1156
+ </template>
1157
+
1158
+ <!-- Using slots (SOption children) -->
1159
+ <template v-else-if="$slots.default">
1160
+ <slot />
1161
+ </template>
1162
+
1163
+ <!-- No options -->
1164
+ <div v-else class="px-4 py-8 text-center">
1165
+ <slot name="empty">
1166
+ <span class="mdi mdi-selection-off text-3xl text-muted-foreground mb-2 block" />
1167
+ <p class="text-sm text-muted-foreground">{{ noOptionsText }}</p>
1168
+ </slot>
1169
+ </div>
1170
+
1171
+ <!-- Loading state -->
1172
+ <div v-if="loading" class="px-4 py-8 text-center">
1173
+ <slot name="loading">
1174
+ <span class="mdi mdi-loading animate-spin text-2xl mb-2 block" :style="{ color: resolvedColor }" />
1175
+ <p class="text-sm text-muted-foreground">Loading...</p>
1176
+ </slot>
1177
+ </div>
1178
+ </div>
1179
+
1180
+ <!-- Footer slot -->
1181
+ <slot name="footer" />
1182
+ </div>
1183
+ </div>
1184
+ </Transition>
1185
+ </Teleport>
1186
+
1187
+ <!-- Non-teleported dropdown (fallback) -->
1188
+ <template v-else>
1189
+ <Transition
1190
+ enter-active-class="transition-all duration-200 ease-out"
1191
+ enter-from-class="opacity-0 -translate-y-2"
1192
+ enter-to-class="opacity-100 translate-y-0"
1193
+ leave-active-class="transition-all duration-150 ease-in"
1194
+ leave-from-class="opacity-100 translate-y-0"
1195
+ leave-to-class="opacity-0 -translate-y-2"
1196
+ >
1197
+ <div
1198
+ v-if="isOpen"
1199
+ ref="dropdownRef"
1200
+ role="listbox"
1201
+ class="s-select-dropdown absolute z-50 w-full mt-1 overflow-hidden border border-border shadow-xl bg-background"
1202
+ :class="roundedConfig"
1203
+ :style="{ maxHeight }"
1204
+ >
1205
+ <div class="overflow-y-auto overscroll-contain py-1" :style="{ maxHeight }">
1206
+ <slot />
1207
+ </div>
1208
+ </div>
1209
+ </Transition>
1210
+ </template>
1211
+ </div>
1212
+ </div>
1213
+ </template>
1214
+
1215
+ <style scoped>
1216
+ .s-select-trigger:focus-visible {
1217
+ outline: none;
1218
+ }
1219
+
1220
+ /* Float label container - add padding top for label space */
1221
+ .s-select-trigger--float-label {
1222
+ margin-top: 0.5rem;
1223
+ }
1224
+
1225
+ /* Float label positioning - starts centered vertically */
1226
+ .s-float-label {
1227
+ transform-origin: left center;
1228
+ will-change: transform, opacity, color;
1229
+ top: 50%;
1230
+ transform: translateY(-50%);
1231
+ }
1232
+
1233
+ /* Float label active state - sits on top of border */
1234
+ .s-float-label--active {
1235
+ font-weight: 500;
1236
+ /* Position on top of the border */
1237
+ top: 0;
1238
+ transform: translateY(-50%);
1239
+ /* Background applied via inline style based on variant */
1240
+ border-radius: 2px;
1241
+ line-height: 1.2;
1242
+ }
1243
+
1244
+ /* Elevation effect on focus/open (Vuesax-style lift) */
1245
+ .s-select-trigger--elevated {
1246
+ transform: translateY(-2px);
1247
+ box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 10px -5px rgba(0, 0, 0, 0.05);
1248
+ }
1249
+
1250
+ /* When elevated with float label, also move label up slightly */
1251
+ .s-select-trigger--elevated.s-select-trigger--float-label .s-float-label--active {
1252
+ transform: translateY(calc(-50% - 2px));
1253
+ }
1254
+
1255
+ /* Smooth transition for trigger */
1256
+ .s-select-trigger {
1257
+ transition: all 0.25s ease;
1258
+ }
1259
+
1260
+ /* Filter input styles */
1261
+ .s-filter-input {
1262
+ caret-color: var(--s-primary);
1263
+ }
1264
+
1265
+ .s-filter-input::selection {
1266
+ background-color: color-mix(in srgb, var(--s-primary) 30%, transparent);
1267
+ }
1268
+
1269
+ .s-select-dropdown {
1270
+ scrollbar-width: thin;
1271
+ scrollbar-color: var(--s-border) transparent;
1272
+ }
1273
+
1274
+ .s-select-dropdown::-webkit-scrollbar {
1275
+ width: 6px;
1276
+ }
1277
+
1278
+ .s-select-dropdown::-webkit-scrollbar-track {
1279
+ background: transparent;
1280
+ }
1281
+
1282
+ .s-select-dropdown::-webkit-scrollbar-thumb {
1283
+ background: var(--s-border);
1284
+ border-radius: 3px;
1285
+ }
1286
+
1287
+ .s-select-dropdown::-webkit-scrollbar-thumb:hover {
1288
+ background: var(--s-input);
1289
+ }
1290
+ </style>