@k3-universe/react-kit 0.0.27 → 0.0.29

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 (422) hide show
  1. package/.storybook/main.ts +1 -1
  2. package/.storybook/preview.ts +18 -10
  3. package/biome.json +10 -0
  4. package/dist/index.js +2319 -1227
  5. package/dist/kit/builder/auth/AuthProvider.d.ts +36 -0
  6. package/dist/kit/builder/auth/AuthProvider.d.ts.map +1 -0
  7. package/dist/kit/builder/auth/adapter.d.ts +14 -0
  8. package/dist/kit/builder/auth/adapter.d.ts.map +1 -0
  9. package/dist/kit/builder/auth/client-adapters.d.ts +149 -0
  10. package/dist/kit/builder/auth/client-adapters.d.ts.map +1 -0
  11. package/dist/kit/builder/auth/components.d.ts +119 -0
  12. package/dist/kit/builder/auth/components.d.ts.map +1 -0
  13. package/dist/kit/builder/auth/hooks.d.ts +158 -0
  14. package/dist/kit/builder/auth/hooks.d.ts.map +1 -0
  15. package/dist/kit/builder/auth/index.d.ts +11 -0
  16. package/dist/kit/builder/auth/index.d.ts.map +1 -0
  17. package/dist/kit/builder/auth/permission-checker.d.ts +31 -0
  18. package/dist/kit/builder/auth/permission-checker.d.ts.map +1 -0
  19. package/dist/kit/builder/auth/storage.d.ts +17 -0
  20. package/dist/kit/builder/auth/storage.d.ts.map +1 -0
  21. package/dist/kit/builder/auth/token-manager.d.ts +9 -0
  22. package/dist/kit/builder/auth/token-manager.d.ts.map +1 -0
  23. package/dist/kit/builder/auth/types.d.ts +183 -0
  24. package/dist/kit/builder/auth/types.d.ts.map +1 -0
  25. package/dist/kit/builder/data-table/components/DataTable.d.ts +2 -1
  26. package/dist/kit/builder/data-table/components/DataTable.d.ts.map +1 -1
  27. package/dist/kit/builder/data-table/components/DataTableColumnHeader.d.ts +2 -2
  28. package/dist/kit/builder/data-table/components/DataTableColumnHeader.d.ts.map +1 -1
  29. package/dist/kit/builder/data-table/components/DataTablePagination.d.ts +2 -1
  30. package/dist/kit/builder/data-table/components/DataTablePagination.d.ts.map +1 -1
  31. package/dist/kit/builder/data-table/components/DataTableViewOptions.d.ts +1 -1
  32. package/dist/kit/builder/data-table/components/DataTableViewOptions.d.ts.map +1 -1
  33. package/dist/kit/builder/data-table/types.d.ts.map +1 -1
  34. package/dist/kit/builder/dialog/index.d.ts +1 -1
  35. package/dist/kit/builder/dialog/index.d.ts.map +1 -1
  36. package/dist/kit/builder/dialog/provider.d.ts +0 -1
  37. package/dist/kit/builder/dialog/provider.d.ts.map +1 -1
  38. package/dist/kit/builder/form/components/FormBuilder.d.ts.map +1 -1
  39. package/dist/kit/builder/form/components/FormBuilderActions.d.ts.map +1 -1
  40. package/dist/kit/builder/form/components/FormBuilderContext.d.ts.map +1 -1
  41. package/dist/kit/builder/form/components/FormBuilderField.d.ts +1 -1
  42. package/dist/kit/builder/form/components/FormBuilderField.d.ts.map +1 -1
  43. package/dist/kit/builder/form/components/fields/ArrayField.d.ts +1 -1
  44. package/dist/kit/builder/form/components/fields/ArrayField.d.ts.map +1 -1
  45. package/dist/kit/builder/form/components/fields/AutocompleteField.d.ts +1 -1
  46. package/dist/kit/builder/form/components/fields/AutocompleteField.d.ts.map +1 -1
  47. package/dist/kit/builder/form/components/fields/CheckboxField.d.ts +1 -1
  48. package/dist/kit/builder/form/components/fields/CheckboxField.d.ts.map +1 -1
  49. package/dist/kit/builder/form/components/fields/DateField.d.ts +1 -1
  50. package/dist/kit/builder/form/components/fields/DateField.d.ts.map +1 -1
  51. package/dist/kit/builder/form/components/fields/DatePickerField.d.ts +1 -1
  52. package/dist/kit/builder/form/components/fields/DatePickerField.d.ts.map +1 -1
  53. package/dist/kit/builder/form/components/fields/DateRangePickerField.d.ts +1 -1
  54. package/dist/kit/builder/form/components/fields/DateRangePickerField.d.ts.map +1 -1
  55. package/dist/kit/builder/form/components/fields/DateTimePickerField.d.ts +1 -1
  56. package/dist/kit/builder/form/components/fields/DateTimePickerField.d.ts.map +1 -1
  57. package/dist/kit/builder/form/components/fields/DateTimeRangePickerField.d.ts +1 -1
  58. package/dist/kit/builder/form/components/fields/DateTimeRangePickerField.d.ts.map +1 -1
  59. package/dist/kit/builder/form/components/fields/FileField.d.ts +1 -1
  60. package/dist/kit/builder/form/components/fields/FileField.d.ts.map +1 -1
  61. package/dist/kit/builder/form/components/fields/MonthPickerField.d.ts +1 -1
  62. package/dist/kit/builder/form/components/fields/MonthPickerField.d.ts.map +1 -1
  63. package/dist/kit/builder/form/components/fields/MonthRangePickerField.d.ts +1 -1
  64. package/dist/kit/builder/form/components/fields/MonthRangePickerField.d.ts.map +1 -1
  65. package/dist/kit/builder/form/components/fields/NumberField.d.ts +1 -1
  66. package/dist/kit/builder/form/components/fields/NumberField.d.ts.map +1 -1
  67. package/dist/kit/builder/form/components/fields/ObjectField.d.ts.map +1 -1
  68. package/dist/kit/builder/form/components/fields/RadioField.d.ts +1 -1
  69. package/dist/kit/builder/form/components/fields/RadioField.d.ts.map +1 -1
  70. package/dist/kit/builder/form/components/fields/SelectField.d.ts +1 -1
  71. package/dist/kit/builder/form/components/fields/SelectField.d.ts.map +1 -1
  72. package/dist/kit/builder/form/components/fields/SwitchField.d.ts +1 -1
  73. package/dist/kit/builder/form/components/fields/SwitchField.d.ts.map +1 -1
  74. package/dist/kit/builder/form/components/fields/TextField.d.ts +1 -1
  75. package/dist/kit/builder/form/components/fields/TextField.d.ts.map +1 -1
  76. package/dist/kit/builder/form/components/fields/TextareaField.d.ts +1 -1
  77. package/dist/kit/builder/form/components/fields/TextareaField.d.ts.map +1 -1
  78. package/dist/kit/builder/form/components/fields/TimePickerField.d.ts +1 -1
  79. package/dist/kit/builder/form/components/fields/TimePickerField.d.ts.map +1 -1
  80. package/dist/kit/builder/form/components/fields/TimeRangePickerField.d.ts +1 -1
  81. package/dist/kit/builder/form/components/fields/TimeRangePickerField.d.ts.map +1 -1
  82. package/dist/kit/builder/form/components/fields/index.d.ts.map +1 -1
  83. package/dist/kit/builder/form/components/fields/types.d.ts.map +1 -1
  84. package/dist/kit/builder/form/components/sectionNodes.d.ts.map +1 -1
  85. package/dist/kit/builder/form/hooks/useFormBuilder.d.ts.map +1 -1
  86. package/dist/kit/builder/form/types.d.ts.map +1 -1
  87. package/dist/kit/builder/form/utils/section-factories.d.ts.map +1 -1
  88. package/dist/kit/builder/page/Page.d.ts.map +1 -1
  89. package/dist/kit/builder/page/index.d.ts.map +1 -1
  90. package/dist/kit/builder/section/SectionBuilder.d.ts +1 -1
  91. package/dist/kit/builder/section/SectionBuilder.d.ts.map +1 -1
  92. package/dist/kit/builder/section/SectionContainer.d.ts +14 -0
  93. package/dist/kit/builder/section/SectionContainer.d.ts.map +1 -0
  94. package/dist/kit/builder/stack-dialog/context.d.ts.map +1 -1
  95. package/dist/kit/builder/stack-dialog/hooks.d.ts.map +1 -1
  96. package/dist/kit/builder/stack-dialog/index.d.ts +3 -3
  97. package/dist/kit/builder/stack-dialog/index.d.ts.map +1 -1
  98. package/dist/kit/builder/stack-dialog/provider.d.ts.map +1 -1
  99. package/dist/kit/builder/stack-dialog/renderer.d.ts.map +1 -1
  100. package/dist/kit/builder/stack-dialog/types.d.ts +1 -1
  101. package/dist/kit/builder/stack-dialog/types.d.ts.map +1 -1
  102. package/dist/kit/components/autocomplete/Autocomplete.d.ts +1 -1
  103. package/dist/kit/components/autocomplete/Autocomplete.d.ts.map +1 -1
  104. package/dist/kit/components/autocomplete/types.d.ts.map +1 -1
  105. package/dist/kit/components/datepicker/DatePicker.d.ts.map +1 -1
  106. package/dist/kit/components/datepicker/DateRangePicker.d.ts.map +1 -1
  107. package/dist/kit/components/datetimepicker/DateTimePicker.d.ts.map +1 -1
  108. package/dist/kit/components/datetimepicker/DateTimeRangePicker.d.ts.map +1 -1
  109. package/dist/kit/components/datetimepicker/index.d.ts.map +1 -1
  110. package/dist/kit/components/fileuploader/FileUploader.d.ts.map +1 -1
  111. package/dist/kit/components/fileuploader/types.d.ts +2 -2
  112. package/dist/kit/components/forminfo/FormInfoError.d.ts.map +1 -1
  113. package/dist/kit/components/login/Login.d.ts +1 -1
  114. package/dist/kit/components/login/Login.d.ts.map +1 -1
  115. package/dist/kit/components/monthpicker/MonthInput.d.ts.map +1 -1
  116. package/dist/kit/components/monthpicker/MonthPicker.d.ts.map +1 -1
  117. package/dist/kit/components/monthpicker/MonthRangeInput.d.ts.map +1 -1
  118. package/dist/kit/components/monthpicker/MonthRangePicker.d.ts.map +1 -1
  119. package/dist/kit/components/timepicker/TimePicker.d.ts.map +1 -1
  120. package/dist/kit/components/timepicker/TimeRangePicker.d.ts.map +1 -1
  121. package/dist/kit/components/timepicker/index.d.ts.map +1 -1
  122. package/dist/kit/layouts/admin/components/AdminLayout.d.ts.map +1 -1
  123. package/dist/kit/layouts/admin/components/ThemeToggle.d.ts.map +1 -1
  124. package/dist/kit/layouts/admin/hooks/menu.d.ts.map +1 -1
  125. package/dist/kit/layouts/admin/providers/AdminMenuProvider.d.ts +1 -1
  126. package/dist/kit/layouts/admin/providers/AdminMenuProvider.d.ts.map +1 -1
  127. package/dist/shadcn/hooks/use-mobile.d.ts.map +1 -1
  128. package/dist/shadcn/ui/accordion.d.ts +2 -2
  129. package/dist/shadcn/ui/accordion.d.ts.map +1 -1
  130. package/dist/shadcn/ui/alert-dialog.d.ts +4 -4
  131. package/dist/shadcn/ui/alert-dialog.d.ts.map +1 -1
  132. package/dist/shadcn/ui/alert.d.ts +4 -4
  133. package/dist/shadcn/ui/alert.d.ts.map +1 -1
  134. package/dist/shadcn/ui/aspect-ratio.d.ts +1 -1
  135. package/dist/shadcn/ui/aspect-ratio.d.ts.map +1 -1
  136. package/dist/shadcn/ui/avatar.d.ts +2 -2
  137. package/dist/shadcn/ui/avatar.d.ts.map +1 -1
  138. package/dist/shadcn/ui/badge.d.ts +2 -2
  139. package/dist/shadcn/ui/badge.d.ts.map +1 -1
  140. package/dist/shadcn/ui/breadcrumb.d.ts +8 -8
  141. package/dist/shadcn/ui/breadcrumb.d.ts.map +1 -1
  142. package/dist/shadcn/ui/button.d.ts +2 -2
  143. package/dist/shadcn/ui/button.d.ts.map +1 -1
  144. package/dist/shadcn/ui/calendar.d.ts +2 -2
  145. package/dist/shadcn/ui/calendar.d.ts.map +1 -1
  146. package/dist/shadcn/ui/card.d.ts +8 -8
  147. package/dist/shadcn/ui/card.d.ts.map +1 -1
  148. package/dist/shadcn/ui/carousel.d.ts +5 -5
  149. package/dist/shadcn/ui/carousel.d.ts.map +1 -1
  150. package/dist/shadcn/ui/chart.d.ts +7 -7
  151. package/dist/shadcn/ui/chart.d.ts.map +1 -1
  152. package/dist/shadcn/ui/checkbox.d.ts +2 -2
  153. package/dist/shadcn/ui/checkbox.d.ts.map +1 -1
  154. package/dist/shadcn/ui/collapsible.d.ts +1 -1
  155. package/dist/shadcn/ui/collapsible.d.ts.map +1 -1
  156. package/dist/shadcn/ui/command.d.ts +2 -2
  157. package/dist/shadcn/ui/command.d.ts.map +1 -1
  158. package/dist/shadcn/ui/context-menu.d.ts +4 -4
  159. package/dist/shadcn/ui/context-menu.d.ts.map +1 -1
  160. package/dist/shadcn/ui/dialog.d.ts +4 -4
  161. package/dist/shadcn/ui/dialog.d.ts.map +1 -1
  162. package/dist/shadcn/ui/drawer.d.ts +3 -3
  163. package/dist/shadcn/ui/drawer.d.ts.map +1 -1
  164. package/dist/shadcn/ui/dropdown-menu.d.ts +4 -4
  165. package/dist/shadcn/ui/dropdown-menu.d.ts.map +1 -1
  166. package/dist/shadcn/ui/form.d.ts +5 -5
  167. package/dist/shadcn/ui/form.d.ts.map +1 -1
  168. package/dist/shadcn/ui/hover-card.d.ts +2 -2
  169. package/dist/shadcn/ui/hover-card.d.ts.map +1 -1
  170. package/dist/shadcn/ui/input-otp.d.ts +4 -4
  171. package/dist/shadcn/ui/input-otp.d.ts.map +1 -1
  172. package/dist/shadcn/ui/input.d.ts +2 -2
  173. package/dist/shadcn/ui/input.d.ts.map +1 -1
  174. package/dist/shadcn/ui/label.d.ts +2 -2
  175. package/dist/shadcn/ui/label.d.ts.map +1 -1
  176. package/dist/shadcn/ui/menubar.d.ts +4 -4
  177. package/dist/shadcn/ui/menubar.d.ts.map +1 -1
  178. package/dist/shadcn/ui/navigation-menu.d.ts +2 -2
  179. package/dist/shadcn/ui/navigation-menu.d.ts.map +1 -1
  180. package/dist/shadcn/ui/pagination.d.ts +6 -6
  181. package/dist/shadcn/ui/pagination.d.ts.map +1 -1
  182. package/dist/shadcn/ui/popover.d.ts +2 -2
  183. package/dist/shadcn/ui/popover.d.ts.map +1 -1
  184. package/dist/shadcn/ui/progress.d.ts +2 -2
  185. package/dist/shadcn/ui/progress.d.ts.map +1 -1
  186. package/dist/shadcn/ui/radio-group.d.ts +2 -2
  187. package/dist/shadcn/ui/radio-group.d.ts.map +1 -1
  188. package/dist/shadcn/ui/resizable.d.ts +2 -2
  189. package/dist/shadcn/ui/resizable.d.ts.map +1 -1
  190. package/dist/shadcn/ui/scroll-area.d.ts +2 -2
  191. package/dist/shadcn/ui/scroll-area.d.ts.map +1 -1
  192. package/dist/shadcn/ui/select.d.ts +3 -3
  193. package/dist/shadcn/ui/select.d.ts.map +1 -1
  194. package/dist/shadcn/ui/separator.d.ts +2 -2
  195. package/dist/shadcn/ui/separator.d.ts.map +1 -1
  196. package/dist/shadcn/ui/sheet.d.ts +5 -5
  197. package/dist/shadcn/ui/sheet.d.ts.map +1 -1
  198. package/dist/shadcn/ui/sidebar.d.ts +26 -26
  199. package/dist/shadcn/ui/sidebar.d.ts.map +1 -1
  200. package/dist/shadcn/ui/skeleton.d.ts +1 -1
  201. package/dist/shadcn/ui/skeleton.d.ts.map +1 -1
  202. package/dist/shadcn/ui/slider.d.ts +2 -2
  203. package/dist/shadcn/ui/slider.d.ts.map +1 -1
  204. package/dist/shadcn/ui/sonner.d.ts.map +1 -1
  205. package/dist/shadcn/ui/switch.d.ts +2 -2
  206. package/dist/shadcn/ui/switch.d.ts.map +1 -1
  207. package/dist/shadcn/ui/table.d.ts +9 -9
  208. package/dist/shadcn/ui/table.d.ts.map +1 -1
  209. package/dist/shadcn/ui/tabs.d.ts +2 -2
  210. package/dist/shadcn/ui/tabs.d.ts.map +1 -1
  211. package/dist/shadcn/ui/textarea.d.ts +2 -2
  212. package/dist/shadcn/ui/textarea.d.ts.map +1 -1
  213. package/dist/shadcn/ui/toggle-group.d.ts +2 -2
  214. package/dist/shadcn/ui/toggle-group.d.ts.map +1 -1
  215. package/dist/shadcn/ui/toggle.d.ts +2 -2
  216. package/dist/shadcn/ui/toggle.d.ts.map +1 -1
  217. package/dist/shadcn/ui/tooltip.d.ts +2 -2
  218. package/dist/shadcn/ui/tooltip.d.ts.map +1 -1
  219. package/package.json +2 -2
  220. package/src/index.ts +1 -1
  221. package/src/kit/builder/auth/AuthProvider.tsx +131 -0
  222. package/src/kit/builder/auth/adapter.ts +436 -0
  223. package/src/kit/builder/auth/client-adapters.ts +398 -0
  224. package/src/kit/builder/auth/components.tsx +221 -0
  225. package/src/kit/builder/auth/hooks.ts +237 -0
  226. package/src/kit/builder/auth/index.ts +134 -0
  227. package/src/kit/builder/auth/permission-checker.ts +150 -0
  228. package/src/kit/builder/auth/storage.ts +366 -0
  229. package/src/kit/builder/auth/token-manager.ts +55 -0
  230. package/src/kit/builder/auth/types.ts +393 -0
  231. package/src/kit/builder/data-table/components/DataTable.tsx +216 -82
  232. package/src/kit/builder/data-table/components/DataTableColumnHeader.tsx +9 -5
  233. package/src/kit/builder/data-table/components/DataTablePagination.tsx +49 -27
  234. package/src/kit/builder/data-table/components/DataTableViewOptions.tsx +13 -4
  235. package/src/kit/builder/data-table/types.ts +18 -3
  236. package/src/kit/builder/dialog/index.ts +5 -1
  237. package/src/kit/builder/dialog/provider.tsx +56 -27
  238. package/src/kit/builder/form/components/FormBuilder.tsx +10 -14
  239. package/src/kit/builder/form/components/FormBuilderActions.tsx +1 -1
  240. package/src/kit/builder/form/components/FormBuilderContext.tsx +13 -6
  241. package/src/kit/builder/form/components/FormBuilderField.tsx +70 -20
  242. package/src/kit/builder/form/components/fields/ArrayField.tsx +148 -62
  243. package/src/kit/builder/form/components/fields/AutocompleteField.tsx +53 -18
  244. package/src/kit/builder/form/components/fields/CheckboxField.tsx +20 -11
  245. package/src/kit/builder/form/components/fields/DateField.tsx +17 -6
  246. package/src/kit/builder/form/components/fields/DatePickerField.tsx +15 -10
  247. package/src/kit/builder/form/components/fields/DateRangePickerField.tsx +20 -15
  248. package/src/kit/builder/form/components/fields/DateTimePickerField.tsx +16 -11
  249. package/src/kit/builder/form/components/fields/DateTimeRangePickerField.tsx +23 -17
  250. package/src/kit/builder/form/components/fields/FileField.tsx +10 -5
  251. package/src/kit/builder/form/components/fields/MonthPickerField.tsx +18 -11
  252. package/src/kit/builder/form/components/fields/MonthRangePickerField.tsx +23 -17
  253. package/src/kit/builder/form/components/fields/NumberField.tsx +9 -4
  254. package/src/kit/builder/form/components/fields/ObjectField.tsx +12 -7
  255. package/src/kit/builder/form/components/fields/RadioField.tsx +32 -14
  256. package/src/kit/builder/form/components/fields/SelectField.tsx +26 -11
  257. package/src/kit/builder/form/components/fields/SwitchField.tsx +20 -11
  258. package/src/kit/builder/form/components/fields/TextField.tsx +11 -5
  259. package/src/kit/builder/form/components/fields/TextareaField.tsx +9 -4
  260. package/src/kit/builder/form/components/fields/TimePickerField.tsx +16 -11
  261. package/src/kit/builder/form/components/fields/TimeRangePickerField.tsx +23 -17
  262. package/src/kit/builder/form/components/fields/index.ts +21 -21
  263. package/src/kit/builder/form/components/fields/types.ts +15 -11
  264. package/src/kit/builder/form/components/sectionNodes.tsx +63 -40
  265. package/src/kit/builder/form/hooks/useFormBuilder.ts +83 -34
  266. package/src/kit/builder/form/types.ts +173 -148
  267. package/src/kit/builder/form/utils/section-factories.ts +4 -1
  268. package/src/kit/builder/form/utils/transformers.ts +4 -4
  269. package/src/kit/builder/form/utils/validations.ts +1 -1
  270. package/src/kit/builder/page/Page.tsx +26 -6
  271. package/src/kit/builder/page/index.ts +1 -1
  272. package/src/kit/builder/section/SectionBuilder.tsx +252 -127
  273. package/src/kit/builder/section/SectionContainer.tsx +85 -0
  274. package/src/kit/builder/stack-dialog/context.ts +10 -4
  275. package/src/kit/builder/stack-dialog/hooks.ts +4 -3
  276. package/src/kit/builder/stack-dialog/index.ts +5 -11
  277. package/src/kit/builder/stack-dialog/provider.tsx +11 -11
  278. package/src/kit/builder/stack-dialog/renderer.tsx +23 -26
  279. package/src/kit/builder/stack-dialog/types.ts +18 -18
  280. package/src/kit/components/autocomplete/Autocomplete.tsx +631 -549
  281. package/src/kit/components/autocomplete/types.ts +17 -17
  282. package/src/kit/components/datepicker/DatePicker.tsx +33 -9
  283. package/src/kit/components/datepicker/DateRangePicker.tsx +159 -87
  284. package/src/kit/components/datetimepicker/DateTimePicker.tsx +136 -30
  285. package/src/kit/components/datetimepicker/DateTimeRangePicker.tsx +257 -67
  286. package/src/kit/components/datetimepicker/index.ts +3 -3
  287. package/src/kit/components/fileuploader/FileUploader.tsx +315 -180
  288. package/src/kit/components/fileuploader/index.ts +3 -3
  289. package/src/kit/components/fileuploader/types.ts +3 -3
  290. package/src/kit/components/forminfo/FormInfoError.tsx +26 -11
  291. package/src/kit/components/login/Login.tsx +13 -4
  292. package/src/kit/components/monthpicker/MonthInput.tsx +13 -4
  293. package/src/kit/components/monthpicker/MonthPicker.tsx +12 -11
  294. package/src/kit/components/monthpicker/MonthRangeInput.tsx +29 -8
  295. package/src/kit/components/monthpicker/MonthRangePicker.tsx +23 -21
  296. package/src/kit/components/timepicker/TimePicker.tsx +19 -11
  297. package/src/kit/components/timepicker/TimeRangePicker.tsx +106 -29
  298. package/src/kit/components/timepicker/index.ts +3 -3
  299. package/src/kit/layouts/admin/components/AdminLayout.tsx +53 -24
  300. package/src/kit/layouts/admin/components/ThemeToggle.tsx +3 -9
  301. package/src/kit/layouts/admin/hooks/menu.ts +11 -5
  302. package/src/kit/layouts/admin/providers/AdminMenuProvider.tsx +59 -39
  303. package/src/kit/layouts/admin/types/index.ts +1 -1
  304. package/src/kit/themes/base.css +1 -1
  305. package/src/kit/themes/clean-slate.css +40 -32
  306. package/src/kit/themes/default.css +34 -24
  307. package/src/kit/themes/minimal-modern.css +37 -29
  308. package/src/kit/themes/spotify.css +56 -39
  309. package/src/shadcn/hooks/use-mobile.ts +13 -11
  310. package/src/shadcn/lib/utils.ts +2 -2
  311. package/src/shadcn/ui/accordion.tsx +14 -14
  312. package/src/shadcn/ui/alert-dialog.tsx +29 -29
  313. package/src/shadcn/ui/alert.tsx +20 -20
  314. package/src/shadcn/ui/aspect-ratio.tsx +4 -4
  315. package/src/shadcn/ui/avatar.tsx +13 -13
  316. package/src/shadcn/ui/badge.tsx +16 -16
  317. package/src/shadcn/ui/breadcrumb.tsx +28 -28
  318. package/src/shadcn/ui/button.tsx +23 -23
  319. package/src/shadcn/ui/calendar.tsx +82 -78
  320. package/src/shadcn/ui/card.tsx +27 -27
  321. package/src/shadcn/ui/carousel.tsx +93 -93
  322. package/src/shadcn/ui/chart.tsx +103 -103
  323. package/src/shadcn/ui/checkbox.tsx +9 -9
  324. package/src/shadcn/ui/collapsible.tsx +6 -6
  325. package/src/shadcn/ui/command.tsx +36 -36
  326. package/src/shadcn/ui/context-menu.tsx +40 -40
  327. package/src/shadcn/ui/dialog.tsx +28 -28
  328. package/src/shadcn/ui/drawer.tsx +30 -30
  329. package/src/shadcn/ui/dropdown-menu.tsx +41 -41
  330. package/src/shadcn/ui/form.tsx +48 -47
  331. package/src/shadcn/ui/hover-card.tsx +11 -11
  332. package/src/shadcn/ui/input-otp.tsx +23 -23
  333. package/src/shadcn/ui/input.tsx +9 -9
  334. package/src/shadcn/ui/label.tsx +8 -8
  335. package/src/shadcn/ui/menubar.tsx +47 -47
  336. package/src/shadcn/ui/navigation-menu.tsx +33 -33
  337. package/src/shadcn/ui/pagination.tsx +28 -28
  338. package/src/shadcn/ui/popover.tsx +12 -12
  339. package/src/shadcn/ui/progress.tsx +8 -8
  340. package/src/shadcn/ui/radio-group.tsx +11 -11
  341. package/src/shadcn/ui/resizable.tsx +14 -14
  342. package/src/shadcn/ui/scroll-area.tsx +15 -15
  343. package/src/shadcn/ui/select.tsx +34 -34
  344. package/src/shadcn/ui/separator.tsx +9 -9
  345. package/src/shadcn/ui/sheet.tsx +36 -36
  346. package/src/shadcn/ui/sidebar.tsx +227 -227
  347. package/src/shadcn/ui/skeleton.tsx +5 -5
  348. package/src/shadcn/ui/slider.tsx +12 -12
  349. package/src/shadcn/ui/sonner.tsx +11 -11
  350. package/src/shadcn/ui/switch.tsx +9 -9
  351. package/src/shadcn/ui/table.tsx +32 -32
  352. package/src/shadcn/ui/tabs.tsx +14 -14
  353. package/src/shadcn/ui/textarea.tsx +7 -7
  354. package/src/shadcn/ui/toggle-group.tsx +17 -17
  355. package/src/shadcn/ui/toggle.tsx +16 -16
  356. package/src/shadcn/ui/tooltip.tsx +11 -11
  357. package/src/stories/FileUploader.stories.tsx +23 -4
  358. package/src/stories/kit/builder/DataTable.Basic.stories.tsx +14 -4
  359. package/src/stories/kit/builder/DataTable.Filters.stories.tsx +36 -14
  360. package/src/stories/kit/builder/DataTable.Pagination.stories.tsx +3 -2
  361. package/src/stories/kit/builder/DataTable.SelectionAndActions.stories.tsx +18 -4
  362. package/src/stories/kit/builder/DataTable.Sorting.stories.tsx +18 -7
  363. package/src/stories/kit/builder/Dialog.stories.tsx +19 -13
  364. package/src/stories/kit/builder/Form.ArrayLayouts.stories.tsx +40 -16
  365. package/src/stories/kit/builder/Form.Autocomplete.stories.tsx +34 -22
  366. package/src/stories/kit/builder/Form.Basic.stories.tsx +38 -6
  367. package/src/stories/kit/builder/Form.Complex.stories.tsx +356 -111
  368. package/src/stories/kit/builder/Form.DateTime.stories.tsx +12 -8
  369. package/src/stories/kit/builder/Form.Dynamic.stories.tsx +695 -132
  370. package/src/stories/kit/builder/Form.Files.stories.tsx +37 -26
  371. package/src/stories/kit/builder/Form.MultipleFormBuilder.stories.tsx +46 -42
  372. package/src/stories/kit/builder/Form.Pickers.stories.tsx +12 -8
  373. package/src/stories/kit/builder/Form.Simple.stories.tsx +15 -6
  374. package/src/stories/kit/builder/Form.Time.stories.tsx +12 -8
  375. package/src/stories/kit/builder/Page.stories.tsx +32 -6
  376. package/src/stories/kit/builder/Section.stories.tsx +58 -11
  377. package/src/stories/kit/components/Autocomplete.stories.tsx +55 -22
  378. package/src/stories/kit/components/DatePicker.stories.tsx +80 -13
  379. package/src/stories/kit/components/DateRangePicker.stories.tsx +52 -11
  380. package/src/stories/kit/components/Login.stories.tsx +8 -2
  381. package/src/stories/kit/components/MonthPicker.stories.tsx +26 -6
  382. package/src/stories/kit/components/MonthRangePicker.stories.tsx +24 -5
  383. package/src/stories/kit/components/TimePicker.stories.tsx +18 -16
  384. package/src/stories/kit/components/TimeRangePicker.stories.tsx +18 -12
  385. package/src/stories/kit/layouts/admin/AdminLayout.Basic.stories.tsx +29 -6
  386. package/src/stories/kit/layouts/admin/AdminLayout.Collapsible.stories.tsx +26 -5
  387. package/src/stories/kit/layouts/admin/AdminLayout.Complex.stories.tsx +101 -18
  388. package/src/stories/kit/layouts/admin/AdminLayout.CustomSidebarHeaderComponent.stories.tsx +18 -4
  389. package/src/stories/kit/layouts/admin/AdminLayout.CustomSidebarTitleAndIcon.stories.tsx +17 -4
  390. package/src/stories/kit/layouts/admin/AdminLayout.HeaderSlots.stories.tsx +28 -6
  391. package/src/stories/shadcn/ui/Accordion.stories.tsx +33 -10
  392. package/src/stories/shadcn/ui/AlertDialog.stories.tsx +3 -1
  393. package/src/stories/shadcn/ui/Button.stories.tsx +3 -1
  394. package/src/stories/shadcn/ui/Calendar.stories.tsx +6 -1
  395. package/src/stories/shadcn/ui/Card.stories.tsx +11 -2
  396. package/src/stories/shadcn/ui/Checkbox.stories.tsx +11 -3
  397. package/src/stories/shadcn/ui/Collapsible.stories.tsx +12 -5
  398. package/src/stories/shadcn/ui/ContextMenu.stories.tsx +12 -4
  399. package/src/stories/shadcn/ui/Dialog.stories.tsx +15 -3
  400. package/src/stories/shadcn/ui/Drawer.stories.tsx +5 -2
  401. package/src/stories/shadcn/ui/DropdownMenu.stories.tsx +15 -5
  402. package/src/stories/shadcn/ui/Form.stories.tsx +5 -2
  403. package/src/stories/shadcn/ui/HoverCard.stories.tsx +8 -2
  404. package/src/stories/shadcn/ui/Input.stories.tsx +3 -1
  405. package/src/stories/shadcn/ui/InputOtp.stories.tsx +9 -2
  406. package/src/stories/shadcn/ui/Menubar.stories.tsx +21 -7
  407. package/src/stories/shadcn/ui/NavigationMenu.stories.tsx +30 -5
  408. package/src/stories/shadcn/ui/Popover.stories.tsx +8 -2
  409. package/src/stories/shadcn/ui/Resizable.stories.tsx +17 -5
  410. package/src/stories/shadcn/ui/ScrollArea.stories.tsx +54 -2
  411. package/src/stories/shadcn/ui/Select.stories.tsx +7 -1
  412. package/src/stories/shadcn/ui/Sheet.stories.tsx +2 -1
  413. package/src/stories/shadcn/ui/Sidebar.stories.tsx +13 -2
  414. package/src/stories/shadcn/ui/Sonner.stories.tsx +12 -2
  415. package/src/stories/shadcn/ui/Table.stories.tsx +86 -27
  416. package/src/stories/shadcn/ui/Tabs.stories.tsx +9 -2
  417. package/src/stories/shadcn/ui/Textarea.stories.tsx +3 -1
  418. package/src/stories/shadcn/ui/Toggle.stories.tsx +10 -2
  419. package/src/stories/shadcn/ui/Tooltip.stories.tsx +6 -1
  420. package/tsconfig.json +1 -5
  421. package/tsconfig.tsbuildinfo +1 -0
  422. package/eslint.config.mjs +0 -19
