@elabs-ai/components-ui 4.0.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 (449) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/dist/index.d.ts +4497 -0
  4. package/dist/index.js +15192 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/cn.d.ts +10 -0
  7. package/dist/lib/cn.js +20 -0
  8. package/dist/lib/cn.js.map +1 -0
  9. package/package.json +106 -0
  10. package/src/blocks/sidebar-02/app-sidebar.tsx +160 -0
  11. package/src/blocks/sidebar-02/logo.tsx +18 -0
  12. package/src/blocks/sidebar-02/nav-main.tsx +13 -0
  13. package/src/blocks/sidebar-02/nav-notifications.tsx +7 -0
  14. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +23 -0
  15. package/src/blocks/sidebar-02/team-switcher.tsx +7 -0
  16. package/src/blocks/sidebar-04/app-sidebar.tsx +236 -0
  17. package/src/blocks/sidebar-04/mail-context.tsx +29 -0
  18. package/src/blocks/sidebar-04/nav-user.tsx +6 -0
  19. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +150 -0
  20. package/src/blocks/sidebar-05/app-sidebar.tsx +375 -0
  21. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +18 -0
  22. package/src/blocks/sidebar-05/team-switcher.tsx +6 -0
  23. package/src/blocks-comparison-table.stories.tsx +165 -0
  24. package/src/components/accordion/accordion.stories.tsx +71 -0
  25. package/src/components/accordion/accordion.test.tsx +18 -0
  26. package/src/components/accordion/accordion.tsx +49 -0
  27. package/src/components/accordion/index.ts +1 -0
  28. package/src/components/advanced-group/advanced-group.stories.tsx +143 -0
  29. package/src/components/advanced-group/advanced-group.test.tsx +81 -0
  30. package/src/components/advanced-group/advanced-group.tsx +111 -0
  31. package/src/components/advanced-group/index.ts +2 -0
  32. package/src/components/alert/alert.stories.tsx +156 -0
  33. package/src/components/alert/alert.test.tsx +57 -0
  34. package/src/components/alert/alert.tsx +91 -0
  35. package/src/components/alert/index.ts +8 -0
  36. package/src/components/alert-dialog/alert-dialog.stories.tsx +73 -0
  37. package/src/components/alert-dialog/alert-dialog.tsx +104 -0
  38. package/src/components/alert-dialog/index.ts +12 -0
  39. package/src/components/app-shell/app-shell.stories.tsx +61 -0
  40. package/src/components/app-shell/app-shell.tsx +31 -0
  41. package/src/components/app-shell/index.ts +1 -0
  42. package/src/components/app-sidebar/app-sidebar.stories.tsx +121 -0
  43. package/src/components/app-sidebar/app-sidebar.test.tsx +55 -0
  44. package/src/components/app-sidebar/app-sidebar.tsx +53 -0
  45. package/src/components/app-sidebar/index.ts +1 -0
  46. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +46 -0
  47. package/src/components/aspect-ratio/aspect-ratio.tsx +3 -0
  48. package/src/components/aspect-ratio/index.ts +1 -0
  49. package/src/components/attribution-panel/attribution-panel.stories.tsx +83 -0
  50. package/src/components/attribution-panel/attribution-panel.test.tsx +123 -0
  51. package/src/components/attribution-panel/attribution-panel.tsx +191 -0
  52. package/src/components/attribution-panel/attribution-types.ts +44 -0
  53. package/src/components/attribution-panel/attributions.generated.ts +1422 -0
  54. package/src/components/attribution-panel/index.ts +3 -0
  55. package/src/components/avatar/avatar.stories.tsx +45 -0
  56. package/src/components/avatar/avatar.test.tsx +13 -0
  57. package/src/components/avatar/avatar.tsx +46 -0
  58. package/src/components/avatar/index.ts +1 -0
  59. package/src/components/badge/badge.stories.tsx +44 -0
  60. package/src/components/badge/badge.test.tsx +10 -0
  61. package/src/components/badge/badge.tsx +28 -0
  62. package/src/components/badge/index.ts +1 -0
  63. package/src/components/bento-grid/bento-grid.stories.tsx +165 -0
  64. package/src/components/bento-grid/bento-grid.test.tsx +119 -0
  65. package/src/components/bento-grid/bento-grid.tsx +339 -0
  66. package/src/components/bento-grid/index.ts +9 -0
  67. package/src/components/bounded-number/bounded-number.stories.tsx +118 -0
  68. package/src/components/bounded-number/bounded-number.test.tsx +73 -0
  69. package/src/components/bounded-number/bounded-number.tsx +110 -0
  70. package/src/components/bounded-number/index.ts +1 -0
  71. package/src/components/breadcrumb/breadcrumb.stories.tsx +47 -0
  72. package/src/components/breadcrumb/breadcrumb.test.tsx +17 -0
  73. package/src/components/breadcrumb/breadcrumb.tsx +86 -0
  74. package/src/components/breadcrumb/index.ts +9 -0
  75. package/src/components/button/button.stories.tsx +70 -0
  76. package/src/components/button/button.test.tsx +50 -0
  77. package/src/components/button/button.tsx +63 -0
  78. package/src/components/button/index.ts +1 -0
  79. package/src/components/button-group/button-group.stories.tsx +45 -0
  80. package/src/components/button-group/button-group.tsx +70 -0
  81. package/src/components/button-group/index.ts +6 -0
  82. package/src/components/calendar/calendar.stories.tsx +95 -0
  83. package/src/components/calendar/calendar.test.tsx +140 -0
  84. package/src/components/calendar/calendar.tsx +70 -0
  85. package/src/components/calendar/index.ts +1 -0
  86. package/src/components/card/card.stories.tsx +438 -0
  87. package/src/components/card/card.test.tsx +153 -0
  88. package/src/components/card/card.tsx +332 -0
  89. package/src/components/card/index.ts +13 -0
  90. package/src/components/carousel/carousel.stories.tsx +102 -0
  91. package/src/components/carousel/carousel.tsx +225 -0
  92. package/src/components/carousel/index.ts +8 -0
  93. package/src/components/change-review/change-review.stories.tsx +288 -0
  94. package/src/components/change-review/change-review.test.tsx +261 -0
  95. package/src/components/change-review/change-review.tsx +637 -0
  96. package/src/components/change-review/index.ts +23 -0
  97. package/src/components/checkbox/checkbox.stories.tsx +119 -0
  98. package/src/components/checkbox/checkbox.test.tsx +40 -0
  99. package/src/components/checkbox/checkbox.tsx +34 -0
  100. package/src/components/checkbox/index.ts +1 -0
  101. package/src/components/collapsible/collapsible.stories.tsx +62 -0
  102. package/src/components/collapsible/collapsible.tsx +22 -0
  103. package/src/components/collapsible/index.ts +1 -0
  104. package/src/components/collapsible-panel/index.ts +8 -0
  105. package/src/components/collapsible-panel/use-collapsible-panel.test.tsx +93 -0
  106. package/src/components/collapsible-panel/use-collapsible-panel.ts +159 -0
  107. package/src/components/color-picker/color-picker.stories.tsx +93 -0
  108. package/src/components/color-picker/color-picker.test.tsx +79 -0
  109. package/src/components/color-picker/color-picker.tsx +341 -0
  110. package/src/components/color-picker/index.ts +1 -0
  111. package/src/components/combobox/combobox.stories.tsx +154 -0
  112. package/src/components/combobox/combobox.test.tsx +246 -0
  113. package/src/components/combobox/combobox.tsx +210 -0
  114. package/src/components/combobox/index.ts +1 -0
  115. package/src/components/command/command.stories.tsx +371 -0
  116. package/src/components/command/command.test.tsx +280 -0
  117. package/src/components/command/command.tsx +301 -0
  118. package/src/components/command/index.ts +13 -0
  119. package/src/components/confirm-dialog/confirm-dialog.stories.tsx +183 -0
  120. package/src/components/confirm-dialog/confirm-dialog.test.tsx +147 -0
  121. package/src/components/confirm-dialog/confirm-dialog.tsx +146 -0
  122. package/src/components/confirm-dialog/index.ts +2 -0
  123. package/src/components/context-menu/context-menu.stories.tsx +70 -0
  124. package/src/components/context-menu/context-menu.tsx +172 -0
  125. package/src/components/context-menu/index.ts +16 -0
  126. package/src/components/copyable-value/copyable-value.stories.tsx +80 -0
  127. package/src/components/copyable-value/copyable-value.test.tsx +128 -0
  128. package/src/components/copyable-value/copyable-value.tsx +101 -0
  129. package/src/components/copyable-value/index.ts +1 -0
  130. package/src/components/date-picker/date-picker.stories.tsx +42 -0
  131. package/src/components/date-picker/date-picker.tsx +49 -0
  132. package/src/components/date-picker/index.ts +1 -0
  133. package/src/components/date-range-picker/date-range-picker.stories.tsx +151 -0
  134. package/src/components/date-range-picker/date-range-picker.test.tsx +62 -0
  135. package/src/components/date-range-picker/date-range-picker.tsx +126 -0
  136. package/src/components/date-range-picker/index.ts +6 -0
  137. package/src/components/descriptions/descriptions.stories.tsx +43 -0
  138. package/src/components/descriptions/descriptions.test.tsx +41 -0
  139. package/src/components/descriptions/descriptions.tsx +95 -0
  140. package/src/components/descriptions/index.ts +6 -0
  141. package/src/components/dialog/dialog.stories.tsx +728 -0
  142. package/src/components/dialog/dialog.test.tsx +285 -0
  143. package/src/components/dialog/dialog.tsx +340 -0
  144. package/src/components/dialog/index.ts +18 -0
  145. package/src/components/dialog/use-dialog-dismiss-guard.test.tsx +80 -0
  146. package/src/components/dialog/use-dialog-dismiss-guard.ts +109 -0
  147. package/src/components/drawer/drawer.stories.tsx +76 -0
  148. package/src/components/drawer/drawer.tsx +85 -0
  149. package/src/components/drawer/index.ts +11 -0
  150. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +176 -0
  151. package/src/components/dropdown-menu/dropdown-menu.test.tsx +109 -0
  152. package/src/components/dropdown-menu/dropdown-menu.tsx +179 -0
  153. package/src/components/dropdown-menu/index.ts +17 -0
  154. package/src/components/empty-state/empty-state.stories.tsx +54 -0
  155. package/src/components/empty-state/empty-state.test.tsx +11 -0
  156. package/src/components/empty-state/empty-state.tsx +30 -0
  157. package/src/components/empty-state/index.ts +1 -0
  158. package/src/components/error-state/error-state.stories.tsx +51 -0
  159. package/src/components/error-state/error-state.tsx +27 -0
  160. package/src/components/error-state/index.ts +1 -0
  161. package/src/components/expand-dialog/expand-dialog.stories.tsx +246 -0
  162. package/src/components/expand-dialog/expand-dialog.test.tsx +185 -0
  163. package/src/components/expand-dialog/expand-dialog.tsx +387 -0
  164. package/src/components/expand-dialog/index.ts +11 -0
  165. package/src/components/field-row/field-row.stories.tsx +114 -0
  166. package/src/components/field-row/field-row.test.tsx +188 -0
  167. package/src/components/field-row/field-row.tsx +115 -0
  168. package/src/components/field-row/index.ts +1 -0
  169. package/src/components/file-upload/file-upload.stories.tsx +183 -0
  170. package/src/components/file-upload/file-upload.test.tsx +169 -0
  171. package/src/components/file-upload/file-upload.tsx +459 -0
  172. package/src/components/file-upload/index.ts +13 -0
  173. package/src/components/form/form.stories.tsx +71 -0
  174. package/src/components/form/form.tsx +141 -0
  175. package/src/components/form/index.ts +10 -0
  176. package/src/components/hover-card/hover-card.stories.tsx +57 -0
  177. package/src/components/hover-card/hover-card.tsx +25 -0
  178. package/src/components/hover-card/index.ts +1 -0
  179. package/src/components/icon-button/icon-button.stories.tsx +108 -0
  180. package/src/components/icon-button/icon-button.test.tsx +93 -0
  181. package/src/components/icon-button/icon-button.tsx +139 -0
  182. package/src/components/icon-button/index.ts +1 -0
  183. package/src/components/input/index.ts +1 -0
  184. package/src/components/input/input.stories.tsx +44 -0
  185. package/src/components/input/input.test.tsx +19 -0
  186. package/src/components/input/input.tsx +26 -0
  187. package/src/components/input-group/index.ts +11 -0
  188. package/src/components/input-group/input-group.stories.tsx +87 -0
  189. package/src/components/input-group/input-group.test.tsx +48 -0
  190. package/src/components/input-group/input-group.tsx +170 -0
  191. package/src/components/input-otp/index.ts +1 -0
  192. package/src/components/input-otp/input-otp.stories.tsx +66 -0
  193. package/src/components/input-otp/input-otp.tsx +68 -0
  194. package/src/components/kbd/index.ts +1 -0
  195. package/src/components/kbd/kbd.stories.tsx +45 -0
  196. package/src/components/kbd/kbd.tsx +20 -0
  197. package/src/components/key-value-editor/index.ts +1 -0
  198. package/src/components/key-value-editor/key-value-editor.stories.tsx +130 -0
  199. package/src/components/key-value-editor/key-value-editor.test.tsx +231 -0
  200. package/src/components/key-value-editor/key-value-editor.tsx +263 -0
  201. package/src/components/label/index.ts +1 -0
  202. package/src/components/label/label.stories.tsx +35 -0
  203. package/src/components/label/label.tsx +19 -0
  204. package/src/components/link-preview/index.ts +2 -0
  205. package/src/components/link-preview/link-preview.stories.tsx +178 -0
  206. package/src/components/link-preview/link-preview.test.tsx +123 -0
  207. package/src/components/link-preview/link-preview.tsx +194 -0
  208. package/src/components/list-editor/index.ts +1 -0
  209. package/src/components/list-editor/list-editor.stories.tsx +119 -0
  210. package/src/components/list-editor/list-editor.test.tsx +96 -0
  211. package/src/components/list-editor/list-editor.tsx +192 -0
  212. package/src/components/loading-state/index.ts +1 -0
  213. package/src/components/loading-state/loading-state.stories.tsx +38 -0
  214. package/src/components/loading-state/loading-state.tsx +26 -0
  215. package/src/components/locale-provider/index.ts +8 -0
  216. package/src/components/locale-provider/locale-provider.stories.tsx +83 -0
  217. package/src/components/locale-provider/locale-provider.test.tsx +238 -0
  218. package/src/components/locale-provider/locale-provider.tsx +187 -0
  219. package/src/components/locale-provider/messages.ts +300 -0
  220. package/src/components/match-highlight/index.ts +7 -0
  221. package/src/components/match-highlight/match-highlight.stories.tsx +146 -0
  222. package/src/components/match-highlight/match-highlight.test.tsx +202 -0
  223. package/src/components/match-highlight/match-highlight.tsx +145 -0
  224. package/src/components/mention-input/index.ts +28 -0
  225. package/src/components/mention-input/mention-input-mirror.stories.tsx +171 -0
  226. package/src/components/mention-input/mention-input.stories.tsx +463 -0
  227. package/src/components/mention-input/mention-input.test.tsx +617 -0
  228. package/src/components/mention-input/mention-input.tsx +1102 -0
  229. package/src/components/mention-input/mention-value.test.ts +253 -0
  230. package/src/components/mention-input/mention-value.ts +193 -0
  231. package/src/components/menubar/index.ts +17 -0
  232. package/src/components/menubar/menubar.stories.tsx +67 -0
  233. package/src/components/menubar/menubar.tsx +198 -0
  234. package/src/components/metric-card/index.ts +1 -0
  235. package/src/components/metric-card/metric-card.stories.tsx +224 -0
  236. package/src/components/metric-card/metric-card.test.tsx +198 -0
  237. package/src/components/metric-card/metric-card.tsx +229 -0
  238. package/src/components/model-picker/index.ts +11 -0
  239. package/src/components/model-picker/model-picker-state.ts +85 -0
  240. package/src/components/model-picker/model-picker.stories.tsx +128 -0
  241. package/src/components/model-picker/model-picker.test.tsx +241 -0
  242. package/src/components/model-picker/model-picker.tsx +270 -0
  243. package/src/components/nav-main/index.ts +1 -0
  244. package/src/components/nav-main/nav-main.tsx +147 -0
  245. package/src/components/nav-notifications/index.ts +5 -0
  246. package/src/components/nav-notifications/nav-notifications.test.tsx +15 -0
  247. package/src/components/nav-notifications/nav-notifications.tsx +70 -0
  248. package/src/components/nav-user/index.ts +1 -0
  249. package/src/components/nav-user/nav-user.test.tsx +29 -0
  250. package/src/components/nav-user/nav-user.tsx +114 -0
  251. package/src/components/navigation-menu/index.ts +10 -0
  252. package/src/components/navigation-menu/navigation-menu.stories.tsx +92 -0
  253. package/src/components/navigation-menu/navigation-menu.tsx +99 -0
  254. package/src/components/number-input/index.ts +1 -0
  255. package/src/components/number-input/number-input.stories.tsx +120 -0
  256. package/src/components/number-input/number-input.test.tsx +96 -0
  257. package/src/components/number-input/number-input.tsx +219 -0
  258. package/src/components/page-shell/index.ts +1 -0
  259. package/src/components/page-shell/page-shell.stories.tsx +105 -0
  260. package/src/components/page-shell/page-shell.test.tsx +57 -0
  261. package/src/components/page-shell/page-shell.tsx +62 -0
  262. package/src/components/pagination/index.ts +9 -0
  263. package/src/components/pagination/pagination.stories.tsx +57 -0
  264. package/src/components/pagination/pagination.tsx +77 -0
  265. package/src/components/popover/index.ts +1 -0
  266. package/src/components/popover/popover.stories.tsx +28 -0
  267. package/src/components/popover/popover.test.tsx +16 -0
  268. package/src/components/popover/popover.tsx +29 -0
  269. package/src/components/progress/index.ts +1 -0
  270. package/src/components/progress/progress.stories.tsx +80 -0
  271. package/src/components/progress/progress.test.tsx +35 -0
  272. package/src/components/progress/progress.tsx +50 -0
  273. package/src/components/radio-group/index.ts +1 -0
  274. package/src/components/radio-group/radio-group.stories.tsx +75 -0
  275. package/src/components/radio-group/radio-group.test.tsx +16 -0
  276. package/src/components/radio-group/radio-group.tsx +33 -0
  277. package/src/components/rating/index.ts +1 -0
  278. package/src/components/rating/rating.stories.tsx +111 -0
  279. package/src/components/rating/rating.test.tsx +41 -0
  280. package/src/components/rating/rating.tsx +226 -0
  281. package/src/components/resizable/index.ts +1 -0
  282. package/src/components/resizable/resizable.stories.tsx +25 -0
  283. package/src/components/resizable/resizable.tsx +40 -0
  284. package/src/components/reveal/index.ts +7 -0
  285. package/src/components/reveal/reveal.stories.tsx +90 -0
  286. package/src/components/reveal/reveal.test.tsx +43 -0
  287. package/src/components/reveal/reveal.tsx +95 -0
  288. package/src/components/revision-timeline/index.ts +10 -0
  289. package/src/components/revision-timeline/revision-timeline.stories.tsx +785 -0
  290. package/src/components/revision-timeline/revision-timeline.test.tsx +509 -0
  291. package/src/components/revision-timeline/revision-timeline.tsx +1107 -0
  292. package/src/components/scroll-area/index.ts +1 -0
  293. package/src/components/scroll-area/scroll-area.stories.tsx +45 -0
  294. package/src/components/scroll-area/scroll-area.tsx +43 -0
  295. package/src/components/section-header/index.ts +1 -0
  296. package/src/components/section-header/section-header.stories.tsx +20 -0
  297. package/src/components/section-header/section-header.tsx +36 -0
  298. package/src/components/segmented-field/index.ts +5 -0
  299. package/src/components/segmented-field/segmented-field.stories.tsx +121 -0
  300. package/src/components/segmented-field/segmented-field.test.tsx +163 -0
  301. package/src/components/segmented-field/segmented-field.tsx +235 -0
  302. package/src/components/select/index.ts +10 -0
  303. package/src/components/select/select.stories.tsx +106 -0
  304. package/src/components/select/select.test.tsx +98 -0
  305. package/src/components/select/select.tsx +230 -0
  306. package/src/components/separator/index.ts +2 -0
  307. package/src/components/separator/separator.stories.tsx +87 -0
  308. package/src/components/separator/separator.tsx +51 -0
  309. package/src/components/sheet/index.ts +12 -0
  310. package/src/components/sheet/sheet.stories.tsx +65 -0
  311. package/src/components/sheet/sheet.tsx +109 -0
  312. package/src/components/sidebar/index.ts +27 -0
  313. package/src/components/sidebar/sidebar.stories.tsx +69 -0
  314. package/src/components/sidebar/sidebar.test.tsx +43 -0
  315. package/src/components/sidebar/sidebar.tsx +688 -0
  316. package/src/components/skeleton/index.ts +1 -0
  317. package/src/components/skeleton/skeleton.stories.tsx +28 -0
  318. package/src/components/skeleton/skeleton.tsx +15 -0
  319. package/src/components/slider/index.ts +1 -0
  320. package/src/components/slider/slider.stories.tsx +211 -0
  321. package/src/components/slider/slider.test.tsx +168 -0
  322. package/src/components/slider/slider.tsx +136 -0
  323. package/src/components/slider-number/index.ts +1 -0
  324. package/src/components/slider-number/slider-number.stories.tsx +123 -0
  325. package/src/components/slider-number/slider-number.test.tsx +165 -0
  326. package/src/components/slider-number/slider-number.tsx +191 -0
  327. package/src/components/sonner/index.ts +1 -0
  328. package/src/components/sonner/sonner.stories.tsx +124 -0
  329. package/src/components/sonner/sonner.test.tsx +76 -0
  330. package/src/components/sonner/sonner.tsx +101 -0
  331. package/src/components/spinner/index.ts +1 -0
  332. package/src/components/spinner/spinner.stories.tsx +27 -0
  333. package/src/components/spinner/spinner.tsx +19 -0
  334. package/src/components/split-panel/index.ts +1 -0
  335. package/src/components/split-panel/split-panel.stories.tsx +60 -0
  336. package/src/components/split-panel/split-panel.test.tsx +38 -0
  337. package/src/components/split-panel/split-panel.tsx +95 -0
  338. package/src/components/state-panel/index.ts +1 -0
  339. package/src/components/state-panel/state-panel.stories.tsx +80 -0
  340. package/src/components/state-panel/state-panel.test.tsx +64 -0
  341. package/src/components/state-panel/state-panel.tsx +167 -0
  342. package/src/components/status-badge/index.ts +18 -0
  343. package/src/components/status-badge/status-badge.stories.tsx +132 -0
  344. package/src/components/status-badge/status-badge.test.tsx +189 -0
  345. package/src/components/status-badge/status-badge.tsx +330 -0
  346. package/src/components/switch/index.ts +1 -0
  347. package/src/components/switch/switch.stories.tsx +58 -0
  348. package/src/components/switch/switch.test.tsx +12 -0
  349. package/src/components/switch/switch.tsx +29 -0
  350. package/src/components/table/index.ts +10 -0
  351. package/src/components/table/table.stories.tsx +53 -0
  352. package/src/components/table/table.test.tsx +17 -0
  353. package/src/components/table/table.tsx +106 -0
  354. package/src/components/tabs/index.ts +1 -0
  355. package/src/components/tabs/tabs.stories.tsx +267 -0
  356. package/src/components/tabs/tabs.test.tsx +94 -0
  357. package/src/components/tabs/tabs.tsx +170 -0
  358. package/src/components/tag-input/index.ts +1 -0
  359. package/src/components/tag-input/tag-input.stories.tsx +120 -0
  360. package/src/components/tag-input/tag-input.test.tsx +97 -0
  361. package/src/components/tag-input/tag-input.tsx +253 -0
  362. package/src/components/team-switcher/index.ts +1 -0
  363. package/src/components/team-switcher/team-switcher.test.tsx +39 -0
  364. package/src/components/team-switcher/team-switcher.tsx +100 -0
  365. package/src/components/textarea/index.ts +1 -0
  366. package/src/components/textarea/textarea.stories.tsx +42 -0
  367. package/src/components/textarea/textarea.tsx +24 -0
  368. package/src/components/theme-switcher/index.ts +2 -0
  369. package/src/components/theme-switcher/theme-switcher.stories.tsx +146 -0
  370. package/src/components/theme-switcher/theme-switcher.test.tsx +253 -0
  371. package/src/components/theme-switcher/theme-switcher.tsx +280 -0
  372. package/src/components/theme-switcher/use-theme-transition.ts +62 -0
  373. package/src/components/timeline/index.ts +13 -0
  374. package/src/components/timeline/timeline.stories.tsx +99 -0
  375. package/src/components/timeline/timeline.test.tsx +196 -0
  376. package/src/components/timeline/timeline.tsx +188 -0
  377. package/src/components/toggle/index.ts +1 -0
  378. package/src/components/toggle/toggle.stories.tsx +116 -0
  379. package/src/components/toggle/toggle.test.tsx +119 -0
  380. package/src/components/toggle/toggle.tsx +64 -0
  381. package/src/components/toggle-group/index.ts +1 -0
  382. package/src/components/toggle-group/toggle-group.stories.tsx +110 -0
  383. package/src/components/toggle-group/toggle-group.tsx +59 -0
  384. package/src/components/toolbar/index.ts +14 -0
  385. package/src/components/toolbar/toolbar.stories.tsx +126 -0
  386. package/src/components/toolbar/toolbar.test.tsx +73 -0
  387. package/src/components/toolbar/toolbar.tsx +177 -0
  388. package/src/components/tooltip/index.ts +1 -0
  389. package/src/components/tooltip/tooltip.stories.tsx +67 -0
  390. package/src/components/tooltip/tooltip.tsx +27 -0
  391. package/src/components/top-nav/index.ts +1 -0
  392. package/src/components/top-nav/top-nav.stories.tsx +18 -0
  393. package/src/components/top-nav/top-nav.tsx +34 -0
  394. package/src/components/transfer/index.ts +2 -0
  395. package/src/components/transfer/transfer.stories.tsx +162 -0
  396. package/src/components/transfer/transfer.test.tsx +177 -0
  397. package/src/components/transfer/transfer.tsx +481 -0
  398. package/src/components/tree/index.ts +6 -0
  399. package/src/components/tree/tree.stories.tsx +618 -0
  400. package/src/components/tree/tree.test.tsx +699 -0
  401. package/src/components/tree/tree.tsx +1149 -0
  402. package/src/components/tree/use-tree-keyboard.ts +314 -0
  403. package/src/components/tree-select/index.ts +1 -0
  404. package/src/components/tree-select/tree-select.stories.tsx +127 -0
  405. package/src/components/tree-select/tree-select.test.tsx +40 -0
  406. package/src/components/tree-select/tree-select.tsx +137 -0
  407. package/src/components/typography/index.ts +31 -0
  408. package/src/components/typography/prose.test.tsx +48 -0
  409. package/src/components/typography/prose.tsx +194 -0
  410. package/src/components/typography/typography.stories.tsx +244 -0
  411. package/src/components/typography/typography.test.tsx +140 -0
  412. package/src/components/typography/typography.tsx +126 -0
  413. package/src/components/view-toolbar/index.ts +10 -0
  414. package/src/components/view-toolbar/view-toolbar.stories.tsx +342 -0
  415. package/src/components/view-toolbar/view-toolbar.test.tsx +262 -0
  416. package/src/components/view-toolbar/view-toolbar.tsx +320 -0
  417. package/src/components/virtual-select/index.ts +2 -0
  418. package/src/components/virtual-select/use-virtual-listbox.ts +132 -0
  419. package/src/components/virtual-select/virtual-select.stories.tsx +203 -0
  420. package/src/components/virtual-select/virtual-select.test.tsx +106 -0
  421. package/src/components/virtual-select/virtual-select.tsx +361 -0
  422. package/src/components/wizard/index.ts +12 -0
  423. package/src/components/wizard/wizard.stories.tsx +148 -0
  424. package/src/components/wizard/wizard.test.tsx +67 -0
  425. package/src/components/wizard/wizard.tsx +354 -0
  426. package/src/decoration.stories.tsx +236 -0
  427. package/src/index.ts +174 -0
  428. package/src/lib/cn.test.ts +39 -0
  429. package/src/lib/cn.ts +31 -0
  430. package/src/lib/compact-number.ts +61 -0
  431. package/src/lib/document-address.test.ts +75 -0
  432. package/src/lib/document-address.ts +265 -0
  433. package/src/lib/download.ts +93 -0
  434. package/src/lib/file-icon.test.ts +45 -0
  435. package/src/lib/file-icon.ts +58 -0
  436. package/src/lib/file-kind.test.ts +98 -0
  437. package/src/lib/file-kind.ts +303 -0
  438. package/src/lib/file-source.test.ts +217 -0
  439. package/src/lib/file-source.ts +269 -0
  440. package/src/lib/merge-refs.test.ts +37 -0
  441. package/src/lib/merge-refs.ts +17 -0
  442. package/src/lib/streamdown-translations.ts +99 -0
  443. package/src/lib/use-copy-to-clipboard.ts +75 -0
  444. package/src/lib/use-mobile.ts +16 -0
  445. package/src/motion.stories.tsx +266 -0
  446. package/src/taste-profile.test.tsx +252 -0
  447. package/src/templates-object-detail-hub.stories.tsx +466 -0
  448. package/src/templates-screen-states.stories.tsx +306 -0
  449. package/src/templates-settings.stories.tsx +245 -0