@@ -1,561 +1,643 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { useCombobox } from "downshift";
3
- import { useDebounce } from "use-debounce";
4
- import { cn } from "../../../shadcn/lib/utils";
5
- import { Popover, PopoverContent, PopoverTrigger } from "../../../shadcn/ui/popover";
6
- import { Badge } from "../../../shadcn/ui/badge";
7
- import { ChevronsUpDown, X, Check, Loader2 } from "lucide-react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useCombobox } from 'downshift';
3
+ import { useDebounce } from 'use-debounce';
4
+ import { cn } from '../../../shadcn/lib/utils';
5
+ import {
6
+ Popover,
7
+ PopoverContent,
8
+ PopoverTrigger,
9
+ } from '../../../shadcn/ui/popover';
10
+ import { Badge } from '../../../shadcn/ui/badge';
11
+ import { ChevronsUpDown, X, Check, Loader2 } from 'lucide-react';
8
12
  import type {
9
- AutocompleteFetcher,
10
- AutocompleteMode,
11
- AutocompleteOption,
12
- AutocompleteFetchResult,
13
- } from "./types";
13
+ AutocompleteFetcher,
14
+ AutocompleteMode,
15
+ AutocompleteOption,
16
+ AutocompleteFetchResult,
17
+ } from './types';
14
18
 
15
19
  export type AutocompleteProps<T = unknown> = {
16
- mode: AutocompleteMode;
17
- options?: AutocompleteOption<T>[];
18
- fetcher?: AutocompleteFetcher<T>;
19
- fetcherFilter?: () => Record<string, string | number | boolean | null>;
20
- pageSize?: number;
21
- value?: string | number | null | Array<string | number>;
22
- onChange?: (
23
- value: string | number | null | Array<string | number>,
24
- selected: AutocompleteOption<T> | AutocompleteOption<T>[] | null,
25
- raw?: T | T[] | null,
26
- ) => void;
27
- placeholder?: string;
28
- disabled?: boolean;
29
- multiple?: boolean;
30
- className?: string;
31
- chipVariant?: "default" | "secondary" | "destructive" | "outline";
32
- chipClassName?: string;
33
- emptyText?: string;
34
- renderOption?: (option: AutocompleteOption<T>, selected: boolean) => React.ReactNode;
35
- defaultOpen?: boolean;
36
- defaultValue?: string | number | null | Array<string | number>;
37
- allowCustomValue?: boolean;
38
- clearable?: boolean;
39
- initialSelectedOptions?: AutocompleteOption<T> | AutocompleteOption<T>[];
40
- loadSelected?: (values: Array<string | number>) => Promise<AutocompleteOption<T>[]>;
20
+ mode: AutocompleteMode;
21
+ options?: AutocompleteOption<T>[];
22
+ fetcher?: AutocompleteFetcher<T>;
23
+ fetcherFilter?: () => Record<string, string | number | boolean | null>;
24
+ pageSize?: number;
25
+ value?: string | number | null | Array<string | number>;
26
+ onChange?: (
27
+ value: string | number | null | Array<string | number>,
28
+ selected: AutocompleteOption<T> | AutocompleteOption<T>[] | null,
29
+ raw?: T | T[] | null,
30
+ ) => void;
31
+ placeholder?: string;
32
+ disabled?: boolean;
33
+ multiple?: boolean;
34
+ className?: string;
35
+ chipVariant?: 'default' | 'secondary' | 'destructive' | 'outline';
36
+ chipClassName?: string;
37
+ emptyText?: string;
38
+ renderOption?: (
39
+ option: AutocompleteOption<T>,
40
+ selected: boolean,
41
+ ) => React.ReactNode;
42
+ defaultOpen?: boolean;
43
+ defaultValue?: string | number | null | Array<string | number>;
44
+ allowCustomValue?: boolean;
45
+ clearable?: boolean;
46
+ initialSelectedOptions?: AutocompleteOption<T> | AutocompleteOption<T>[];
47
+ loadSelected?: (
48
+ values: Array<string | number>,
49
+ ) => Promise<AutocompleteOption<T>[]>;
41
50
  };