@@ -0,0 +1,246 @@
1
+ import { createRef } from "react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { render, screen } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { Combobox } from "./combobox";
6
+ describe("Combobox", () => {
7
+ it("renders the placeholder on the trigger", () => {
8
+ render(<Combobox options={[{ label: "A", value: "a" }]} placeholder="Pick one" />);
9
+ // The trigger uses role="combobox"; that ARIA role does not take its
10
+ // accessible name from text content, so assert the visible placeholder text.
11
+ expect(screen.getByText("Pick one")).toBeInTheDocument();
12
+ });
13
+
14
+ // #343 — disabled prop
15
+ it("disables the trigger and never opens the popover when disabled", async () => {
16
+ const ref = createRef<HTMLButtonElement>();
17
+ render(
18
+ <Combobox ref={ref} disabled options={[{ label: "A", value: "a" }]} placeholder="Pick one" />,
19
+ );
20
+ const trigger = screen.getByRole("combobox");
21
+ expect(trigger).toBeDisabled();
22
+ expect(ref.current).toBe(trigger);
23
+
24
+ await userEvent.click(trigger);
25
+ expect(screen.queryByPlaceholderText("Search…")).not.toBeInTheDocument();
26
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
27
+ });
28
+
29
+ it("is excluded from tab order when disabled (native disabled semantics)", () => {
30
+ render(<Combobox disabled options={[{ label: "A", value: "a" }]} />);
31
+ const trigger = screen.getByRole("combobox");
32
+ // A native `disabled` button is always excluded from the tab order —
33
+ // no `tabIndex` gymnastics needed.
34
+ expect(trigger).toBeDisabled();
35
+ });
36
+
37
+ // #347 — accessible-name passthrough
38
+ it("names the trigger via aria-label, distinct from the selected value", () => {
39
+ render(
40
+ <Combobox
41
+ aria-label="Session switcher"
42
+ options={[{ label: "Alpha", value: "a" }]}
43
+ value="a"
44
+ />,
45
+ );
46
+ const trigger = screen.getByRole("combobox", { name: "Session switcher" });
47
+ // The value is still visible as the control's current state.
48
+ expect(trigger).toHaveTextContent("Alpha");
49
+ });
50
+
51
+ it("spreads triggerProps onto the trigger without clobbering role/aria-expanded", () => {
52
+ render(
53
+ <Combobox
54
+ options={[{ label: "A", value: "a" }]}
55
+ triggerProps={{ "data-testid": "env-combobox" }}
56
+ />,
57
+ );
58
+ const trigger = screen.getByTestId("env-combobox");
59
+ expect(trigger).toHaveAttribute("role", "combobox");
60
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
61
+ });
62
+
63
+ it("shows the selected custom value on the trigger even though it isn't in options", async () => {
64
+ render(
65
+ <Combobox allowCustomValue options={[{ label: "Alpha", value: "a" }]} value="Custom Value" />,
66
+ );
67
+ expect(screen.getByRole("combobox")).toHaveTextContent("Custom Value");
68
+ });
69
+ });
70
+
71
+ // #359 — allowCustomValue. A nested describe because these tests TYPE into the
72
+ // cmdk-backed search input, which internally calls `Element.scrollIntoView` to
73
+ // keep the auto-selected item visible — jsdom doesn't implement it, and the
74
+ // project's global vitest setup deliberately provides no stub (see
75
+ // packages/ui/vitest.setup.ts), so third-party libraries that don't
76
+ // feature-detect need a local, scoped stub (same pattern as tree.test.tsx).
77
+ describe("Combobox allowCustomValue", () => {
78
+ let originalScrollIntoView: typeof Element.prototype.scrollIntoView;
79
+
80
+ beforeEach(() => {
81
+ originalScrollIntoView = Element.prototype.scrollIntoView;
82
+ Element.prototype.scrollIntoView = vi.fn();
83
+ });
84
+
85
+ afterEach(() => {
86
+ Element.prototype.scrollIntoView = originalScrollIntoView;
87
+ });
88
+
89
+ it("accepts a typed value not present in options when allowCustomValue is set (Enter)", async () => {
90
+ const onValueChange = vi.fn();
91
+ render(
92
+ <Combobox
93
+ allowCustomValue
94
+ options={[{ label: "Alpha", value: "a" }]}
95
+ onValueChange={onValueChange}
96
+ />,
97
+ );
98
+ await userEvent.click(screen.getByRole("combobox"));
99
+ const input = await screen.findByPlaceholderText("Search…");
100
+ await userEvent.type(input, "Custom Value{Enter}");
101
+ expect(onValueChange).toHaveBeenCalledWith("Custom Value");
102
+ });
103
+
104
+ it("accepts a typed value not present in options when allowCustomValue is set (click)", async () => {
105
+ const onValueChange = vi.fn();
106
+ render(
107
+ <Combobox
108
+ allowCustomValue
109
+ options={[{ label: "Alpha", value: "a" }]}
110
+ onValueChange={onValueChange}
111
+ />,
112
+ );
113
+ await userEvent.click(screen.getByRole("combobox"));
114
+ const input = await screen.findByPlaceholderText("Search…");
115
+ await userEvent.type(input, "Canary");
116
+ const customItem = await screen.findByRole("option", { name: /use.*canary/i });
117
+ await userEvent.click(customItem);
118
+ expect(onValueChange).toHaveBeenCalledWith("Canary");
119
+ });
120
+
121
+ it("does NOT emit an unlisted value when allowCustomValue is unset (regression guard)", async () => {
122
+ const onValueChange = vi.fn();
123
+ render(<Combobox options={[{ label: "Alpha", value: "a" }]} onValueChange={onValueChange} />);
124
+ await userEvent.click(screen.getByRole("combobox"));
125
+ const input = await screen.findByPlaceholderText("Search…");
126
+ await userEvent.type(input, "Custom Value{Enter}");
127
+ expect(onValueChange).not.toHaveBeenCalled();
128
+ expect(screen.queryByRole("option", { name: /use/i })).not.toBeInTheDocument();
129
+ });
130
+
131
+ it("still filters real suggestions while allowCustomValue is set", async () => {
132
+ render(
133
+ <Combobox
134
+ allowCustomValue
135
+ options={[
136
+ { label: "Alpha", value: "a" },
137
+ { label: "Beta", value: "b" },
138
+ ]}
139
+ />,
140
+ );
141
+ await userEvent.click(screen.getByRole("combobox"));
142
+ const input = await screen.findByPlaceholderText("Search…");
143
+ await userEvent.type(input, "Alpha");
144
+ expect(await screen.findByRole("option", { name: "Alpha" })).toBeInTheDocument();
145
+ expect(screen.queryByRole("option", { name: "Beta" })).not.toBeInTheDocument();
146
+ // An exact match to an existing option must not also offer a redundant
147
+ // "Use…" custom-value entry.
148
+ expect(screen.queryByRole("option", { name: /use/i })).not.toBeInTheDocument();
149
+ });
150
+
151
+ it("keyboard: ArrowDown moves the highlight off the auto-selected top match, and Enter accepts it", async () => {
152
+ const onValueChange = vi.fn();
153
+ render(
154
+ <Combobox
155
+ allowCustomValue
156
+ options={[
157
+ { label: "Alpha", value: "alpha" },
158
+ { label: "Alphabet", value: "alphabet" },
159
+ ]}
160
+ onValueChange={onValueChange}
161
+ />,
162
+ );
163
+ await userEvent.click(screen.getByRole("combobox"));
164
+ const input = await screen.findByPlaceholderText("Search…");
165
+ // "Alpha" is an EXACT match (case-insensitive) for the typed text, so no
166
+ // custom-value item is offered — only the two real options are ranked.
167
+ await userEvent.type(input, "Alpha");
168
+ await screen.findByRole("option", { name: "Alpha" });
169
+ await screen.findByRole("option", { name: "Alphabet" });
170
+ expect(screen.queryByRole("option", { name: /use/i })).not.toBeInTheDocument();
171
+ // The exact match ("Alpha") auto-ranks first/highlighted; one ArrowDown
172
+ // moves the highlight to "Alphabet", and Enter accepts THAT option — not
173
+ // whatever was auto-selected by default.
174
+ await userEvent.keyboard("{ArrowDown}{Enter}");
175
+ expect(onValueChange).toHaveBeenCalledWith("alphabet");
176
+ });
177
+
178
+ // Regression lock: cmdk re-sorts items by score and auto-highlights the top
179
+ // one. Giving the synthetic "Use …" row the SEARCH TEXT as its `value` made
180
+ // it score a perfect 1, so it outranked every real PARTIAL match and stole
181
+ // the highlight — typing "Alp" and pressing Enter emitted "Alp" instead of
182
+ // selecting "Alpha". The row now carries a sentinel value scored just above
183
+ // zero, so it always renders but always sorts last.
184
+ it("ranks the custom-value row LAST, so Enter still selects a real partial match", async () => {
185
+ const onValueChange = vi.fn();
186
+ render(
187
+ <Combobox
188
+ allowCustomValue
189
+ options={[
190
+ { label: "Alpha", value: "alpha" },
191
+ { label: "Beta", value: "beta" },
192
+ ]}
193
+ onValueChange={onValueChange}
194
+ />,
195
+ );
196
+ await userEvent.click(screen.getByRole("combobox"));
197
+ const input = await screen.findByPlaceholderText("Search…");
198
+ // A PARTIAL match — "Alpha" is offered AND the custom row is offered.
199
+ await userEvent.type(input, "Alp");
200
+ const options = screen.getAllByRole("option");
201
+ expect(options.map((o) => o.textContent)).toEqual(["Alpha", 'Use "Alp"']);
202
+ // The real suggestion — not the custom row — is the auto-highlighted one.
203
+ expect(options[0]).toHaveAttribute("aria-selected", "true");
204
+ expect(options[1]).toHaveAttribute("aria-selected", "false");
205
+ await userEvent.keyboard("{Enter}");
206
+ expect(onValueChange).toHaveBeenCalledWith("alpha");
207
+ expect(onValueChange).not.toHaveBeenCalledWith("Alp");
208
+ });
209
+
210
+ it("still accepts the custom value from a partial match via ArrowDown + Enter", async () => {
211
+ const onValueChange = vi.fn();
212
+ render(
213
+ <Combobox
214
+ allowCustomValue
215
+ options={[{ label: "Alpha", value: "alpha" }]}
216
+ onValueChange={onValueChange}
217
+ />,
218
+ );
219
+ await userEvent.click(screen.getByRole("combobox"));
220
+ const input = await screen.findByPlaceholderText("Search…");
221
+ await userEvent.type(input, "Alp");
222
+ // Move off the auto-highlighted "Alpha" onto the trailing custom row.
223
+ await userEvent.keyboard("{ArrowDown}{Enter}");
224
+ expect(onValueChange).toHaveBeenCalledWith("Alp");
225
+ });
226
+
227
+ it("Escape closes the popover without clearing the already-selected value", async () => {
228
+ const onValueChange = vi.fn();
229
+ render(
230
+ <Combobox
231
+ allowCustomValue
232
+ options={[{ label: "Alpha", value: "a" }]}
233
+ value="a"
234
+ onValueChange={onValueChange}
235
+ />,
236
+ );
237
+ const trigger = screen.getByRole("combobox");
238
+ await userEvent.click(trigger);
239
+ const input = await screen.findByPlaceholderText("Search…");
240
+ await userEvent.type(input, "something");
241
+ await userEvent.keyboard("{Escape}");
242
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
243
+ expect(onValueChange).not.toHaveBeenCalled();
244
+ expect(trigger).toHaveTextContent("Alpha");
245
+ });
246
+ });
@@ -0,0 +1,210 @@
1
+ import { forwardRef, useState, type ComponentPropsWithoutRef } from "react";
2
+ import { defaultFilter } from "cmdk";
3
+ import { Check, ChevronsUpDown, Plus } from "lucide-react";
4
+ import { cn } from "../../lib/cn";
5
+ import { Button } from "../button";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "../popover";
7
+ import { useLocale } from "../locale-provider";
8
+ import {
9
+ Command,
10
+ CommandEmpty,
11
+ CommandGroup,
12
+ CommandInput,
13
+ CommandItem,
14
+ CommandList,
15
+ } from "../command";
16
+
17
+ export interface ComboboxOption {
18
+ label: string;
19
+ value: string;
20
+ }
21
+
22
+ export interface ComboboxProps {
23
+ options: ComboboxOption[];
24
+ value?: string;
25
+ onValueChange?: (value: string) => void;
26
+ placeholder?: string;
27
+ searchPlaceholder?: string;
28
+ /**
29
+ * Message shown when the search yields no results. Defaults to "No
30
+ * results." — or, when `allowCustomValue` is set, a message that also
31
+ * signals a custom value is acceptable.
32
+ */
33
+ emptyText?: string;
34
+ className?: string;
35
+ /** Disables the trigger and prevents the popover from opening. */
36
+ disabled?: boolean;
37
+ /**
38
+ * When `true`, typed text that doesn't match any option becomes a
39
+ * submittable value: a synthetic "Use…" suggestion is shown, and pressing
40
+ * Enter with no listed option highlighted accepts the typed text via
41
+ * `onValueChange`. Suggestions still filter as usual. Default `false`
42
+ * (selection-only, unchanged behavior) — #359.
43
+ */
44
+ allowCustomValue?: boolean;
45
+ /** `id` applied to the trigger button. */
46
+ id?: string;
47
+ /**
48
+ * The trigger's accessible name always defaults to the SELECTED VALUE (or
49
+ * the placeholder) — that tells AT what is currently chosen, never what the
50
+ * control is FOR. Pass `aria-label`/`aria-labelledby` to also name the
51
+ * control's purpose (e.g. "Session switcher").
52
+ */
53
+ "aria-label"?: string;
54
+ "aria-labelledby"?: string;
55
+ /**
56
+ * Escape hatch spread onto the trigger `Button` (after the named props
57
+ * above, so it wins if both are set) for anything not covered by a named
58
+ * prop — `data-*`, `name`, `data-testid`, etc. `role`/`aria-expanded` stay
59
+ * component-owned and can't be passed here; disabling stays on the
60
+ * top-level `disabled` prop so the popover-open guard below can't drift
61
+ * out of sync with the trigger's actual disabled state.
62
+ */
63
+ triggerProps?: Omit<
64
+ ComponentPropsWithoutRef<typeof Button>,
65
+ "role" | "aria-expanded" | "disabled"
66
+ > & {
67
+ /** Arbitrary `data-*` attributes (test hooks, analytics, …). */
68
+ [key: `data-${string}`]: string | number | boolean | undefined;
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Sentinel `value` for the synthetic "Use …" row (`allowCustomValue`).
74
+ *
75
+ * It must NOT be the search text. cmdk scores every item against the search and
76
+ * re-sorts the list by score, then auto-highlights the top one — so a row whose
77
+ * `value` IS the search text scores a perfect 1 and outranks every real partial
78
+ * match. Typing "Prod" against a "Production" option then highlighted
79
+ * `Use "Prod"`, and Enter emitted the raw string instead of selecting the
80
+ * option — the exact opposite of #359's "Enter accepts the highlighted
81
+ * suggestion, or the typed value if none is highlighted".
82
+ */
83
+ const CUSTOM_VALUE_ITEM = "__brand-ui-combobox-custom-value__";
84
+
85
+ /**
86
+ * Keeps the "Use …" row visible (score > 0) but always ranked LAST, behind
87
+ * every real suggestion, so the top match stays highlighted and Enter still
88
+ * selects it. When nothing else matches, the row is the only item and Enter
89
+ * accepts the typed value — which is the behavior #359 asks for.
90
+ */
91
+ const customValueFilter = (value: string, search: string, keywords?: string[]) =>
92
+ value === CUSTOM_VALUE_ITEM ? Number.MIN_VALUE : defaultFilter(value, search, keywords);
93
+
94
+ /** Single-select combobox (Popover + Command). Controlled or uncontrolled. */
95
+ export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Combobox(
96
+ {
97
+ options,
98
+ value,
99
+ onValueChange,
100
+ placeholder = "Select…",
101
+ searchPlaceholder = "Search…",
102
+ emptyText,
103
+ className,
104
+ disabled = false,
105
+ allowCustomValue = false,
106
+ id,
107
+ "aria-label": ariaLabel,
108
+ "aria-labelledby": ariaLabelledby,
109
+ triggerProps,
110
+ },
111
+ ref,
112
+ ) {
113
+ const { t } = useLocale();
114
+ const [open, setOpen] = useState(false);
115
+ const [internal, setInternal] = useState("");
116
+ const [search, setSearch] = useState("");
117
+ const selected = value ?? internal;
118
+ const setValue = (v: string) => (onValueChange ? onValueChange(v) : setInternal(v));
119
+ const current = options.find((o) => o.value === selected);
120
+
121
+ const trimmedSearch = search.trim();
122
+ const hasExactMatch = options.some(
123
+ (o) => o.label.trim().toLowerCase() === trimmedSearch.toLowerCase(),
124
+ );
125
+ const showCustomValueItem = allowCustomValue && trimmedSearch.length > 0 && !hasExactMatch;
126
+ const resolvedEmptyText =
127
+ emptyText ?? (allowCustomValue ? t("ui.combobox.emptyAllowCustom") : t("noResults"));
128
+
129
+ return (
130
+ <Popover
131
+ open={disabled ? false : open}
132
+ onOpenChange={(o) => {
133
+ if (disabled) return;
134
+ setOpen(o);
135
+ // Reset the search text on close, matching the previous (uncontrolled
136
+ // cmdk-internal) behavior where the popover content unmounted and its
137
+ // own internal search state reset with it.
138
+ if (!o) setSearch("");
139
+ }}
140
+ >
141
+ <PopoverTrigger asChild>
142
+ <Button
143
+ ref={ref}
144
+ variant="outline"
145
+ id={id}
146
+ aria-label={ariaLabel}
147
+ aria-labelledby={ariaLabelledby}
148
+ {...triggerProps}
149
+ role="combobox"
150
+ aria-expanded={open}
151
+ disabled={disabled}
152
+ data-slot="combobox"
153
+ className={cn("w-56 justify-between font-normal", triggerProps?.className, className)}
154
+ >
155
+ {current ? current.label : selected || placeholder}
156
+ <ChevronsUpDown className="ms-2 size-4 shrink-0 opacity-50" />
157
+ </Button>
158
+ </PopoverTrigger>
159
+ <PopoverContent
160
+ className="w-56 p-0"
161
+ data-slot="combobox-content"
162
+ // Radix Popover.Content renders role="dialog"; without a name axe's
163
+ // aria-dialog-name rule (rightly) flags it. Reuse the trigger's own
164
+ // purpose label when the consumer set one, else fall back to the
165
+ // (already-translatable) search placeholder — no new hardcoded copy.
166
+ aria-label={ariaLabel ?? searchPlaceholder}
167
+ >
168
+ <Command filter={allowCustomValue ? customValueFilter : undefined}>
169
+ <CommandInput placeholder={searchPlaceholder} value={search} onValueChange={setSearch} />
170
+ <CommandList>
171
+ <CommandEmpty>{resolvedEmptyText}</CommandEmpty>
172
+ <CommandGroup>
173
+ {options.map((o) => (
174
+ <CommandItem
175
+ key={o.value}
176
+ value={o.label}
177
+ onSelect={() => {
178
+ setValue(o.value === selected ? "" : o.value);
179
+ setOpen(false);
180
+ }}
181
+ >
182
+ <Check
183
+ className={cn(
184
+ "me-2 size-4",
185
+ selected === o.value ? "opacity-100" : "opacity-0",
186
+ )}
187
+ />
188
+ {o.label}
189
+ </CommandItem>
190
+ ))}
191
+ {showCustomValueItem ? (
192
+ <CommandItem
193
+ value={CUSTOM_VALUE_ITEM}
194
+ data-slot="combobox-custom-value"
195
+ onSelect={() => {
196
+ setValue(trimmedSearch);
197
+ setOpen(false);
198
+ }}
199
+ >
200
+ <Plus className="me-2 size-4 shrink-0 opacity-50" />
201
+ {t("ui.combobox.useCustomValue", { value: trimmedSearch })}
202
+ </CommandItem>
203
+ ) : null}
204
+ </CommandGroup>
205
+ </CommandList>
206
+ </Command>
207
+ </PopoverContent>
208
+ </Popover>
209
+ );
210
+ });
@@ -0,0 +1 @@
1
+ export { Combobox, type ComboboxProps, type ComboboxOption } from "./combobox";