42
51
 
43
52
  const DEFAULT_PAGE_SIZE = 50;
44
53
 
45
54
  export function Autocomplete<T = unknown>({
46
- mode = "client",
47
- options = [],
48
- fetcher,
49
- fetcherFilter,
50
- pageSize = DEFAULT_PAGE_SIZE,
51
- value: controlledValue,
52
- onChange,
53
- placeholder = "Search or select...",
54
- disabled = false,
55
- multiple = false,
56
- className,
57
- chipVariant = "secondary",
58
- chipClassName,
59
- emptyText = "No results found",
60
- renderOption,
61
- defaultOpen,
62
- defaultValue,
63
- allowCustomValue = false,
64
- clearable = true,
65
- initialSelectedOptions,
66
- loadSelected,
55
+ mode = 'client',
56
+ options = [],
57
+ fetcher,
58
+ fetcherFilter,
59
+ pageSize = DEFAULT_PAGE_SIZE,
60
+ value: controlledValue,
61
+ onChange,
62
+ placeholder = 'Search or select...',
63
+ disabled = false,
64
+ multiple = false,
65
+ className,
66
+ chipVariant = 'secondary',
67
+ chipClassName,
68
+ emptyText = 'No results found',
69
+ renderOption,
70
+ defaultOpen,
71
+ defaultValue,
72
+ allowCustomValue = false,
73
+ clearable = true,
74
+ initialSelectedOptions,
75
+ loadSelected,
67
76
  }: AutocompleteProps<T>) {
68
- const isMultiple = !!multiple;
69
- const isControlled = controlledValue !== undefined;
70
-
71
- // Internal value state
72
- const [internalValue, setInternalValue] = useState<string | number | null | Array<string | number>>(() => {
73
- if (defaultValue !== undefined) return defaultValue;
74
- return isMultiple ? [] : null;
75
- });
76
-
77
- const currentValue = isControlled ? controlledValue : internalValue;
78
-
79
- // Label and raw data maps
80
- const labelMapRef = useRef<Map<string | number, string>>(new Map());
81
- const rawMapRef = useRef<Map<string | number, T>>(new Map());
82
-
83
- const storeOption = useCallback((opt: AutocompleteOption<T>) => {
84
- labelMapRef.current.set(opt.value, opt.label);
85
- if (opt.raw !== undefined) {
86
- rawMapRef.current.set(opt.value, opt.raw);
87
- }
88
- }, []);
89
-
90
- // Immediately populate labels for initial values (runs once on mount)
91
- if (labelMapRef.current.size === 0 && currentValue) {
92
- const values = Array.isArray(currentValue) ? currentValue : [currentValue];
93
- // First, store initialSelectedOptions if provided
94
- if (initialSelectedOptions) {
95
- const arr = Array.isArray(initialSelectedOptions) ? initialSelectedOptions : [initialSelectedOptions];
96
- arr.forEach(opt => {
97
- labelMapRef.current.set(opt.value, opt.label);
98
- if (opt.raw !== undefined) {
99
- rawMapRef.current.set(opt.value, opt.raw);
100
- }
101
- });
102
- }
103
- // Then look up missing values in options array
104
- values.forEach(val => {
105
- if (!labelMapRef.current.has(val)) {
106
- const option = options.find(opt => opt.value === val);
107
- if (option) {
108
- labelMapRef.current.set(option.value, option.label);
109
- if (option.raw !== undefined) {
110
- rawMapRef.current.set(option.value, option.raw);
111
- }
112
- }
113
- }
114
- });
115
- }
116
-
117
- // Data state
118
- const [items, setItems] = useState<AutocompleteOption<T>[]>([]);
119
- const [loading, setLoading] = useState(false);
120
- const [hasMore, setHasMore] = useState(false);
121
- const [page, setPage] = useState(1);
122
- const [searchInput, setSearchInput] = useState("");
123
- const [debouncedSearch] = useDebounce(searchInput, 300);
124
- const [clearCounter, setClearCounter] = useState(0);
125
-
126
- // Popover state
127
- const [isOpen, setIsOpen] = useState(!!defaultOpen);
128
-
129
- // Clear input in multiple mode after selection
130
- useEffect(() => {
131
- if (clearCounter > 0) {
132
- setSearchInput("");
133
- }
134
- }, [clearCounter]);
135
-
136
- // Load data
137
- const loadData = useCallback(
138
- async (pageNum: number, search: string) => {
139
- if (mode === "server") {
140
- if (!fetcher) return;
141
- setLoading(true);
142
- try {
143
- const res: AutocompleteFetchResult<T> = await fetcher({
144
- search,
145
- moreFilter: fetcherFilter,
146
- cursor: null,
147
- page: pageNum,
148
- pageSize,
149
- });
150
- res.items.forEach(storeOption);
151
- setItems((prev) => (pageNum === 1 ? res.items : [...prev, ...res.items]));
152
- setHasMore(!!res.hasMore);
153
- } catch (_error) {
154
- if (pageNum === 1) setItems([]);
155
- setHasMore(false);
156
- } finally {
157
- setLoading(false);
158
- }
159
- } else {
160
- // Client mode
161
- const filtered = search
162
- ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()))
163
- : options;
164
- options.forEach(storeOption);
165
- const start = (pageNum - 1) * pageSize;
166
- const slice = filtered.slice(start, start + pageSize);
167
- setItems((prev) => (pageNum === 1 ? slice : [...prev, ...slice]));
168
- setHasMore(start + pageSize < filtered.length);
169
- }
170
- },
171
- [mode, fetcher, fetcherFilter, options, pageSize, storeOption],
172
- );
173
-
174
- // Reset and load on search change
175
- useEffect(() => {
176
- if (!isOpen) return;
177
- setPage(1);
178
- loadData(1, debouncedSearch);
179
- // eslint-disable-next-line react-hooks/exhaustive-deps
180
- }, [isOpen, debouncedSearch]);
181
-
182
- // Load more pages
183
- useEffect(() => {
184
- if (!isOpen || page <= 1) return;
185
- loadData(page, debouncedSearch);
186
- // eslint-disable-next-line react-hooks/exhaustive-deps
187
- }, [page]);
188
-
189
- // Store initial/selected options (takes precedence)
190
- useEffect(() => {
191
- if (initialSelectedOptions) {
192
- const arr = Array.isArray(initialSelectedOptions) ? initialSelectedOptions : [initialSelectedOptions];
193
- arr.forEach(storeOption);
194
- }
195
- }, [initialSelectedOptions, storeOption]);
196
-
197
- // Auto-populate missing initial options from provided options array
198
- useEffect(() => {
199
- if (!currentValue || (Array.isArray(currentValue) && currentValue.length === 0)) return;
200
-
201
- const values = Array.isArray(currentValue) ? currentValue : [currentValue];
202
-
203
- // Only look up values that are missing (not in label map)
204
- const missingValues = values.filter(v => !labelMapRef.current.has(v));
205
-
206
- if (missingValues.length === 0) return;
207
-
208
- // Look up missing values in the options array
209
- missingValues.forEach(val => {
210
- const option = options.find(opt => opt.value === val);
211
- if (option) {
212
- storeOption(option);
213
- }
214
- });
215
- }, [currentValue, options, storeOption]);
216
-
217
- // Load selected labels on mount for initial values
218
- const hasLoadedInitial = useRef(false);
219
- const [labelsLoadedCounter, setLabelsLoadedCounter] = useState(0);
220
-
221
- useEffect(() => {
222
- // Only run once on mount
223
- if (!loadSelected || hasLoadedInitial.current) return;
224
- if (!currentValue) return;
225
-
226
- const values = Array.isArray(currentValue) ? currentValue : [currentValue];
227
- if (values.length === 0) return;
228
-
229
- // Check if any values are missing labels
230
- const missing = values.filter((v) => !labelMapRef.current.has(v));
231
- if (missing.length === 0) {
232
- hasLoadedInitial.current = true;
233
- return;
234
- }
235
-
236
- hasLoadedInitial.current = true;
237
- let cancelled = false;
238
- loadSelected(missing).then((opts) => {
239
- if (!cancelled && opts.length > 0) {
240
- opts.forEach(storeOption);
241
- // Only trigger re-render if we actually stored something
242
- setLabelsLoadedCounter(c => c + 1);
243
- }
244
- // eslint-disable-next-line @typescript-eslint/no-empty-function
245
- }).catch(() => {});
246
- return () => {
247
- cancelled = true;
248
- };
249
- // eslint-disable-next-line react-hooks/exhaustive-deps
250
- }, []); // Only run once on mount - uses closure values
251
-
252
- // Load selected labels if missing when dropdown opens
253
- useEffect(() => {
254
- if (!loadSelected || !isOpen) return;
255
- const values = Array.isArray(currentValue) ? currentValue : currentValue ? [currentValue] : [];
256
- const missing = values.filter((v) => !labelMapRef.current.has(v));
257
- if (missing.length === 0) return;
258
-
259
- let cancelled = false;
260
- loadSelected(missing).then((opts) => {
261
- if (!cancelled) opts.forEach(storeOption);
262
- // eslint-disable-next-line @typescript-eslint/no-empty-function
263
- }).catch(() => {});
264
- return () => {
265
- cancelled = true;
266
- };
267
- }, [currentValue, loadSelected, isOpen, storeOption]);
268
-
269
- // Get label helper
270
- const getLabel = useCallback((v: string | number) => {
271
- return labelMapRef.current.get(v) ?? String(v);
272
- }, []);
273
-
274
- // Selected items for display
275
- const selectedItems = useMemo(() => {
276
- if (!isMultiple) {
277
- if (currentValue === null || currentValue === undefined || Array.isArray(currentValue)) return [];
278
- return [{ value: currentValue, label: getLabel(currentValue), raw: rawMapRef.current.get(currentValue) }];
279
- }
280
- const values = Array.isArray(currentValue) ? currentValue : [];
281
- return values.map((v) => ({ value: v, label: getLabel(v), raw: rawMapRef.current.get(v) }));
282
- // eslint-disable-next-line react-hooks/exhaustive-deps
283
- }, [currentValue, isMultiple, getLabel, labelsLoadedCounter]); // labelsLoadedCounter triggers re-compute when loadSelected completes
284
-
285
- // Handle selection
286
- const handleSelect = useCallback(
287
- (item: AutocompleteOption<T> | null) => {
288
- if (!item) return;
289
- storeOption(item);
290
-
291
- if (isMultiple) {
292
- const values = Array.isArray(currentValue) ? currentValue : [];
293
- const exists = values.includes(item.value);
294
-
295
- // Skip if already selected (don't toggle), but still clear the search
296
- if (exists) {
297
- setClearCounter(c => c + 1);
298
- return;
299
- }
300
-
301
- const newValues = [...values, item.value];
302
-
303
- if (!isControlled) setInternalValue(newValues);
304
- const newOptions = newValues.map((v) => ({ value: v, label: getLabel(v), raw: rawMapRef.current.get(v) }));
305
- const raws = newOptions.map((o) => o.raw).filter((r): r is T => r !== undefined);
306
- onChange?.(newValues, newOptions, raws);
307
-
308
- // Trigger input clear
309
- setClearCounter(c => c + 1);
310
- } else {
311
- if (!isControlled) setInternalValue(item.value);
312
- onChange?.(item.value, item, item.raw ?? null);
313
- setIsOpen(false);
314
- }
315
- },
316
- [isMultiple, currentValue, isControlled, onChange, getLabel, storeOption],
317
- );
318
-
319
- // Handle remove chip
320
- const handleRemove = useCallback(
321
- (valueToRemove: string | number) => {
322
- const values = Array.isArray(currentValue) ? currentValue : [];
323
- const newValues = values.filter((v) => v !== valueToRemove);
324
- if (!isControlled) setInternalValue(newValues);
325
- const newOptions = newValues.map((v) => ({ value: v, label: getLabel(v), raw: rawMapRef.current.get(v) }));
326
- const raws = newOptions.map((o) => o.raw).filter((r): r is T => r !== undefined);
327
- onChange?.(newValues, newOptions, raws);
328
- },
329
- [currentValue, isControlled, onChange, getLabel],
330
- );
331
-
332
- // Handle clear
333
- const handleClear = useCallback(() => {
334
- const newValue = isMultiple ? [] : null;
335
- if (!isControlled) setInternalValue(newValue);
336
- onChange?.(newValue, isMultiple ? [] : null, isMultiple ? [] : null);
337
- }, [isMultiple, isControlled, onChange]);
338
-
339
- // Handle custom value creation
340
- const handleCreateCustom = useCallback(() => {
341
- const trimmed = searchInput.trim();
342
- if (!trimmed || !allowCustomValue) return;
343
-
344
- const newOption: AutocompleteOption<T> = { value: trimmed, label: trimmed };
345
- storeOption(newOption);
346
-
347
- if (isMultiple) {
348
- const values = Array.isArray(currentValue) ? currentValue : [];
349
- if (values.includes(trimmed)) return;
350
- const newValues = [...values, trimmed];
351
- if (!isControlled) setInternalValue(newValues);
352
- const newOptions = newValues.map((v) => ({ value: v, label: getLabel(v), raw: rawMapRef.current.get(v) }));
353
- onChange?.(newValues, newOptions, []);
354
- setSearchInput("");
355
- } else {
356
- if (!isControlled) setInternalValue(trimmed);
357
- onChange?.(trimmed, newOption, null);
358
- setSearchInput("");
359
- setIsOpen(false);
360
- }
361
- }, [searchInput, allowCustomValue, isMultiple, currentValue, isControlled, onChange, getLabel, storeOption]);
362
-
363
- // Compute input value based on mode and state
364
- const computedInputValue = useMemo(() => {
365
- // In multiple mode or when dropdown is open, show search input
366
- if (isMultiple || isOpen) {
367
- return searchInput;
368
- }
369
- // In single mode when closed, show selected item label
370
- if (selectedItems.length > 0) {
371
- return selectedItems[0].label;
372
- }
373
- return "";
374
- }, [isMultiple, isOpen, searchInput, selectedItems]);
375
-
376
- // Downshift
377
- const {
378
- getInputProps,
379
- getItemProps,
380
- getMenuProps,
381
- highlightedIndex,
382
- } = useCombobox({
383
- items,
384
- itemToString: (item) => item?.label ?? "",
385
- selectedItem: isMultiple ? null : (selectedItems[0] ?? null),
386
- onSelectedItemChange: ({ selectedItem }) => handleSelect(selectedItem),
387
- isOpen,
388
- onIsOpenChange: ({ isOpen: newIsOpen }) => setIsOpen(newIsOpen ?? false),
389
- inputValue: computedInputValue,
390
- onInputValueChange: ({ inputValue }) => setSearchInput(inputValue ?? ""),
391
- });
392
-
393
- // Refs
394
- const parentRef = useRef<HTMLDivElement>(null);
395
- const inputRef = useRef<HTMLInputElement>(null);
396
-
397
- // Stable ref callback for merging Downshift's ref with our parentRef
398
- const menuRefCallback = useCallback((node: HTMLDivElement | null) => {
399
- parentRef.current = node;
400
- }, []);
401
-
402
- const handleScroll = useCallback(() => {
403
- if (!parentRef.current || !hasMore || loading) return;
404
-
405
- const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
406
- const scrolledToBottom = scrollHeight - scrollTop - clientHeight < 50;
407
-
408
- if (scrolledToBottom) {
409
- setPage((p) => p + 1);
410
- }
411
- }, [hasMore, loading]);
412
-
413
- useEffect(() => {
414
- const element = parentRef.current;
415
- if (!element) return;
416
-
417
- element.addEventListener('scroll', handleScroll);
418
- return () => element.removeEventListener('scroll', handleScroll);
419
- }, [handleScroll]);
420
-
421
- const showClearButton = clearable && (
422
- (isMultiple && selectedItems.length > 0) ||
423
- (!isMultiple && currentValue !== null && currentValue !== undefined && !Array.isArray(currentValue))
424
- );
425
-
426
- return (
427
- <Popover open={isOpen} onOpenChange={setIsOpen}>
428
- <PopoverTrigger asChild>
429
- <div
430
- className={cn(
431
- "flex min-h-10 w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm",
432
- "ring-offset-background",
433
- "focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
434
- disabled && "cursor-not-allowed opacity-50",
435
- className,
436
- )}
437
- >
438
- {isMultiple && (
439
- <div className="flex flex-wrap gap-1">
440
- {selectedItems.map((item) => (
441
- <Badge key={item.value} variant={chipVariant} className={cn("gap-1", chipClassName)}>
442
- <span className="max-w-[150px] truncate">{item.label}</span>
443
- <button
444
- type="button"
445
- onClick={(e) => {
446
- e.stopPropagation();
447
- handleRemove(item.value);
448
- }}
449
- className="rounded-sm opacity-70 hover:opacity-100"
450
- >
451
- <X className="h-3 w-3" />
452
- </button>
453
- </Badge>
454
- ))}
455
- </div>
456
- )}
457
- <input
458
- {...getInputProps({
459
- ref: inputRef,
460
- placeholder,
461
- disabled,
462
- onClick: () => {
463
- if (!isOpen) setIsOpen(true);
464
- },
465
- onKeyDown: (e) => {
466
- if (e.key === "Enter" && allowCustomValue && searchInput.trim() && items.length === 0) {
467
- e.preventDefault();
468
- handleCreateCustom();
469
- }
470
- },
471
- })}
472
- className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground min-w-[120px]"
473
- />
474
- <div className="flex items-center gap-2 shrink-0">
475
- {showClearButton && (
476
- <button
477
- type="button"
478
- onClick={(e) => {
479
- e.stopPropagation();
480
- handleClear();
481
- }}
482
- className="rounded-sm opacity-70 hover:opacity-100"
483
- >
484
- <X className="h-4 w-4" />
485
- </button>
486
- )}
487
- <ChevronsUpDown className="h-4 w-4 opacity-50" />
488
- </div>
489
- </div>
490
- </PopoverTrigger>
491
- <PopoverContent
492
- className="p-0"
493
- style={{ width: "var(--radix-popover-trigger-width)" }}
494
- align="start"
495
- onOpenAutoFocus={(e) => e.preventDefault()}
496
- >
497
- <div
498
- {...getMenuProps({
499
- ref: menuRefCallback
500
- })}
501
- className="max-h-[300px] overflow-auto"
502
- >
503
- {loading && items.length === 0 ? (
504
- <div className="flex items-center justify-center py-6">
505
- <Loader2 className="h-4 w-4 animate-spin mr-2" />
506
- <span className="text-sm text-muted-foreground">Loading...</span>
507
- </div>
508
- ) : items.length === 0 ? (
509
- <div className="py-6 text-center text-sm text-muted-foreground">
510
- {allowCustomValue && searchInput.trim() ? (
511
- <>Press Enter to add &quot;{searchInput.trim()}&quot;</>
512
- ) : (
513
- emptyText
514
- )}
515
- </div>
516
- ) : (
517
- <>
518
- {items.map((item, index) => {
519
- const isSelected = isMultiple
520
- ? Array.isArray(currentValue) && currentValue.includes(item.value)
521
- : currentValue === item.value;
522
- const isHighlighted = highlightedIndex === index;
523
-
524
- return (
525
- <div
526
- key={item.value}
527
- {...getItemProps({ item, index })}
528
- className={cn(
529
- "flex cursor-pointer items-center justify-between px-2 py-2 text-sm outline-none transition-colors",
530
- isHighlighted && "bg-accent text-accent-foreground",
531
- isSelected && "font-medium",
532
- )}
533
- >
534
- <div className="flex-1 truncate">
535
- {renderOption ? renderOption(item, isSelected) : item.label}
536
- </div>
537
- {isSelected && <Check className="h-4 w-4 shrink-0" />}
538
- </div>
539
- );
540
- })}
541
- </>
542
- )}
543
- {hasMore && items.length > 0 && (
544
- <div className="flex items-center justify-center border-t py-2">
545
- {loading ? (
546
- <>
547
- <Loader2 className="h-3 w-3 animate-spin mr-1" />
548
- <span className="text-xs text-muted-foreground">Loading more...</span>
549
- </>
550
- ) : (
551
- <span className="text-xs text-muted-foreground">Scroll for more</span>
552
- )}
553
- </div>
554
- )}
555
- </div>
556
- </PopoverContent>
557
- </Popover>
558
- );
77
+ const isMultiple = !!multiple;
78
+ const isControlled = controlledValue !== undefined;
79
+
80
+ // Internal value state
81
+ const [internalValue, setInternalValue] = useState<
82
+ string | number | null | Array<string | number>
83
+ >(() => {
84
+ if (defaultValue !== undefined) return defaultValue;
85
+ return isMultiple ? [] : null;
86
+ });
87
+
88
+ const currentValue = isControlled ? controlledValue : internalValue;
89
+
90
+ // Label and raw data maps
91
+ const labelMapRef = useRef<Map<string | number, string>>(new Map());
92
+ const rawMapRef = useRef<Map<string | number, T>>(new Map());
93
+
94
+ const storeOption = useCallback((opt: AutocompleteOption<T>) => {
95
+ labelMapRef.current.set(opt.value, opt.label);
96
+ if (opt.raw !== undefined) {
97
+ rawMapRef.current.set(opt.value, opt.raw);
98
+ }
99
+ }, []);
100
+
101
+ // Immediately populate labels for initial values (runs once on mount)
102
+ if (labelMapRef.current.size === 0 && currentValue) {
103
+ const values = Array.isArray(currentValue) ? currentValue : [currentValue];
104
+ // First, store initialSelectedOptions if provided
105
+ if (initialSelectedOptions) {
106
+ const arr = Array.isArray(initialSelectedOptions)
107
+ ? initialSelectedOptions
108
+ : [initialSelectedOptions];
109
+ arr.forEach((opt) => {
110
+ labelMapRef.current.set(opt.value, opt.label);
111
+ if (opt.raw !== undefined) {
112
+ rawMapRef.current.set(opt.value, opt.raw);
113
+ }
114
+ });
115
+ }
116
+ // Then look up missing values in options array
117
+ values.forEach((val) => {
118
+ if (!labelMapRef.current.has(val)) {
119
+ const option = options.find((opt) => opt.value === val);
120
+ if (option) {
121
+ labelMapRef.current.set(option.value, option.label);
122
+ if (option.raw !== undefined) {
123
+ rawMapRef.current.set(option.value, option.raw);
124
+ }
125
+ }
126
+ }
127
+ });
128
+ }
129
+
130
+ // Data state
131
+ const [items, setItems] = useState<AutocompleteOption<T>[]>([]);
132
+ const [loading, setLoading] = useState(false);
133
+ const [hasMore, setHasMore] = useState(false);
134
+ const [page, setPage] = useState(1);
135
+ const [searchInput, setSearchInput] = useState('');
136
+ const [debouncedSearch] = useDebounce(searchInput, 300);
137
+ const [clearCounter, setClearCounter] = useState(0);
138
+
139
+ // Popover state
140
+ const [isOpen, setIsOpen] = useState(!!defaultOpen);
141
+
142
+ // Clear input in multiple mode after selection
143
+ useEffect(() => {
144
+ if (clearCounter > 0) {
145
+ setSearchInput('');
146
+ }
147
+ }, [clearCounter]);
148
+
149
+ // Load data
150
+ const loadData = useCallback(
151
+ async (pageNum: number, search: string) => {
152
+ if (mode === 'server') {
153
+ if (!fetcher) return;
154
+ setLoading(true);
155
+ try {
156
+ const res: AutocompleteFetchResult<T> = await fetcher({
157
+ search,
158
+ moreFilter: fetcherFilter,
159
+ cursor: null,
160
+ page: pageNum,
161
+ pageSize,
162
+ });
163
+ res.items.forEach(storeOption);
164
+ setItems((prev) =>
165
+ pageNum === 1 ? res.items : [...prev, ...res.items],
166
+ );
167
+ setHasMore(!!res.hasMore);
168
+ } catch (_error) {
169
+ if (pageNum === 1) setItems([]);
170
+ setHasMore(false);
171
+ } finally {
172
+ setLoading(false);
173
+ }
174
+ } else {
175
+ // Client mode
176
+ const filtered = search
177
+ ? options.filter((o) =>
178
+ o.label.toLowerCase().includes(search.toLowerCase()),
179
+ )
180
+ : options;
181
+ options.forEach(storeOption);
182
+ const start = (pageNum - 1) * pageSize;
183
+ const slice = filtered.slice(start, start + pageSize);
184
+ setItems((prev) => (pageNum === 1 ? slice : [...prev, ...slice]));
185
+ setHasMore(start + pageSize < filtered.length);
186
+ }
187
+ },
188
+ [mode, fetcher, fetcherFilter, options, pageSize, storeOption],
189
+ );
190
+
191
+ // Reset and load on search change
192
+ useEffect(() => {
193
+ if (!isOpen) return;
194
+ setPage(1);
195
+ loadData(1, debouncedSearch);
196
+ // eslint-disable-next-line react-hooks/exhaustive-deps
197
+ }, [isOpen, debouncedSearch, loadData]);
198
+
199
+ // Load more pages
200
+ useEffect(() => {
201
+ if (!isOpen || page <= 1) return;
202
+ loadData(page, debouncedSearch);
203
+ // eslint-disable-next-line react-hooks/exhaustive-deps
204
+ }, [isOpen, page, loadData, debouncedSearch]);
205
+
206
+ // Store initial/selected options (takes precedence)
207
+ useEffect(() => {
208
+ if (initialSelectedOptions) {
209
+ const arr = Array.isArray(initialSelectedOptions)
210
+ ? initialSelectedOptions
211
+ : [initialSelectedOptions];
212
+ arr.forEach(storeOption);
213
+ }
214
+ }, [initialSelectedOptions, storeOption]);
215
+
216
+ // Auto-populate missing initial options from provided options array
217
+ useEffect(() => {
218
+ if (
219
+ !currentValue ||
220
+ (Array.isArray(currentValue) && currentValue.length === 0)
221
+ )
222
+ return;
223
+
224
+ const values = Array.isArray(currentValue) ? currentValue : [currentValue];
225
+
226
+ // Only look up values that are missing (not in label map)
227
+ const missingValues = values.filter((v) => !labelMapRef.current.has(v));
228
+
229
+ if (missingValues.length === 0) return;
230
+
231
+ // Look up missing values in the options array
232
+ missingValues.forEach((val) => {
233
+ const option = options.find((opt) => opt.value === val);
234
+ if (option) {
235
+ storeOption(option);
236
+ }
237
+ });
238
+ }, [currentValue, options, storeOption]);
239
+
240
+ // Load selected labels on mount for initial values
241
+ const hasLoadedInitial = useRef(false);
242
+ const [, setLabelsLoadedCounter] = useState(0);
243
+
244
+ useEffect(() => {
245
+ // Only run once on mount
246
+ if (!loadSelected || hasLoadedInitial.current) return;
247
+ if (!currentValue) return;
248
+
249
+ const values = Array.isArray(currentValue) ? currentValue : [currentValue];
250
+ if (values.length === 0) return;
251
+
252
+ // Check if any values are missing labels
253
+ const missing = values.filter((v) => !labelMapRef.current.has(v));
254
+ if (missing.length === 0) {
255
+ hasLoadedInitial.current = true;
256
+ return;
257
+ }
258
+
259
+ hasLoadedInitial.current = true;
260
+ let cancelled = false;
261
+ loadSelected(missing)
262
+ .then((opts) => {
263
+ if (!cancelled && opts.length > 0) {
264
+ opts.forEach(storeOption);
265
+ // Only trigger re-render if we actually stored something
266
+ setLabelsLoadedCounter((c) => c + 1);
267
+ }
268
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
269
+ })
270
+ .catch(() => {});
271
+ return () => {
272
+ cancelled = true;
273
+ };
274
+ // eslint-disable-next-line react-hooks/exhaustive-deps
275
+ }, [loadSelected, storeOption, currentValue]); // Only run once on mount - uses closure values
276
+
277
+ // Load selected labels if missing when dropdown opens
278
+ useEffect(() => {
279
+ if (!loadSelected || !isOpen) return;
280
+ const values = Array.isArray(currentValue)
281
+ ? currentValue
282
+ : currentValue
283
+ ? [currentValue]
284
+ : [];
285
+ const missing = values.filter((v) => !labelMapRef.current.has(v));
286
+ if (missing.length === 0) return;
287
+
288
+ let cancelled = false;
289
+ loadSelected(missing)
290
+ .then((opts) => {
291
+ if (!cancelled) opts.forEach(storeOption);
292
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
293
+ })
294
+ .catch(() => {});
295
+ return () => {
296
+ cancelled = true;
297
+ };
298
+ }, [currentValue, loadSelected, isOpen, storeOption]);
299
+
300
+ // Get label helper
301
+ const getLabel = useCallback((v: string | number) => {
302
+ return labelMapRef.current.get(v) ?? String(v);
303
+ }, []);
304
+
305
+ // Selected items for display
306
+ const selectedItems = useMemo(() => {
307
+ if (!isMultiple) {
308
+ if (
309
+ currentValue === null ||
310
+ currentValue === undefined ||
311
+ Array.isArray(currentValue)
312
+ )
313
+ return [];
314
+ return [
315
+ {
316
+ value: currentValue,
317
+ label: getLabel(currentValue),
318
+ raw: rawMapRef.current.get(currentValue),
319
+ },
320
+ ];
321
+ }
322
+ const values = Array.isArray(currentValue) ? currentValue : [];
323
+ return values.map((v) => ({
324
+ value: v,
325
+ label: getLabel(v),
326
+ raw: rawMapRef.current.get(v),
327
+ }));
328
+ // eslint-disable-next-line react-hooks/exhaustive-deps
329
+ }, [currentValue, isMultiple, getLabel]); // labelsLoadedCounter triggers re-compute when loadSelected completes
330
+
331
+ // Handle selection
332
+ const handleSelect = useCallback(
333
+ (item: AutocompleteOption<T> | null) => {
334
+ if (!item) return;
335
+ storeOption(item);
336
+
337
+ if (isMultiple) {
338
+ const values = Array.isArray(currentValue) ? currentValue : [];
339
+ const exists = values.includes(item.value);
340
+
341
+ // Skip if already selected (don't toggle), but still clear the search
342
+ if (exists) {
343
+ setClearCounter((c) => c + 1);
344
+ return;
345
+ }
346
+
347
+ const newValues = [...values, item.value];
348
+
349
+ if (!isControlled) setInternalValue(newValues);
350
+ const newOptions = newValues.map((v) => ({
351
+ value: v,
352
+ label: getLabel(v),
353
+ raw: rawMapRef.current.get(v),
354
+ }));
355
+ const raws = newOptions
356
+ .map((o) => o.raw)
357
+ .filter((r): r is T => r !== undefined);
358
+ onChange?.(newValues, newOptions, raws);
359
+
360
+ // Trigger input clear
361
+ setClearCounter((c) => c + 1);
362
+ } else {
363
+ if (!isControlled) setInternalValue(item.value);
364
+ onChange?.(item.value, item, item.raw ?? null);
365
+ setIsOpen(false);
366
+ }
367
+ },
368
+ [isMultiple, currentValue, isControlled, onChange, getLabel, storeOption],
369
+ );
370
+
371
+ // Handle remove chip
372
+ const handleRemove = useCallback(
373
+ (valueToRemove: string | number) => {
374
+ const values = Array.isArray(currentValue) ? currentValue : [];
375
+ const newValues = values.filter((v) => v !== valueToRemove);
376
+ if (!isControlled) setInternalValue(newValues);
377
+ const newOptions = newValues.map((v) => ({
378
+ value: v,
379
+ label: getLabel(v),
380
+ raw: rawMapRef.current.get(v),
381
+ }));
382
+ const raws = newOptions
383
+ .map((o) => o.raw)
384
+ .filter((r): r is T => r !== undefined);
385
+ onChange?.(newValues, newOptions, raws);
386
+ },
387
+ [currentValue, isControlled, onChange, getLabel],
388
+ );
389
+
390
+ // Handle clear
391
+ const handleClear = useCallback(() => {
392
+ const newValue = isMultiple ? [] : null;
393
+ if (!isControlled) setInternalValue(newValue);
394
+ onChange?.(newValue, isMultiple ? [] : null, isMultiple ? [] : null);
395
+ }, [isMultiple, isControlled, onChange]);
396
+
397
+ // Handle custom value creation
398
+ const handleCreateCustom = useCallback(() => {
399
+ const trimmed = searchInput.trim();
400
+ if (!trimmed || !allowCustomValue) return;
401
+
402
+ const newOption: AutocompleteOption<T> = { value: trimmed, label: trimmed };
403
+ storeOption(newOption);
404
+
405
+ if (isMultiple) {
406
+ const values = Array.isArray(currentValue) ? currentValue : [];
407
+ if (values.includes(trimmed)) return;
408
+ const newValues = [...values, trimmed];
409
+ if (!isControlled) setInternalValue(newValues);
410
+ const newOptions = newValues.map((v) => ({
411
+ value: v,
412
+ label: getLabel(v),
413
+ raw: rawMapRef.current.get(v),
414
+ }));
415
+ onChange?.(newValues, newOptions, []);
416
+ setSearchInput('');
417
+ } else {
418
+ if (!isControlled) setInternalValue(trimmed);
419
+ onChange?.(trimmed, newOption, null);
420
+ setSearchInput('');
421
+ setIsOpen(false);
422
+ }
423
+ }, [
424
+ searchInput,
425
+ allowCustomValue,
426
+ isMultiple,
427
+ currentValue,
428
+ isControlled,
429
+ onChange,
430
+ getLabel,
431
+ storeOption,
432
+ ]);
433
+
434
+ // Compute input value based on mode and state
435
+ const computedInputValue = useMemo(() => {
436
+ // In multiple mode or when dropdown is open, show search input
437
+ if (isMultiple || isOpen) {
438
+ return searchInput;
439
+ }
440
+ // In single mode when closed, show selected item label
441
+ if (selectedItems.length > 0) {
442
+ return selectedItems[0].label;
443
+ }
444
+ return '';
445
+ }, [isMultiple, isOpen, searchInput, selectedItems]);
446
+
447
+ // Downshift
448
+ const { getInputProps, getItemProps, getMenuProps, highlightedIndex } =
449
+ useCombobox({
450
+ items,
451
+ itemToString: (item) => item?.label ?? '',
452
+ selectedItem: isMultiple ? null : (selectedItems[0] ?? null),
453
+ onSelectedItemChange: ({ selectedItem }) => handleSelect(selectedItem),
454
+ isOpen,
455
+ onIsOpenChange: ({ isOpen: newIsOpen }) => setIsOpen(newIsOpen ?? false),
456
+ inputValue: computedInputValue,
457
+ onInputValueChange: ({ inputValue }) => setSearchInput(inputValue ?? ''),
458
+ });
459
+
460
+ // Refs
461
+ const parentRef = useRef<HTMLDivElement>(null);
462
+ const inputRef = useRef<HTMLInputElement>(null);
463
+
464
+ // Stable ref callback for merging Downshift's ref with our parentRef
465
+ const menuRefCallback = useCallback((node: HTMLDivElement | null) => {
466
+ parentRef.current = node;
467
+ }, []);
468
+
469
+ const handleScroll = useCallback(() => {
470
+ if (!parentRef.current || !hasMore || loading) return;
471
+
472
+ const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
473
+ const scrolledToBottom = scrollHeight - scrollTop - clientHeight < 50;
474
+
475
+ if (scrolledToBottom) {
476
+ setPage((p) => p + 1);
477
+ }
478
+ }, [hasMore, loading]);
479
+
480
+ useEffect(() => {
481
+ const element = parentRef.current;
482
+ if (!element) return;
483
+
484
+ element.addEventListener('scroll', handleScroll);
485
+ return () => element.removeEventListener('scroll', handleScroll);
486
+ }, [handleScroll]);
487
+
488
+ const showClearButton =
489
+ clearable &&
490
+ ((isMultiple && selectedItems.length > 0) ||
491
+ (!isMultiple &&
492
+ currentValue !== null &&
493
+ currentValue !== undefined &&
494
+ !Array.isArray(currentValue)));
495
+
496
+ return (
497
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
498
+ <PopoverTrigger asChild>
499
+ <div
500
+ className={cn(
501
+ 'flex min-h-10 w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm',
502
+ 'ring-offset-background',
503
+ 'focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
504
+ disabled && 'cursor-not-allowed opacity-50',
505
+ className,
506
+ )}
507
+ >
508
+ {isMultiple && (
509
+ <div className="flex flex-wrap gap-1">
510
+ {selectedItems.map((item) => (
511
+ <Badge
512
+ key={item.value}
513
+ variant={chipVariant}
514
+ className={cn('gap-1', chipClassName)}
515
+ >
516
+ <span className="max-w-[150px] truncate">{item.label}</span>
517
+ <button
518
+ type="button"
519
+ onClick={(e) => {
520
+ e.stopPropagation();
521
+ handleRemove(item.value);
522
+ }}
523
+ className="rounded-sm opacity-70 hover:opacity-100"
524
+ >
525
+ <X className="h-3 w-3" />
526
+ </button>
527
+ </Badge>
528
+ ))}
529
+ </div>
530
+ )}
531
+ <input
532
+ {...getInputProps({
533
+ ref: inputRef,
534
+ placeholder,
535
+ disabled,
536
+ onClick: () => {
537
+ if (!isOpen) setIsOpen(true);
538
+ },
539
+ onKeyDown: (e) => {
540
+ if (
541
+ e.key === 'Enter' &&
542
+ allowCustomValue &&
543
+ searchInput.trim() &&
544
+ items.length === 0
545
+ ) {
546
+ e.preventDefault();
547
+ handleCreateCustom();
548
+ }
549
+ },
550
+ })}
551
+ className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground min-w-[120px]"
552
+ />
553
+ <div className="flex items-center gap-2 shrink-0">
554
+ {showClearButton && (
555
+ <button
556
+ type="button"
557
+ onClick={(e) => {
558
+ e.stopPropagation();
559
+ handleClear();
560
+ }}
561
+ className="rounded-sm opacity-70 hover:opacity-100"
562
+ >
563
+ <X className="h-4 w-4" />
564
+ </button>
565
+ )}
566
+ <ChevronsUpDown className="h-4 w-4 opacity-50" />
567
+ </div>
568
+ </div>
569
+ </PopoverTrigger>
570
+ <PopoverContent
571
+ className="p-0"
572
+ style={{ width: 'var(--radix-popover-trigger-width)' }}
573
+ align="start"
574
+ onOpenAutoFocus={(e) => e.preventDefault()}
575
+ >
576
+ <div
577
+ {...getMenuProps({
578
+ ref: menuRefCallback,
579
+ })}
580
+ className="max-h-[300px] overflow-auto"
581
+ >
582
+ {loading && items.length === 0 ? (
583
+ <div className="flex items-center justify-center py-6">
584
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
585
+ <span className="text-sm text-muted-foreground">Loading...</span>
586
+ </div>
587
+ ) : items.length === 0 ? (
588
+ <div className="py-6 text-center text-sm text-muted-foreground">
589
+ {allowCustomValue && searchInput.trim() ? (
590
+ <>Press Enter to add &quot;{searchInput.trim()}&quot;</>
591
+ ) : (
592
+ emptyText
593
+ )}
594
+ </div>
595
+ ) : (
596
+ items.map((item, index) => {
597
+ const isSelected = isMultiple
598
+ ? Array.isArray(currentValue) &&
599
+ currentValue.includes(item.value)
600
+ : currentValue === item.value;
601
+ const isHighlighted = highlightedIndex === index;
602
+
603
+ return (
604
+ <div
605
+ key={item.value}
606
+ {...getItemProps({ item, index })}
607
+ className={cn(
608
+ 'flex cursor-pointer items-center justify-between px-2 py-2 text-sm outline-none transition-colors',
609
+ isHighlighted && 'bg-accent text-accent-foreground',
610
+ isSelected && 'font-medium',
611
+ )}
612
+ >
613
+ <div className="flex-1 truncate">
614
+ {renderOption ? renderOption(item, isSelected) : item.label}
615
+ </div>
616
+ {isSelected && <Check className="h-4 w-4 shrink-0" />}
617
+ </div>
618
+ );
619
+ })
620
+ )}
621
+ {hasMore && items.length > 0 && (
622
+ <div className="flex items-center justify-center border-t py-2">
623
+ {loading ? (
624
+ <>
625
+ <Loader2 className="h-3 w-3 animate-spin mr-1" />
626
+ <span className="text-xs text-muted-foreground">
627
+ Loading more...
628
+ </span>
629
+ </>
630
+ ) : (
631
+ <span className="text-xs text-muted-foreground">
632
+ Scroll for more
633
+ </span>
634
+ )}
635
+ </div>
636
+ )}
637
+ </div>
638
+ </PopoverContent>
639
+ </Popover>
640
+ );
559
641
  }
560
642
 
561
643
  export default Autocomplete;