@justanarthur/modo-ui 0.6.0-canary.8 → 0.6.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 (490) hide show
  1. package/README.md +66 -164
  2. package/dist/base.css +4 -141
  3. package/dist/blocks/sidebar-app/index.js +26 -40
  4. package/dist/components/AI/input-message/index.js +16 -23
  5. package/dist/components/AI/thinking-indicator/index.js +6 -9
  6. package/dist/components/accordion/index.d.ts +11 -39
  7. package/dist/components/accordion/index.js +13 -18
  8. package/dist/components/badge/index.d.ts +11 -18
  9. package/dist/components/badge/index.js +7 -12
  10. package/dist/components/button/index.d.ts +17 -53
  11. package/dist/components/button/index.js +6 -11
  12. package/dist/components/card/index.d.ts +9 -32
  13. package/dist/components/card/index.js +11 -17
  14. package/dist/components/copy/index.d.ts +8 -41
  15. package/dist/components/copy/index.js +9 -15
  16. package/dist/components/inputs/checkbox/index.d.ts +9 -28
  17. package/dist/components/inputs/checkbox/index.js +13 -18
  18. package/dist/components/inputs/color-picker/index.d.ts +14 -22
  19. package/dist/components/inputs/color-picker/index.js +18 -26
  20. package/dist/components/inputs/combobox/index.d.ts +20 -85
  21. package/dist/components/inputs/combobox/index.js +18 -27
  22. package/dist/components/inputs/date-time/calendar/index.d.ts +18 -24
  23. package/dist/components/inputs/date-time/calendar/index.js +11 -15
  24. package/dist/components/inputs/date-time/date-picker/index.d.ts +16 -105
  25. package/dist/components/inputs/date-time/date-picker/index.js +25 -35
  26. package/dist/components/inputs/date-time/date-range-picker/index.d.ts +12 -106
  27. package/dist/components/inputs/date-time/date-range-picker/index.js +24 -35
  28. package/dist/components/inputs/date-time/duration-picker/index.d.ts +13 -66
  29. package/dist/components/inputs/date-time/duration-picker/index.js +24 -33
  30. package/dist/components/inputs/date-time/month-picker/index.d.ts +10 -60
  31. package/dist/components/inputs/date-time/month-picker/index.js +23 -32
  32. package/dist/components/inputs/date-time/time-picker/index.d.ts +10 -58
  33. package/dist/components/inputs/date-time/time-picker/index.js +24 -33
  34. package/dist/components/inputs/input/index.d.ts +13 -95
  35. package/dist/components/inputs/input/index.js +9 -18
  36. package/dist/components/inputs/radio/index.d.ts +5 -20
  37. package/dist/components/inputs/radio/index.js +12 -16
  38. package/dist/components/inputs/search/index.d.ts +3 -3
  39. package/dist/components/inputs/search/index.js +18 -26
  40. package/dist/components/inputs/select/index.d.ts +23 -99
  41. package/dist/components/inputs/select/index.js +14 -29
  42. package/dist/components/inputs/slider/index.d.ts +0 -9
  43. package/dist/components/inputs/slider/index.js +5 -8
  44. package/dist/components/inputs/switch/index.d.ts +2 -8
  45. package/dist/components/inputs/switch/index.js +12 -15
  46. package/dist/components/inputs/textarea/index.d.ts +1 -20
  47. package/dist/components/inputs/textarea/index.js +6 -9
  48. package/dist/components/navigation/sidebar/index.d.ts +21 -44
  49. package/dist/components/navigation/sidebar/index.js +31 -42
  50. package/dist/components/navigation/tabs/index.d.ts +2 -8
  51. package/dist/components/navigation/tabs/index.js +10 -15
  52. package/dist/components/navigation/tabs-subtle/index.js +10 -13
  53. package/dist/components/overlays/alert-dialog/index.d.ts +16 -135
  54. package/dist/components/overlays/alert-dialog/index.js +13 -23
  55. package/dist/components/overlays/command/index.d.ts +20 -25
  56. package/dist/components/overlays/command/index.js +17 -25
  57. package/dist/components/overlays/context-menu/index.d.ts +17 -39
  58. package/dist/components/overlays/context-menu/index.js +18 -29
  59. package/dist/components/overlays/dialog/index.d.ts +16 -42
  60. package/dist/components/overlays/dialog/index.js +12 -20
  61. package/dist/components/overlays/dropdown/index.d.ts +34 -304
  62. package/dist/components/overlays/dropdown/index.js +17 -44
  63. package/dist/components/overlays/popover/index.d.ts +12 -38
  64. package/dist/components/overlays/popover/index.js +9 -13
  65. package/dist/components/overlays/preview-card/index.js +8 -12
  66. package/dist/components/overlays/sheet/index.d.ts +8 -25
  67. package/dist/components/overlays/sheet/index.js +13 -19
  68. package/dist/components/overlays/toast/index.d.ts +50 -209
  69. package/dist/components/overlays/toast/index.js +10 -23
  70. package/dist/components/overlays/tooltip/index.d.ts +21 -123
  71. package/dist/components/overlays/tooltip/index.js +6 -16
  72. package/dist/components/table/index.d.ts +8 -63
  73. package/dist/components/table/index.js +9 -20
  74. package/dist/index.d.ts +1048 -2999
  75. package/dist/index.js +82 -231
  76. package/dist/preset.d.ts +0 -22
  77. package/dist/preset.js +2 -24
  78. package/dist/primitives/code/index.js +5 -8
  79. package/dist/primitives/craft/index.js +6 -10
  80. package/dist/primitives/fluid-hover/index.js +8 -10
  81. package/dist/primitives/morph/index.js +10 -16
  82. package/dist/primitives/motion/index.js +8 -11
  83. package/dist/primitives/scroll-area/index.js +3 -5
  84. package/dist/primitives/sizes/index.js +4 -6
  85. package/dist/primitives/surface/index.js +4 -6
  86. package/dist/shared/{components-4y816hwp.js → components-0q0sqfh4.js} +49 -27
  87. package/dist/shared/components-0sn7ypjw.js +61 -0
  88. package/dist/shared/{components-vc864c51.js → components-11da9ajg.js} +5 -8
  89. package/dist/shared/components-1434p1mh.js +123 -0
  90. package/dist/shared/{components-t2a3qmy9.js → components-1z6tdadk.js} +24 -40
  91. package/dist/shared/{components-qs4rkqsj.js → components-27nnvrwe.js} +14 -15
  92. package/dist/shared/{components-av5t26rc.js → components-2dxkv6zm.js} +7 -8
  93. package/dist/shared/{components-wqrvqq5p.js → components-2ecg8svx.js} +152 -234
  94. package/dist/shared/{components-3ydr2fqe.js → components-2ph6t61s.js} +34 -37
  95. package/dist/shared/{components-6gk118w3.js → components-39jq9va1.js} +24 -29
  96. package/dist/shared/components-3ky44908.js +9 -0
  97. package/dist/shared/{components-y1qvbawa.js → components-3mkepka5.js} +25 -28
  98. package/dist/shared/{components-xqszvkj4.js → components-4700p9kz.js} +22 -24
  99. package/dist/shared/{components-yhgq0qxx.js → components-59efxjd9.js} +12 -13
  100. package/dist/shared/{components-9j9abkct.js → components-5vgpnxgy.js} +27 -51
  101. package/dist/shared/{components-8tt3xbb5.js → components-5xc2j8jz.js} +18 -19
  102. package/dist/shared/{components-jymmaevv.js → components-6p8tfcd6.js} +29 -57
  103. package/dist/shared/{components-4nck8acq.js → components-7r3etddy.js} +54 -19
  104. package/dist/shared/{components-skfd7kff.js → components-8snj490h.js} +32 -78
  105. package/dist/shared/{components-ctqmge8t.js → components-b0eac83j.js} +30 -9
  106. package/dist/shared/{components-9sttq78t.js → components-b2wyq2cq.js} +59 -41
  107. package/dist/shared/{components-vkwq2k96.js → components-b3c5et5b.js} +38 -49
  108. package/dist/shared/{components-rrbpmfx4.js → components-c6s6b43g.js} +2 -3
  109. package/dist/shared/{components-pw06rbxq.js → components-chacnk61.js} +19 -19
  110. package/dist/shared/{components-71nbt03m.js → components-cv16mryv.js} +7 -26
  111. package/dist/shared/{components-eqze3y30.js → components-d6xtjht3.js} +30 -35
  112. package/dist/shared/{components-5y5shqfd.js → components-dk0gjr5a.js} +18 -19
  113. package/dist/shared/{components-khspp26b.js → components-fzrq6tr3.js} +34 -37
  114. package/dist/shared/{components-km7fhgbk.js → components-gc5vtq2f.js} +30 -66
  115. package/dist/shared/components-gf5s6ap9.js +42 -0
  116. package/dist/shared/{components-qqcmtrbv.js → components-gz8zf3ec.js} +55 -56
  117. package/dist/shared/{components-mattt5jp.js → components-h2ajmhwn.js} +35 -107
  118. package/dist/shared/{components-awra5sff.js → components-hqeq3w9q.js} +16 -24
  119. package/dist/shared/{components-5tzhqbcw.js → components-hw9yk32z.js} +24 -36
  120. package/dist/shared/components-hyq23phv.js +310 -0
  121. package/dist/shared/{components-8mt5bsv6.js → components-jbbmdr74.js} +22 -41
  122. package/dist/shared/{components-dcc02hs2.js → components-jjw69c4c.js} +4 -7
  123. package/dist/shared/{components-2syq4mwc.js → components-jthe1pwb.js} +420 -139
  124. package/dist/shared/{components-r7q1zm7r.js → components-jwst3vtj.js} +76 -224
  125. package/dist/shared/{components-qdm3yh26.js → components-kvr7f9hy.js} +10 -19
  126. package/dist/shared/{components-s8yqk2dc.js → components-kwa6mrf9.js} +11 -46
  127. package/dist/shared/{components-de99ykt7.js → components-men63rwj.js} +2 -3
  128. package/dist/shared/{components-6b0zb8z3.js → components-mjthm1kf.js} +9 -13
  129. package/dist/shared/{components-2c64r8c9.js → components-n42c79bz.js} +22 -23
  130. package/dist/shared/{components-tz1fv35x.js → components-n7mjr357.js} +2 -3
  131. package/dist/shared/components-ng72k97e.js +128 -0
  132. package/dist/shared/{components-dyae0y7h.js → components-pbmqctfa.js} +65 -169
  133. package/dist/shared/{components-px2zm7dc.js → components-q5xj5c16.js} +8 -9
  134. package/dist/shared/components-raknhppb.js +64 -0
  135. package/dist/shared/{components-8rb25gbe.js → components-rr7a6ja9.js} +27 -59
  136. package/dist/shared/{components-5012r6gr.js → components-s083f38z.js} +34 -164
  137. package/dist/shared/components-s5ngg32c.js +267 -0
  138. package/dist/shared/components-sn1rxx1d.js +143 -0
  139. package/dist/shared/{components-r8eqs7gz.js → components-ta7shw28.js} +4 -4
  140. package/dist/shared/{components-9as3rf68.js → components-vc7vq5nz.js} +3 -6
  141. package/dist/shared/{components-ft3zdc79.js → components-vfbhxf5r.js} +5 -6
  142. package/dist/shared/components-vynjtrk6.js +19 -0
  143. package/dist/shared/{components-w0xq6q9k.js → components-w1r67ag9.js} +149 -294
  144. package/dist/shared/{components-z0bqtvcp.js → components-w90endve.js} +72 -71
  145. package/dist/shared/{components-t5pk5nmc.js → components-wvp1m52g.js} +102 -149
  146. package/dist/shared/components-wy9jpqny.js +146 -0
  147. package/dist/shared/{components-5sxjeh6h.js → components-y9y0g8er.js} +28 -52
  148. package/dist/shared/components-ypp01w3p.js +986 -0
  149. package/dist/shared/{components-t50hrswb.js → components-zfnmn5d4.js} +36 -82
  150. package/dist/shared/{components-jqpefjsn.js → components-zrkzcayz.js} +6 -54
  151. package/dist/shared/{components-vf45j12c.js → components-zx6b6yby.js} +18 -4
  152. package/dist/styles.css +32 -320
  153. package/dist/tailwind.css +0 -16
  154. package/dist/tokens.css +0 -67
  155. package/package.json +4 -227
  156. package/DESIGN.md +0 -623
  157. package/blocks/sidebar-app/examples.mdx +0 -33
  158. package/blocks/sidebar-app/index.tsx +0 -218
  159. package/blocks/sidebar-app/inset-topbar.tsx +0 -25
  160. package/blocks/sidebar-app/menu-grid.ts +0 -26
  161. package/blocks/sidebar-app/nav-data.ts +0 -36
  162. package/blocks/sidebar-app/search-field.tsx +0 -48
  163. package/blocks/sidebar-app/user-footer.tsx +0 -67
  164. package/blocks/sidebar-app/workspace-header.tsx +0 -129
  165. package/components/AI/input-message/examples.mdx +0 -130
  166. package/components/AI/input-message/file-thumbnail.tsx +0 -173
  167. package/components/AI/input-message/index.tsx +0 -1385
  168. package/components/AI/thinking-indicator/examples.mdx +0 -27
  169. package/components/AI/thinking-indicator/index.tsx +0 -210
  170. package/components/accordion/examples.mdx +0 -233
  171. package/components/accordion/index.tsx +0 -1071
  172. package/components/avatar/examples.mdx +0 -95
  173. package/components/avatar/index.tsx +0 -129
  174. package/components/badge/change-demo.tsx +0 -29
  175. package/components/badge/examples.mdx +0 -124
  176. package/components/badge/index.tsx +0 -225
  177. package/components/badge/variants.ts +0 -73
  178. package/components/button/examples.mdx +0 -168
  179. package/components/button/index.tsx +0 -424
  180. package/components/button/loading-demo.tsx +0 -25
  181. package/components/button/variants.ts +0 -168
  182. package/components/card/examples.mdx +0 -251
  183. package/components/card/index.tsx +0 -1070
  184. package/components/copy/examples.mdx +0 -125
  185. package/components/copy/index.tsx +0 -662
  186. package/components/inputs/checkbox/examples.mdx +0 -87
  187. package/components/inputs/checkbox/index.tsx +0 -611
  188. package/components/inputs/checkbox/select-all-demo.tsx +0 -48
  189. package/components/inputs/color-picker/examples.mdx +0 -111
  190. package/components/inputs/color-picker/index.tsx +0 -1981
  191. package/components/inputs/color-picker/variants.ts +0 -401
  192. package/components/inputs/combobox/examples.mdx +0 -366
  193. package/components/inputs/combobox/index.tsx +0 -1527
  194. package/components/inputs/combobox/remote-demo.tsx +0 -105
  195. package/components/inputs/date-time/calendar/examples.mdx +0 -45
  196. package/components/inputs/date-time/calendar/index.tsx +0 -370
  197. package/components/inputs/date-time/calendar/use-month-window.ts +0 -173
  198. package/components/inputs/date-time/date-picker/examples.mdx +0 -243
  199. package/components/inputs/date-time/date-picker/index.tsx +0 -476
  200. package/components/inputs/date-time/date-picker/masked-date-input.tsx +0 -348
  201. package/components/inputs/date-time/date-range-picker/examples.mdx +0 -93
  202. package/components/inputs/date-time/date-range-picker/index.tsx +0 -525
  203. package/components/inputs/date-time/duration-picker/examples.mdx +0 -40
  204. package/components/inputs/date-time/duration-picker/index.tsx +0 -253
  205. package/components/inputs/date-time/month-picker/examples.mdx +0 -45
  206. package/components/inputs/date-time/month-picker/index.tsx +0 -254
  207. package/components/inputs/date-time/picker-shell.tsx +0 -581
  208. package/components/inputs/date-time/time-columns.tsx +0 -94
  209. package/components/inputs/date-time/time-picker/examples.mdx +0 -57
  210. package/components/inputs/date-time/time-picker/index.tsx +0 -227
  211. package/components/inputs/field/examples.mdx +0 -117
  212. package/components/inputs/field/index.tsx +0 -268
  213. package/components/inputs/file-dropzone/examples.mdx +0 -83
  214. package/components/inputs/file-dropzone/file-row.tsx +0 -201
  215. package/components/inputs/file-dropzone/index.tsx +0 -316
  216. package/components/inputs/file-dropzone/upload-demo.tsx +0 -67
  217. package/components/inputs/file-dropzone/variants.ts +0 -94
  218. package/components/inputs/input/examples.mdx +0 -166
  219. package/components/inputs/input/index.tsx +0 -494
  220. package/components/inputs/input/password-demo.tsx +0 -30
  221. package/components/inputs/label/examples.mdx +0 -67
  222. package/components/inputs/label/index.tsx +0 -85
  223. package/components/inputs/number-input/examples.mdx +0 -69
  224. package/components/inputs/number-input/index.tsx +0 -221
  225. package/components/inputs/radio/examples.mdx +0 -77
  226. package/components/inputs/radio/index.tsx +0 -636
  227. package/components/inputs/search/examples.mdx +0 -74
  228. package/components/inputs/search/index.tsx +0 -333
  229. package/components/inputs/select/examples.mdx +0 -296
  230. package/components/inputs/select/index.tsx +0 -1044
  231. package/components/inputs/select/variants.ts +0 -32
  232. package/components/inputs/slider/examples.mdx +0 -153
  233. package/components/inputs/slider/index.tsx +0 -1867
  234. package/components/inputs/switch/examples.mdx +0 -96
  235. package/components/inputs/switch/index.tsx +0 -504
  236. package/components/inputs/textarea/examples.mdx +0 -126
  237. package/components/inputs/textarea/index.tsx +0 -207
  238. package/components/navigation/navigation-menu/examples.mdx +0 -136
  239. package/components/navigation/navigation-menu/index.tsx +0 -704
  240. package/components/navigation/pagination/examples.mdx +0 -74
  241. package/components/navigation/pagination/index.tsx +0 -401
  242. package/components/navigation/sidebar/examples.mdx +0 -325
  243. package/components/navigation/sidebar/index.tsx +0 -453
  244. package/components/navigation/sidebar/sidebar-core.tsx +0 -1408
  245. package/components/navigation/sidebar/sidebar-menu.tsx +0 -1495
  246. package/components/navigation/sidebar/sidebar.css +0 -74
  247. package/components/navigation/sidebar/variants.ts +0 -55
  248. package/components/navigation/tabs/examples.mdx +0 -89
  249. package/components/navigation/tabs/index.tsx +0 -494
  250. package/components/navigation/tabs-subtle/examples.mdx +0 -49
  251. package/components/navigation/tabs-subtle/index.tsx +0 -512
  252. package/components/overlays/alert-dialog/examples.mdx +0 -123
  253. package/components/overlays/alert-dialog/index.tsx +0 -276
  254. package/components/overlays/alert-dialog/pending-demo.tsx +0 -36
  255. package/components/overlays/command/examples.mdx +0 -113
  256. package/components/overlays/command/index.tsx +0 -425
  257. package/components/overlays/context-menu/examples.mdx +0 -56
  258. package/components/overlays/context-menu/index.tsx +0 -279
  259. package/components/overlays/dialog/examples.mdx +0 -165
  260. package/components/overlays/dialog/index.tsx +0 -451
  261. package/components/overlays/dropdown/dropdown-search.tsx +0 -395
  262. package/components/overlays/dropdown/examples.mdx +0 -444
  263. package/components/overlays/dropdown/index.tsx +0 -1241
  264. package/components/overlays/dropdown/menu-item.tsx +0 -510
  265. package/components/overlays/dropdown/menu-parts.tsx +0 -725
  266. package/components/overlays/popover/anchored-demo.tsx +0 -59
  267. package/components/overlays/popover/examples.mdx +0 -203
  268. package/components/overlays/popover/index.tsx +0 -317
  269. package/components/overlays/preview-card/examples.mdx +0 -61
  270. package/components/overlays/preview-card/index.tsx +0 -204
  271. package/components/overlays/sheet/examples.mdx +0 -101
  272. package/components/overlays/sheet/index.tsx +0 -307
  273. package/components/overlays/toast/examples.mdx +0 -85
  274. package/components/overlays/toast/global-demo.tsx +0 -72
  275. package/components/overlays/toast/index.tsx +0 -544
  276. package/components/overlays/toast/manager.ts +0 -193
  277. package/components/overlays/tooltip/examples.mdx +0 -106
  278. package/components/overlays/tooltip/index.tsx +0 -539
  279. package/components/progress/examples.mdx +0 -59
  280. package/components/progress/index.tsx +0 -174
  281. package/components/progress/progress.css +0 -49
  282. package/components/progress/step-demo.tsx +0 -27
  283. package/components/table/examples.mdx +0 -188
  284. package/components/table/index.tsx +0 -535
  285. package/components/table/sort-demo.tsx +0 -61
  286. package/dist/components/avatar/index.d.ts +0 -46
  287. package/dist/components/avatar/index.js +0 -19
  288. package/dist/components/badge/variants.d.ts +0 -33
  289. package/dist/components/badge/variants.js +0 -10
  290. package/dist/components/button/variants.d.ts +0 -39
  291. package/dist/components/button/variants.js +0 -8
  292. package/dist/components/inputs/color-picker/variants.d.ts +0 -61
  293. package/dist/components/inputs/color-picker/variants.js +0 -28
  294. package/dist/components/inputs/field/index.d.ts +0 -116
  295. package/dist/components/inputs/field/index.js +0 -29
  296. package/dist/components/inputs/file-dropzone/index.d.ts +0 -167
  297. package/dist/components/inputs/file-dropzone/index.js +0 -33
  298. package/dist/components/inputs/file-dropzone/variants.d.ts +0 -35
  299. package/dist/components/inputs/file-dropzone/variants.js +0 -12
  300. package/dist/components/inputs/label/index.d.ts +0 -32
  301. package/dist/components/inputs/label/index.js +0 -11
  302. package/dist/components/inputs/number-input/index.d.ts +0 -87
  303. package/dist/components/inputs/number-input/index.js +0 -21
  304. package/dist/components/inputs/select/variants.d.ts +0 -5
  305. package/dist/components/inputs/select/variants.js +0 -6
  306. package/dist/components/navigation/navigation-menu/index.d.ts +0 -126
  307. package/dist/components/navigation/navigation-menu/index.js +0 -41
  308. package/dist/components/navigation/pagination/index.d.ts +0 -75
  309. package/dist/components/navigation/pagination/index.js +0 -41
  310. package/dist/components/navigation/sidebar/variants.d.ts +0 -25
  311. package/dist/components/navigation/sidebar/variants.js +0 -28
  312. package/dist/components/progress/index.d.ts +0 -56
  313. package/dist/components/progress/index.js +0 -15
  314. package/dist/docs/index.d.ts +0 -83
  315. package/dist/docs/index.js +0 -288
  316. package/dist/docs/vite.d.ts +0 -31
  317. package/dist/docs/vite.js +0 -45
  318. package/dist/lib/date-locale.d.ts +0 -26
  319. package/dist/lib/date-locale.js +0 -24
  320. package/dist/lib/field-classes.d.ts +0 -149
  321. package/dist/lib/field-classes.js +0 -41
  322. package/dist/lib/field-variants.d.ts +0 -47
  323. package/dist/lib/field-variants.js +0 -26
  324. package/dist/lib/fluid-hover-highlight.d.ts +0 -112
  325. package/dist/lib/fluid-hover-highlight.js +0 -17
  326. package/dist/lib/focus-ring.d.ts +0 -18
  327. package/dist/lib/focus-ring.js +0 -11
  328. package/dist/lib/goo-indicator.d.ts +0 -46
  329. package/dist/lib/goo-indicator.js +0 -14
  330. package/dist/lib/icon-context.d.ts +0 -30
  331. package/dist/lib/icon-context.js +0 -12
  332. package/dist/lib/inert.d.ts +0 -9
  333. package/dist/lib/inert.js +0 -8
  334. package/dist/lib/morph-layers.d.ts +0 -93
  335. package/dist/lib/morph-layers.js +0 -13
  336. package/dist/lib/morph-part.d.ts +0 -21
  337. package/dist/lib/morph-part.js +0 -12
  338. package/dist/lib/popup.d.ts +0 -20
  339. package/dist/lib/popup.js +0 -12
  340. package/dist/lib/reduced-motion.d.ts +0 -9
  341. package/dist/lib/reduced-motion.js +0 -6
  342. package/dist/lib/row-index.d.ts +0 -10
  343. package/dist/lib/row-index.js +0 -8
  344. package/dist/lib/row-order.d.ts +0 -28
  345. package/dist/lib/row-order.js +0 -10
  346. package/dist/lib/scroll-column.d.ts +0 -31
  347. package/dist/lib/scroll-column.js +0 -19
  348. package/dist/lib/shape-context.d.ts +0 -30
  349. package/dist/lib/shape-context.js +0 -13
  350. package/dist/lib/shape-radius.d.ts +0 -19
  351. package/dist/lib/shape-radius.js +0 -6
  352. package/dist/lib/size-context.d.ts +0 -94
  353. package/dist/lib/size-context.js +0 -20
  354. package/dist/lib/size-scale.d.ts +0 -79
  355. package/dist/lib/size-scale.js +0 -8
  356. package/dist/lib/slot.d.ts +0 -39
  357. package/dist/lib/slot.js +0 -14
  358. package/dist/lib/springs.d.ts +0 -44
  359. package/dist/lib/springs.js +0 -8
  360. package/dist/lib/surface-classes.d.ts +0 -19
  361. package/dist/lib/surface-classes.js +0 -10
  362. package/dist/lib/surface-context.d.ts +0 -9
  363. package/dist/lib/surface-context.js +0 -8
  364. package/dist/lib/use-controllable-state.d.ts +0 -2
  365. package/dist/lib/use-controllable-state.js +0 -6
  366. package/dist/lib/use-fluid-hover.d.ts +0 -138
  367. package/dist/lib/use-fluid-hover.js +0 -14
  368. package/dist/lib/use-is-mobile.d.ts +0 -2
  369. package/dist/lib/use-is-mobile.js +0 -6
  370. package/dist/lib/use-keyboard-nav-gate.d.ts +0 -20
  371. package/dist/lib/use-keyboard-nav-gate.js +0 -7
  372. package/dist/lib/use-merge-split.d.ts +0 -47
  373. package/dist/lib/use-merge-split.js +0 -15
  374. package/dist/lib/use-morph.d.ts +0 -101
  375. package/dist/lib/use-morph.js +0 -20
  376. package/dist/lib/use-touch-primary.d.ts +0 -2
  377. package/dist/lib/use-touch-primary.js +0 -6
  378. package/dist/lib/utils.d.ts +0 -3
  379. package/dist/lib/utils.js +0 -6
  380. package/dist/primitives/separator/index.d.ts +0 -25
  381. package/dist/primitives/separator/index.js +0 -9
  382. package/dist/primitives/skeleton/index.d.ts +0 -35
  383. package/dist/primitives/skeleton/index.js +0 -13
  384. package/dist/shared/components-124epb7f.js +0 -53
  385. package/dist/shared/components-1kab3j44.js +0 -337
  386. package/dist/shared/components-26snnbs9.js +0 -322
  387. package/dist/shared/components-34txk9rj.js +0 -35
  388. package/dist/shared/components-3h92d94m.js +0 -23
  389. package/dist/shared/components-4vfm04wq.js +0 -82
  390. package/dist/shared/components-54nq988h.js +0 -123
  391. package/dist/shared/components-7s7pykk5.js +0 -281
  392. package/dist/shared/components-7zkhz3r8.js +0 -399
  393. package/dist/shared/components-8v644a0s.js +0 -298
  394. package/dist/shared/components-a4evvq8h.js +0 -271
  395. package/dist/shared/components-adkarj8n.js +0 -35
  396. package/dist/shared/components-b1e239g3.js +0 -28
  397. package/dist/shared/components-b94t6qtj.js +0 -143
  398. package/dist/shared/components-c9yr4ed1.js +0 -44
  399. package/dist/shared/components-cwg1d0w6.js +0 -254
  400. package/dist/shared/components-d3shbwqq.js +0 -95
  401. package/dist/shared/components-dhgk6mbt.js +0 -16
  402. package/dist/shared/components-drxrtnp2.js +0 -25
  403. package/dist/shared/components-ej4r7wmj.js +0 -482
  404. package/dist/shared/components-f7h57m6t.js +0 -59
  405. package/dist/shared/components-fejdzvqc.js +0 -448
  406. package/dist/shared/components-gx4h5rbb.js +0 -40
  407. package/dist/shared/components-gyt7my41.js +0 -328
  408. package/dist/shared/components-h6p86be3.js +0 -19
  409. package/dist/shared/components-h83az0bz.js +0 -33
  410. package/dist/shared/components-hdzrqqpx.js +0 -55
  411. package/dist/shared/components-krtx3m5b.js +0 -84
  412. package/dist/shared/components-kswxnm3h.js +0 -24
  413. package/dist/shared/components-mpa2y6x8.js +0 -277
  414. package/dist/shared/components-mrbaxt8q.js +0 -62
  415. package/dist/shared/components-pt5ftm4p.js +0 -38
  416. package/dist/shared/components-rpw4md2b.js +0 -179
  417. package/dist/shared/components-rtadhd9j.js +0 -64
  418. package/dist/shared/components-tz4gaf1v.js +0 -23
  419. package/dist/shared/components-wbbckbw8.js +0 -34
  420. package/dist/shared/components-xdqmt6ed.js +0 -20
  421. package/dist/shared/components-xjkdvnbg.js +0 -107
  422. package/dist/shared/components-z3v0n0de.js +0 -11
  423. package/dist/shared/components-z6dfqtyt.js +0 -1545
  424. package/dist/tokens/colors.css +0 -230
  425. package/dist/tokens/motion.css +0 -40
  426. package/dist/tokens/radius.css +0 -10
  427. package/dist/tokens/spacing.css +0 -9
  428. package/dist/tokens/typography.css +0 -119
  429. package/docs/chrome.css +0 -120
  430. package/docs/chrome.tsx +0 -283
  431. package/docs/index.ts +0 -9
  432. package/docs/vite.ts +0 -85
  433. package/index.ts +0 -60
  434. package/lib/date-locale.ts +0 -195
  435. package/lib/field-classes.ts +0 -121
  436. package/lib/field-variants.ts +0 -117
  437. package/lib/fluid-hover-highlight.tsx +0 -139
  438. package/lib/focus-ring.tsx +0 -46
  439. package/lib/goo-indicator.tsx +0 -261
  440. package/lib/icon-context.tsx +0 -310
  441. package/lib/inert.ts +0 -22
  442. package/lib/morph-layers.tsx +0 -102
  443. package/lib/morph-part.tsx +0 -49
  444. package/lib/popup.ts +0 -52
  445. package/lib/reduced-motion.ts +0 -19
  446. package/lib/row-index.tsx +0 -29
  447. package/lib/row-order.tsx +0 -126
  448. package/lib/scroll-column.tsx +0 -273
  449. package/lib/shape-context.tsx +0 -138
  450. package/lib/shape-radius.ts +0 -67
  451. package/lib/size-context.tsx +0 -66
  452. package/lib/size-scale.ts +0 -115
  453. package/lib/slot.ts +0 -115
  454. package/lib/springs.ts +0 -62
  455. package/lib/surface-classes.ts +0 -58
  456. package/lib/surface-context.tsx +0 -21
  457. package/lib/use-controllable-state.ts +0 -44
  458. package/lib/use-fluid-hover.ts +0 -541
  459. package/lib/use-is-mobile.ts +0 -25
  460. package/lib/use-keyboard-nav-gate.ts +0 -45
  461. package/lib/use-merge-split.tsx +0 -209
  462. package/lib/use-morph.ts +0 -557
  463. package/lib/use-touch-primary.tsx +0 -43
  464. package/lib/utils.ts +0 -37
  465. package/preset.ts +0 -199
  466. package/primitives/code/examples.mdx +0 -27
  467. package/primitives/code/index.tsx +0 -80
  468. package/primitives/craft/demos.tsx +0 -173
  469. package/primitives/craft/examples.mdx +0 -142
  470. package/primitives/craft/index.tsx +0 -76
  471. package/primitives/fluid-hover/examples.mdx +0 -84
  472. package/primitives/fluid-hover/index.tsx +0 -316
  473. package/primitives/morph/examples.mdx +0 -139
  474. package/primitives/morph/index.tsx +0 -192
  475. package/primitives/motion/examples.mdx +0 -100
  476. package/primitives/motion/index.tsx +0 -160
  477. package/primitives/scroll-area/examples.mdx +0 -58
  478. package/primitives/scroll-area/index.tsx +0 -204
  479. package/primitives/separator/examples.mdx +0 -30
  480. package/primitives/separator/index.tsx +0 -53
  481. package/primitives/sizes/examples.mdx +0 -167
  482. package/primitives/sizes/index.tsx +0 -89
  483. package/primitives/skeleton/examples.mdx +0 -51
  484. package/primitives/skeleton/index.tsx +0 -72
  485. package/primitives/skeleton/skeleton.css +0 -15
  486. package/primitives/surface/examples.mdx +0 -173
  487. package/primitives/surface/index.tsx +0 -109
  488. package/primitives/surface/surface.css +0 -176
  489. /package/dist/shared/{components-3ey0hzpb.js → components-1v7rxytx.js} +0 -0
  490. /package/dist/shared/{components-jcnjay5g.js → components-ezxhrmer.js} +0 -0
@@ -1,1527 +0,0 @@
1
- // biome-ignore-all lint: vendored upstream code keeps its own patterns (see the header)
2
- /*
3
- * Ported from Fluid Functionalism — github.com/mickadesign/fluid-functionalism
4
- * `registry/base/combobox.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
5
- * MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
6
- * Local modifications:
7
- * - `"use client"` dropped (no RSC here).
8
- * - Imports rewritten: `@/lib/*` → `../../../lib/*` (`SizeProvider` →
9
- * `../../../primitives/sizes`), `@/hooks/*` → `../../../lib/*`,
10
- * `@/lib/elevated` → `../../../primitives/surface`, `@/components/ui/scroll-area` →
11
- * `../../../primitives/scroll-area`, `@/components/ui/fluid-hover-highlight` →
12
- * `../../../lib/fluid-hover-highlight`, `framer-motion` → `motion/react`.
13
- * - `React.*` type references replaced with named type imports (no React namespace import).
14
- * - The root's generics (`<T extends ComboboxItemData, Multiple extends boolean>`) dropped:
15
- * modo's parser needs a plain (non-generic) function declaration for the root plus a
16
- * same-file `interface ComboboxProps`. Item objects gained an index signature so rows can still
17
- * carry extra fields (`icon`, …), `value`/`defaultValue` are `string | string[]`, and
18
- * `onValueChange` is a union of the two handler shapes so `setState` still assigns.
19
- * `ComboboxValue<Multiple>` is still exported for callers that spell the value out.
20
- * - Uncontrolled create mode added: `creatable` offers the create row with no callback and
21
- * the component keeps the items it makes (merged into `items`, deduped by value), so a
22
- * combobox can grow its own list. `onCreate` is unchanged and still wins when it returns
23
- * an item.
24
- * - `noUncheckedIndexedAccess`: the single-mode checked rect guards `checkedIndices[0]`.
25
- * - modo item: TSDoc (FF docs page + API tables), statics `Combobox.Input` / `.Chips` /
26
- * `.Content` / `.List` / `.Item` / `.Empty` assigned on the root, upstream named exports
27
- * kept, `export default Combobox`.
28
- * - Styling reads DS tokens (AGENTS.md styling): `text-[Npx]` →
29
- * `text-<role>[-compact]`; the hex focus-ring fallback →
30
- * `ring-focus-ring` / `border-focus-ring`; `duration-80|120|160` and
31
- * tier-length JS durations → `duration-<tier>` / `spring.*`.
32
- * - The popup morphs out of the field through the shared morph layer
33
- * (`lib/use-morph.ts` + `MorphSurface`, goo by default on
34
- * `spring.goo`; `Combobox.Content` takes `from` / `effect` /
35
- * `hideSource` / `tier`). Whatever opened it (typing, a press on the input
36
- * or the chevron), the source is the field the list anchors to. The
37
- * `scaleY` motion wrapper and the `actionsRef` deferred unmount (with its
38
- * fallback timer) are gone: the morph holds Base UI's unmount itself. The
39
- * popup paints its level through the morph's surface layers instead of
40
- * `render={<Elevated/>}`, re-providing it with `SurfaceProvider`.
41
- * - `effect="morph"` with `hideSource` opens the list in place over its
42
- * trigger: the positioner's `sideOffset` becomes the engine's `inPlaceOffset`.
43
- * - The fields' ladder is the shared `lib/field-classes.ts` recipe (the local
44
- * `fieldVariants` cva moved there, still exported here as
45
- * `comboboxFieldVariants`; its default is now `borderless`, the fields pass
46
- * `bordered`): `error` stamps `data-invalid` instead of overriding the ring
47
- * per state, so an invalid field also tints its lit fill; the leading icon
48
- * darkens and thickens on hover too, not only on focus. Both fields build
49
- * their box with the shared `useFieldBox` (ladder in the variant, state
50
- * attributes, radius, inset), as every text field does; `variant` is typed
51
- * `FieldVariant` (no `null`).
52
- * - Hooks for remote data (local): `loading` (the chevron turns into a
53
- * spinner, a status row under the list, the empty message held back),
54
- * a controlled query (`inputValue` / `defaultInputValue` /
55
- * `onInputValueChange`), `filter={null}` to keep every item the caller
56
- * hands in, `readOnly`, a controlled popup (`open` / `defaultOpen` /
57
- * `onOpenChange`), a `footer` under the list (`Combobox.Content`), and
58
- * object items whose `label` is any node with a `textValue` to filter and
59
- * fill the field by.
60
- * - Field parity (local): the fields read a surrounding Field's invalid state
61
- * (`error` still forces it; `invalid` is a prop too, and `aria-invalid`
62
- * follows the resolved state), `hideTrigger` drops the chevron button,
63
- * `labels` on the root translates the "Open" / "Clear" / "Remove …" button
64
- * names and the default "Search…" placeholder, and `onInputValueChange`
65
- * hears Base UI's change details (`reason`) as a second argument.
66
- */
67
-
68
- import { Combobox as ComboboxPrimitive, type ComboboxRootChangeEventDetails } from '@base-ui/react/combobox'
69
- import { AnimatePresence, animate, motion, useMotionValue } from 'motion/react'
70
- import {
71
- createContext,
72
- forwardRef,
73
- type HTMLAttributes,
74
- type InputHTMLAttributes,
75
- type MutableRefObject,
76
- type ReactNode,
77
- type RefObject,
78
- useCallback,
79
- useContext,
80
- useEffect,
81
- useImperativeHandle,
82
- useMemo,
83
- useRef,
84
- useState,
85
- } from 'react'
86
- import { type FieldVariant, fieldIconClasses, fieldVariants, useFieldBox } from '../../../lib/field-classes'
87
- import { FluidHoverHighlight } from '../../../lib/fluid-hover-highlight'
88
- import { type IconComponent, useIcon } from '../../../lib/icon-context'
89
- import { MorphSurface } from '../../../lib/morph-layers'
90
- import { isDisabledRow, popupScrollAreaClass, popupViewportClass } from '../../../lib/popup'
91
- import { useReduceMotion } from '../../../lib/reduced-motion'
92
- import { shapeMap, useShape } from '../../../lib/shape-context'
93
- import { type SizeVariant, useSize } from '../../../lib/size-context'
94
- import { spring } from '../../../lib/springs'
95
- import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
96
- import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
97
- import { useControllableState } from '../../../lib/use-controllable-state'
98
- import { useFluidHover, useRegisterFluidHoverItem } from '../../../lib/use-fluid-hover'
99
- import { SelectionBackgrounds, useMergeSplitBlocks, useSelectionRuns } from '../../../lib/use-merge-split'
100
- import { inPlaceOffset, type MorphOrigin, opensInPlace, useMorph, useMorphOrigin } from '../../../lib/use-morph'
101
- import { cn } from '../../../lib/utils'
102
- import { ScrollArea } from '../../../primitives/scroll-area'
103
- import { SizeProvider } from '../../../primitives/sizes'
104
-
105
- // ---------------------------------------------------------------------------
106
- // Combobox
107
- //
108
- // A text field that filters a list as you type. Built on Base UI's Combobox
109
- // primitive, which owns the filtering, the combobox/listbox ARIA wiring
110
- // (aria-activedescendant — the input keeps focus while arrows move a
111
- // highlight through the rows), positioning, dismissal, and the hidden form
112
- // input. This layer keeps the fluid-hover overlays, the morph open/close
113
- // (lib/use-morph.ts), and the animated checkmark — the same visuals as
114
- // Select.
115
- //
116
- // Items are data: pass `items` to the root and render rows from the
117
- // ComboboxList function child. String items are their own value and label;
118
- // object items carry `{ value, label }` plus anything else you need.
119
- // ---------------------------------------------------------------------------
120
-
121
- /** An object item: `value` and `label`, plus whatever the rows need. */
122
- interface ComboboxItemObject {
123
- value: string
124
- /** What the row and the chip show: text, or any node with a `textValue`. */
125
- label: ReactNode
126
- /** The item as text: what the query filters and the field shows. Defaults to `label` when it is a string, else `value`. */
127
- textValue?: string
128
- [key: string]: unknown
129
- }
130
-
131
- type ComboboxItemData = string | ComboboxItemObject
132
-
133
- function itemValue(item: ComboboxItemData): string {
134
- return typeof item === 'string' ? item : item.value
135
- }
136
-
137
- /** The item as text: what filters, fills the field and names a chip. */
138
- function itemLabel(item: ComboboxItemData): string {
139
- if (typeof item === 'string') return item
140
- return item.textValue ?? (typeof item.label === 'string' ? item.label : item.value)
141
- }
142
-
143
- /** The item as shown: its label node. */
144
- function itemNode(item: ComboboxItemData): ReactNode {
145
- return typeof item === 'string' ? item : item.label
146
- }
147
-
148
- /** Every fixed word the field's buttons announce, for translating it. */
149
- interface ComboboxLabels {
150
- /** Names the chevron button. Defaults to `"Open"`. */
151
- open?: string
152
- /** Names the clear ✕ of a `clearable` field. Defaults to `"Clear"`. */
153
- clear?: string
154
- /** Names a chip's remove ✕ from the chip's text: `(label) => …`. Defaults to ``(label) => `Remove ${label}` ``. */
155
- remove?: (label: string) => string
156
- /** The fields' placeholder while nothing is typed or selected, unless a field sets its own. Defaults to `"Search…"`. */
157
- placeholder?: string
158
- }
159
-
160
- /** What `Combobox.Content`'s footer gets when it is a function. */
161
- interface ComboboxFooterContext {
162
- /** Closes the popup. */
163
- close: () => void
164
- /** The trimmed query. */
165
- query: string
166
- }
167
-
168
- /** A node, or a function of the popup's state, rendered under the list. */
169
- type ComboboxFooter = ReactNode | ((context: ComboboxFooterContext) => ReactNode)
170
-
171
- // The create row is one more item in the list the primitive filters and
172
- // highlights, so Enter and the arrows reach it like any row. Its value can't
173
- // collide with a consumer's; its label is the query it would create.
174
- const CREATE_VALUE = '\0create'
175
-
176
- function isCreateItem(item: ComboboxItemData): boolean {
177
- return itemValue(item) === CREATE_VALUE
178
- }
179
-
180
- function defaultCreateLabel(query: string): ReactNode {
181
- return `Create “${query}”`
182
- }
183
-
184
- type ComboboxValue<Multiple extends boolean> = Multiple extends true ? string[] : string
185
-
186
- interface ComboboxContextValue {
187
- /** Selected values — one entry in single mode, any number in multiple. */
188
- values: string[]
189
- multiple: boolean
190
- inputValue: string
191
- open: boolean
192
- /** What opened the popup; its trigger is always the field (`anchorRef`). */
193
- origin: RefObject<MorphOrigin>
194
- /** The field (input group or chips container) the popup anchors to.
195
- * `RefObject<HTMLDivElement>` (not `… | null`) so the ref prop on Base UI's
196
- * InputGroup/Chips accepts it under @types/react 18. */
197
- anchorRef: RefObject<HTMLDivElement>
198
- disabled: boolean
199
- itemsByValue: Map<string, ComboboxItemData>
200
- /** The create row's label for the current query; null while no row is
201
- * offered. */
202
- createRow: ReactNode | null
203
- /** `hideSelected` has emptied the list with nothing typed: every item is
204
- * a chip already. */
205
- allSelected: boolean
206
- /** Local: items are on their way; the empty message waits. */
207
- loading: boolean
208
- /** Local: the loading row's text. */
209
- loadingLabel: ReactNode
210
- /** Local: shows the picks but takes no input. */
211
- readOnly: boolean
212
- /** Local: opens or closes the popup (the footer's `close`). */
213
- setOpen: (open: boolean) => void
214
- /** Local: the translated button names and default placeholder. */
215
- labels: ComboboxLabels
216
- }
217
-
218
- const NO_LABELS: ComboboxLabels = {}
219
-
220
- const ComboboxContext = createContext<ComboboxContextValue | null>(null)
221
-
222
- /** Highlighted row (the primitive's active index over the rendered rows)
223
- * and whether a keyboard/auto highlight put it there — pointer highlights
224
- * are left to fluid hover. Its own context so a per-pointer highlight
225
- * re-renders the list, not the field and every row. */
226
- interface Highlight {
227
- index: number
228
- keyboard: boolean
229
- }
230
- const ComboboxHighlightContext = createContext<Highlight | null>(null)
231
-
232
- function useComboboxContext() {
233
- const ctx = useContext(ComboboxContext)
234
- if (!ctx) throw new Error('Combobox compound components must be inside <Combobox>')
235
- return ctx
236
- }
237
-
238
- // Content context for fluid hover
239
- interface ComboboxContentContextValue {
240
- registerItem: (index: number, element: HTMLElement | null) => void
241
- activeIndex: number | null
242
- }
243
-
244
- const ComboboxContentContext = createContext<ComboboxContentContextValue | null>(null)
245
-
246
- // Each rendered row learns its index from the list, so consumers never
247
- // number rows by hand.
248
- const ComboboxItemIndexContext = createContext<number>(0)
249
-
250
- // ---------------------------------------------------------------------------
251
- // Combobox (root)
252
- // ---------------------------------------------------------------------------
253
-
254
- interface ComboboxProps {
255
- /** `Combobox.Input` or `Combobox.Chips`, plus `Combobox.Content`. */
256
- children: ReactNode
257
- /** The options. A string is its own value and label; an object carries `value`, `label`, and whatever your rows need. */
258
- items: readonly ComboboxItemData[]
259
- /** Any number of picks. Values become `string[]`; use `Combobox.Chips` for the field. Defaults to `false`. */
260
- multiple?: boolean
261
- /** Selected value. `""` means none; an array when `multiple`. Controlled — pair it with `onValueChange`. */
262
- value?: string | string[]
263
- /** Selection on first render when uncontrolled. Defaults to none. */
264
- defaultValue?: string | string[]
265
- /** Called with the pick, `""` when cleared, or the array when `multiple`. */
266
- onValueChange?: ((value: string) => void) | ((value: string[]) => void)
267
- /** Match an item against the query, or `null` to show every item as given (filtered elsewhere, e.g. on a server). Defaults to a case-insensitive "contains" on the item's text. */
268
- filter?: ((item: ComboboxItemData, query: string) => boolean) | null
269
- /** Controlled query, the field's text. Pair it with `onInputValueChange`. */
270
- inputValue?: string
271
- /** Initial query, for an uncontrolled field. Defaults to the picked item's text in single mode, else `""`. */
272
- defaultInputValue?: string
273
- /** Called with the field's text on every keystroke, and when a pick or a close rewrites it. The second argument is Base UI's change details: `reason` tells typing (`"input-change"`) from a pick, a clear or a close rewriting the text (`"item-press"`, `"clear-press"`, `"none"`, …). */
274
- onInputValueChange?: (inputValue: string, details: ComboboxRootChangeEventDetails) => void
275
- /** Items are loading: the chevron turns into a spinner, a status row sits under the list and the empty message waits. Defaults to `false`. */
276
- loading?: boolean
277
- /** The loading row's text. Defaults to `"Loading…"`. */
278
- loadingLabel?: ReactNode
279
- /** Shows the picks but takes no input: the popup stays shut and the clear ✕ and chip ✕ go. Defaults to `false`. */
280
- readOnly?: boolean
281
- /** Controlled open state of the popup. Pair it with `onOpenChange`. */
282
- open?: boolean
283
- /** Initial open state, for an uncontrolled popup. Defaults to `false`. */
284
- defaultOpen?: boolean
285
- /** Called when the popup opens or closes. */
286
- onOpenChange?: (open: boolean) => void
287
- /** Adds a last row that creates the trimmed query whenever no label matches it exactly. Add the item to `items` and return it to select it. */
288
- onCreate?: (query: string) => ComboboxItemData | void
289
- /** The create row's label. Defaults to ``(query) => `Create “${query}”` ``. */
290
- createLabel?: (query: string) => ReactNode
291
- /** Local addition: offer the create row with no callback — the component keeps the items it makes, so nothing outside has to hold the list. Defaults to `false`. */
292
- creatable?: boolean
293
- /** Multiple only. Picked items leave the list, so it reads as what is left to add. Deselect from the chips. Defaults to `false`. */
294
- hideSelected?: boolean
295
- /** Disables the field and the popup. Defaults to `false`. */
296
- disabled?: boolean
297
- /** Form field name; a hidden input carries the value. */
298
- name?: string
299
- /** Marks the hidden form input required. */
300
- required?: boolean
301
- /** Pins field and popup to one step of the size ladder (default 36px, compact 28px — see the Sizes primitive). Omitted, both follow the surrounding SizeProvider. */
302
- size?: SizeVariant
303
- /** Every fixed word the field's buttons announce (`open`, `clear`, `remove`, `placeholder`), for translating it. Defaults to English: `"Open"`, `"Clear"`, `"Remove …"`, `"Search…"`. */
304
- labels?: ComboboxLabels
305
- }
306
-
307
- function toValues(v: string | readonly string[] | undefined): string[] {
308
- if (v === undefined) return []
309
- if (Array.isArray(v)) return v as string[]
310
- return v === '' ? [] : [v as string]
311
- }
312
-
313
- /**
314
- * Type to filter a list. 1 pick, or chips.
315
- *
316
- * Items are data on the root: strings, which are their own value and label,
317
- * or `{ value, label, … }` objects. `Combobox.List` takes a function child and
318
- * renders one `Combobox.Item` per match, so the filtering, the ARIA wiring
319
- * (the input keeps focus while the arrows move a highlight through the rows),
320
- * the positioning and the hidden form input stay with Base UI. On top of that
321
- * sit the fluid-hover highlight, the merged selection background of
322
- * `Checkbox.Group`, and a list that oozes out of the field and back through the
323
- * shared morph (see Morph), goo by default. `multiple` turns the picks into a
324
- * `string[]` and pairs with `Combobox.Chips`: one chip per pick, Backspace in
325
- * an empty field drops the last one. Uncontrolled with `defaultValue`, or
326
- * controlled with `value` plus `onValueChange`.
327
- *
328
- * For items that come from elsewhere (a server, a paged API), own the query
329
- * with `inputValue` + `onInputValueChange`, pass `filter={null}` so the list
330
- * shows exactly the items you hand it, and set `loading` while they are on
331
- * their way: the chevron blurs into a spinner and a status row sits under
332
- * the list instead of the empty message. `Combobox.Content`'s `footer` pins
333
- * an action under the list ("Load more", "Add new…"). An object item's
334
- * `label` may be any node; its `textValue` is what filters, fills the field
335
- * and names its chip. `readOnly` shows the picks and takes no input. Inside a
336
- * Field the fields take its invalid state, like Input does, and `hideTrigger`
337
- * drops the chevron (and with it the loading spinner; the popup's status row is then the only loading signal) for a field that opens by typing. The words the buttons
338
- * announce ("Open", "Clear", "Remove …") and the default "Search…" placeholder
339
- * come from `labels`, for translating them.
340
- *
341
- * Statics:
342
- * - `Combobox.Input` — the single-pick field: icon, text input, clear ✕, chevron (`hideTrigger` drops it); `invalid` or `error` turn it destructive, as a surrounding Field's invalid state does.
343
- * - `Combobox.Chips` — the multiple field: one chip per pick ahead of the input, with the same `hideTrigger`, `invalid` and `error`.
344
- * - `Combobox.Content` — the popup surface, holding `Combobox.Empty` and `Combobox.List`:
345
- * `side`, `align`, `sideOffset`, a `footer` under the list, and the morph options
346
- * `from`, `effect`, `hideSource`, `tier`.
347
- * - `Combobox.List` — a row per match, from a `(item, index) => ReactNode` child.
348
- * - `Combobox.Item` — one row: `value`, optional `icon` and `disabled`.
349
- * - `Combobox.Empty` — what shows when nothing matches.
350
- *
351
- * @example {@include ./examples.mdx}
352
- */
353
- function Combobox({
354
- children,
355
- items,
356
- multiple,
357
- value,
358
- defaultValue,
359
- onValueChange,
360
- filter,
361
- onCreate,
362
- createLabel = defaultCreateLabel,
363
- creatable = false,
364
- hideSelected = false,
365
- disabled = false,
366
- name,
367
- required,
368
- inputValue: inputValueProp,
369
- defaultInputValue,
370
- onInputValueChange,
371
- labels,
372
- loading = false,
373
- loadingLabel = 'Loading…',
374
- readOnly = false,
375
- open: openProp,
376
- defaultOpen = false,
377
- onOpenChange,
378
- size,
379
- }: ComboboxProps) {
380
- const isMultiple = !!multiple
381
- const [internalValues, setInternalValues] = useState<string[]>(() => toValues(defaultValue))
382
- // The primitive's change details ride along to the caller's handler: the setter below drops no-op
383
- // writes, so the details are parked in a ref the handler reads when the write goes through.
384
- const inputDetails = useRef<ComboboxRootChangeEventDetails | undefined>(undefined)
385
- const [inputValue, setInputValue] = useControllableState(inputValueProp, defaultInputValue ?? '', next =>
386
- onInputValueChange?.(next, inputDetails.current as ComboboxRootChangeEventDetails),
387
- )
388
- const [open, setOpen] = useControllableState(openProp, defaultOpen, onOpenChange)
389
- const [highlight, setHighlight] = useState<Highlight | null>(null)
390
- // Items the create row made while no consumer owns the list.
391
- const [createdItems, setCreatedItems] = useState<ComboboxItemData[]>([])
392
- const { origin, capture } = useMorphOrigin()
393
- const anchorRef = useRef<HTMLDivElement | null>(null)
394
- // Memoized on the prop identity: a string value is stable by nature and a
395
- // consumer's array is state, so the derived array (and everything keyed on
396
- // it) only changes when the selection does.
397
- const controlledValues = useMemo(() => toValues(value), [value])
398
- const values = value !== undefined ? controlledValues : internalValues
399
-
400
- // The consumer's items plus anything `creatable` made here. A consumer that
401
- // adds a created item to `items` itself wins — the internal copy is deduped
402
- // away, so the two modes never double a row.
403
- const allItems = useMemo<readonly ComboboxItemData[]>(() => {
404
- const own = createdItems.filter(made => !items.some(item => itemValue(item) === itemValue(made)))
405
- return own.length ? [...items, ...own] : items
406
- }, [items, createdItems])
407
-
408
- // The query the primitive filters on: the trimmed input. In single mode
409
- // the field shows the selected label on open, which is an exact match, so
410
- // no create row appears until the user types past it.
411
- const query = inputValue.trim()
412
- const canCreate = !!onCreate || creatable
413
- const createItem = useMemo<ComboboxItemData | null>(() => {
414
- if (!canCreate || query === '') return null
415
- const lower = query.toLocaleLowerCase()
416
- const exists = allItems.some(item => itemLabel(item).toLocaleLowerCase() === lower)
417
- return exists ? null : { value: CREATE_VALUE, label: query }
418
- }, [canCreate, query, allItems])
419
-
420
- const itemsByValue = useMemo(() => {
421
- const map = new Map<string, ComboboxItemData>()
422
- for (const item of allItems) map.set(itemValue(item), item)
423
- // Rows hand the primitive the item they were rendered from; the create
424
- // row's carries the query it stands for.
425
- if (createItem) map.set(CREATE_VALUE, createItem)
426
- return map
427
- }, [allItems, createItem])
428
- const selectedItems = useMemo(
429
- () => values.map(v => itemsByValue.get(v)).filter(Boolean) as ComboboxItemData[],
430
- [values, itemsByValue],
431
- )
432
-
433
- // What the list shows: the items less the chips when `hideSelected`, plus
434
- // the create row last, so Enter picks a real match while one exists and
435
- // only creates once nothing matches.
436
- const hideChecked = hideSelected && isMultiple
437
- const listItems = useMemo<readonly ComboboxItemData[]>(() => {
438
- const visible = hideChecked ? allItems.filter(item => !values.includes(itemValue(item))) : allItems
439
- return createItem ? [...visible, createItem] : visible
440
- }, [allItems, hideChecked, values, createItem])
441
- const allSelected = hideChecked && query === '' && allItems.length > 0 && listItems.length === 0
442
-
443
- // Base UI hands back the item(s); the public API speaks in values. A pick
444
- // on the create row is not a selection: it asks the consumer for the item,
445
- // and selects whatever comes back.
446
- const onCreateRef = useRef(onCreate)
447
- onCreateRef.current = onCreate
448
- const handleValueChange = useCallback(
449
- (next: ComboboxItemData[] | ComboboxItemData | null) => {
450
- const picked = Array.isArray(next) ? next : next == null ? [] : [next]
451
- const created = picked.find(isCreateItem)
452
- let nextValues = picked.filter(item => !isCreateItem(item)).map(itemValue)
453
- if (created) {
454
- const madeQuery = itemLabel(created)
455
- const made = onCreateRef.current?.(madeQuery) ?? undefined
456
- if (made !== undefined) {
457
- nextValues = [...nextValues, itemValue(made)]
458
- } else if (creatable) {
459
- // No consumer owns the list, so the item is made and kept here: the
460
- // row it adds is indistinguishable from a real one.
461
- const own: ComboboxItemData = { value: madeQuery, label: madeQuery }
462
- setCreatedItems(prev => (prev.some(item => itemValue(item) === madeQuery) ? prev : [...prev, own]))
463
- nextValues = [...nextValues, madeQuery]
464
- } else if (!isMultiple) {
465
- // Nothing to select: a single field keeps its pick.
466
- return
467
- }
468
- }
469
- if (value === undefined) setInternalValues(nextValues)
470
- ;(onValueChange as ((value: string | string[]) => void) | undefined)?.(
471
- isMultiple ? nextValues : (nextValues[0] ?? ''),
472
- )
473
- },
474
- [value, onValueChange, isMultiple, creatable],
475
- )
476
-
477
- // The create row must survive the filter that hides every other miss.
478
- // Without a consumer filter the primitive's own collator match is kept.
479
- const { contains } = ComboboxPrimitive.useFilter()
480
- const filterFn = useMemo(() => {
481
- // `null`: the caller filters; the list shows what it is handed.
482
- if (filter === null) return null
483
- if (!filter && !createItem) return undefined
484
- const match = filter
485
- ? (item: ComboboxItemData, q: string) => filter(item, q)
486
- : (item: ComboboxItemData, q: string) => contains(item, q, itemLabel)
487
- return (item: ComboboxItemData, q: string) => isCreateItem(item) || match(item, q)
488
- }, [filter, createItem, contains])
489
-
490
- const createRow = createItem ? createLabel(query) : null
491
- const ctx = useMemo(
492
- () => ({
493
- values,
494
- multiple: isMultiple,
495
- inputValue,
496
- open,
497
- origin,
498
- anchorRef,
499
- disabled,
500
- itemsByValue,
501
- createRow,
502
- allSelected,
503
- loading,
504
- loadingLabel,
505
- readOnly,
506
- setOpen,
507
- labels: labels ?? NO_LABELS,
508
- }),
509
- [
510
- values,
511
- isMultiple,
512
- inputValue,
513
- open,
514
- origin,
515
- disabled,
516
- itemsByValue,
517
- createRow,
518
- allSelected,
519
- loading,
520
- loadingLabel,
521
- readOnly,
522
- setOpen,
523
- labels,
524
- ],
525
- )
526
-
527
- // A size prop pins the whole compound (field + portalled popup — React
528
- // context crosses portals) to one step of the ladder.
529
- const root = (
530
- <ComboboxContext.Provider value={ctx}>
531
- <ComboboxHighlightContext.Provider value={highlight}>
532
- <ComboboxPrimitive.Root
533
- items={listItems}
534
- multiple={isMultiple}
535
- // Always controlled; "" (no selection) maps to Base UI's null. The
536
- // value shape follows `multiple`, which the primitive's generics
537
- // can't express from a boolean prop — hence the cast.
538
- value={(isMultiple ? selectedItems : (selectedItems[0] ?? null)) as never}
539
- onValueChange={handleValueChange as never}
540
- isItemEqualToValue={(a: ComboboxItemData, b: ComboboxItemData) => itemValue(a) === itemValue(b)}
541
- itemToStringLabel={itemLabel}
542
- itemToStringValue={itemValue}
543
- filter={filterFn}
544
- open={open}
545
- onOpenChange={(next, details) => {
546
- // The list grows out of the field, whichever part opened it.
547
- if (next) capture({ event: details.event, trigger: anchorRef.current ?? undefined })
548
- setOpen(next)
549
- }}
550
- // Controlled only when the caller owns the query; otherwise Base UI
551
- // keeps it and this mirrors it.
552
- inputValue={inputValueProp}
553
- defaultInputValue={defaultInputValue}
554
- onInputValueChange={(next, details) => {
555
- inputDetails.current = details
556
- setInputValue(next)
557
- }}
558
- autoHighlight={ALWAYS_HIGHLIGHT}
559
- onItemHighlighted={(item, details) =>
560
- setHighlight(item === undefined ? null : { index: details.index, keyboard: details.reason !== 'pointer' })
561
- }
562
- disabled={disabled}
563
- readOnly={readOnly}
564
- name={name}
565
- required={required}
566
- // Non-modal: the page keeps scrolling and the Positioner tracks the
567
- // anchor, so the popup follows its field instead of detaching.
568
- modal={false}
569
- >
570
- {children}
571
- </ComboboxPrimitive.Root>
572
- </ComboboxHighlightContext.Provider>
573
- </ComboboxContext.Provider>
574
- )
575
-
576
- return size ? <SizeProvider size={size}>{root}</SizeProvider> : root
577
- }
578
-
579
- Combobox.displayName = 'Combobox'
580
-
581
- // ---------------------------------------------------------------------------
582
- // ComboboxInput — the field: leading icon, text input, clear ✕, chevron.
583
- // ---------------------------------------------------------------------------
584
-
585
- // The field follows the global pill/rounded shape; the popup does not. Like
586
- // Dropdown, the list keeps the smaller "rounded" radii whatever the rest of
587
- // the UI is shaped: pill corners on a popover distort its padding and break
588
- // the concentric fit of the rows' hover and selection backgrounds inside it.
589
- const popupShape = shapeMap.rounded
590
-
591
- // The first row is highlighted the moment the list opens and follows the
592
- // query as it filters, so Enter always has a target. AriaCombobox accepts
593
- // "always" for this; ComboboxRoot's prop type still says boolean.
594
- const ALWAYS_HIGHLIGHT = 'always' as unknown as boolean
595
-
596
- // The clear and chevron buttons share one quiet style; they sit inside the
597
- // field's ring so they need no frame of their own.
598
- const fieldButtonClass =
599
- 'flex shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors duration-fast hover:text-foreground focus-visible:ring-1 focus-visible:ring-focus-ring data-[disabled]:pointer-events-none'
600
- // The clear ✕ is a real icon button: the hover fill says "press me", where
601
- // the chevron beside it only decorates the field it belongs to.
602
- const clearButtonClass = cn(fieldButtonClass, 'hover:bg-hover active:bg-active transition-[color,background-color]')
603
- // A chip's ✕ sits on the chip's own hover-tinted fill, so it steps up a notch.
604
- const chipRemoveClass = cn(fieldButtonClass, 'rounded hover:bg-active')
605
-
606
- interface ComboboxFieldProps
607
- extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'value' | 'defaultValue' | 'onChange'> {
608
- /** Framed at rest, or invisible until hovered or focused. Defaults to `"bordered"`. */
609
- variant?: FieldVariant
610
- /** Leading icon inside the field. */
611
- icon?: IconComponent
612
- /** Shown while nothing is typed or selected. Defaults to `labels.placeholder` on the root, else `"Search…"`. */
613
- placeholder?: string
614
- /** Error message under the field; tints the ring and sets `aria-invalid`. */
615
- error?: string
616
- /** Marks the value invalid without a message: the field turns destructive and sets `aria-invalid`. Defaults to the surrounding Field's invalid state. */
617
- invalid?: boolean
618
- /** Hides the chevron button, for a field that opens by typing or arrow keys alone. It also drops the loading spinner, which lives in that button, so the popup's status row is the only loading signal. Defaults to `false`. */
619
- hideTrigger?: boolean
620
- /** Render a ✕ that clears the selection and query. Its slot is always
621
- * reserved so the field never changes width. Defaults to `false`. */
622
- clearable?: boolean
623
- /** Size override for the field alone. Prefer the `size` prop on <Combobox>
624
- * (or a surrounding SizeProvider) so the popup matches. */
625
- size?: SizeVariant
626
- }
627
-
628
- type ComboboxInputProps = ComboboxFieldProps
629
-
630
- // Icon swap (motion.md): the chevron and the spinner share one cell.
631
- const SHOWN = { opacity: 1, scale: 1, filter: 'blur(0px)' }
632
- const HIDDEN = { opacity: 0, scale: 0.6, filter: 'blur(4px)' }
633
- const swapEnter = { type: 'tween' as const, duration: spring.fast.duration, ease: 'easeOut' as const }
634
- const swapLeave = { type: 'tween' as const, ...spring.fast.exit, ease: 'easeIn' as const }
635
-
636
- /** The trailing clear ✕ and chevron, shared by both fields. */
637
- function FieldControls({
638
- clearable,
639
- hideTrigger,
640
- compact,
641
- iconSize,
642
- }: {
643
- clearable: boolean
644
- hideTrigger: boolean
645
- compact: boolean
646
- iconSize: number
647
- }) {
648
- const XIcon = useIcon('x')
649
- const Loader = useIcon('loader-circle')
650
- const pill = useShape().variant === 'pill'
651
- const { loading, readOnly, labels } = useComboboxContext()
652
- const reduced = useReduceMotion()
653
- const swap = (shown: boolean) => (reduced ? { opacity: shown ? 1 : 0 } : shown ? SHOWN : HIDDEN)
654
- return (
655
- <>
656
- {clearable && !readOnly && (
657
- // The primitive hides the button while there is nothing to clear; a
658
- // fixed slot keeps the field's width steady either way.
659
- <span className={cn('flex shrink-0 items-center justify-center', compact ? 'size-5' : 'size-6')}>
660
- <ComboboxPrimitive.Clear
661
- aria-label={labels.clear ?? 'Clear'}
662
- className={cn(clearButtonClass, pill && 'rounded-full', compact ? 'size-5' : 'size-6')}
663
- >
664
- <XIcon size={iconSize} strokeWidth={1.5} />
665
- </ComboboxPrimitive.Clear>
666
- </span>
667
- )}
668
- {/* Not a tab stop: the field itself opens on ArrowDown or typing. */}
669
- {!hideTrigger && (
670
- <ComboboxPrimitive.Trigger
671
- aria-label={labels.open ?? 'Open'}
672
- tabIndex={-1}
673
- className={cn(fieldButtonClass, compact ? 'size-5' : 'size-6')}
674
- >
675
- {/* While loading, the chevron blurs into a spinner in the same cell. */}
676
- <span className="grid" style={{ width: iconSize, height: iconSize }}>
677
- <motion.span
678
- className="col-start-1 row-start-1 flex"
679
- initial={false}
680
- animate={swap(!loading)}
681
- transition={loading ? swapLeave : swapEnter}
682
- >
683
- <svg
684
- width={iconSize}
685
- height={iconSize}
686
- viewBox="0 0 24 24"
687
- fill="none"
688
- stroke="currentColor"
689
- strokeWidth={2}
690
- strokeLinecap="round"
691
- strokeLinejoin="round"
692
- className={cn('transition-colors duration-fast', readOnly && 'invisible')}
693
- >
694
- <path d="M6 9l6 6 6-6" />
695
- </svg>
696
- </motion.span>
697
- <motion.span
698
- aria-hidden
699
- className="col-start-1 row-start-1 flex"
700
- initial={false}
701
- animate={swap(loading)}
702
- transition={loading ? swapEnter : swapLeave}
703
- >
704
- <Loader size={iconSize} strokeWidth={1.5} className="animate-spin motion-reduce:animate-none" />
705
- </motion.span>
706
- </span>
707
- </ComboboxPrimitive.Trigger>
708
- )}
709
- </>
710
- )
711
- }
712
-
713
- const ComboboxInput = forwardRef<HTMLInputElement, ComboboxInputProps>(
714
- (
715
- {
716
- className,
717
- variant = 'bordered',
718
- icon: Icon,
719
- placeholder,
720
- error,
721
- invalid,
722
- hideTrigger = false,
723
- clearable = false,
724
- size,
725
- ...props
726
- },
727
- ref,
728
- ) => {
729
- const { anchorRef, readOnly, labels } = useComboboxContext()
730
- // `error` forces the destructive state; otherwise the prop, then a surrounding Field's.
731
- const box = useFieldBox({ variant, invalid: error ? true : invalid, readOnly, size })
732
- const { size: sizeClasses, props: boxProps } = box
733
- const compact = sizeClasses.variant === 'compact'
734
-
735
- return (
736
- <div className="flex flex-col gap-1">
737
- {/* The group is the popup's anchor, so the list spans the whole
738
- field (icon to chevron), not just the text input. */}
739
- <ComboboxPrimitive.InputGroup
740
- ref={anchorRef}
741
- {...boxProps}
742
- className={cn(
743
- boxProps.className,
744
- 'flex items-center',
745
- sizeClasses.control,
746
- sizeClasses.gap,
747
- compact ? 'min-w-[128px]' : 'min-w-[160px]',
748
- className,
749
- )}
750
- >
751
- {Icon && <Icon size={sizeClasses.icon} strokeWidth={1.5} className={fieldIconClasses} />}
752
- <ComboboxPrimitive.Input
753
- ref={ref}
754
- placeholder={placeholder ?? labels.placeholder ?? 'Search…'}
755
- aria-invalid={box.invalid || undefined}
756
- className={cn(
757
- 'min-w-0 flex-1 rounded-none bg-transparent text-foreground placeholder:text-muted-foreground outline-none font-[inherit]',
758
- sizeClasses.text,
759
- // The caret is as tall as the line box (Chrome, Safari); the
760
- // ladder's leading keeps it in proportion to the field.
761
- compact ? 'leading-5' : 'leading-6',
762
- )}
763
- {...props}
764
- />
765
- <FieldControls
766
- clearable={clearable}
767
- hideTrigger={hideTrigger}
768
- compact={compact}
769
- iconSize={sizeClasses.icon}
770
- />
771
- </ComboboxPrimitive.InputGroup>
772
- {error && <span className="text-caption text-destructive pl-3">{error}</span>}
773
- </div>
774
- )
775
- },
776
- )
777
-
778
- ComboboxInput.displayName = 'ComboboxInput'
779
-
780
- // ---------------------------------------------------------------------------
781
- // ComboboxChips — the multiple-selection field: one chip per selected item
782
- // ahead of the text input, wrapping onto new lines as they accumulate.
783
- // Backspace in an empty input removes the last chip; ArrowLeft walks into
784
- // the chips, where Backspace/Delete removes the focused one.
785
- // ---------------------------------------------------------------------------
786
-
787
- type ComboboxChipsProps = ComboboxFieldProps
788
-
789
- /** Springs the chips field's height to the measured chip row stack (plus
790
- * `padY`, the field's vertical padding) as chips wrap and unwrap. The
791
- * height lives in a MotionValue driving an inline style, and for a reason:
792
- * the field must stay pinned at its old height through the reflow that
793
- * wraps a chip, or its auto height lands on the new row before any spring
794
- * can start and the growth snaps. First measure sets the pin; every later
795
- * one springs it. Measured (offsetHeight + ResizeObserver), never
796
- * `height: "auto"`: framer resolves "auto" from the element's *visual*
797
- * (transformed) size, so under a scaled ancestor (the /demo card) the
798
- * spring would overshoot. Layout metrics are transform-immune. */
799
- function useChipRowHeight(padY: number) {
800
- const height = useMotionValue<number | 'auto'>('auto')
801
- const roRef = useRef<ResizeObserver | null>(null)
802
- const padRef = useRef(padY)
803
- padRef.current = padY
804
- const ref = useCallback(
805
- (el: HTMLDivElement | null) => {
806
- roRef.current?.disconnect()
807
- roRef.current = null
808
- if (!el) return
809
- const measure = () => {
810
- if (el.offsetHeight <= 0) return
811
- const target = el.offsetHeight + padRef.current
812
- if (height.get() === 'auto') height.set(target)
813
- else if (height.get() !== target) animate(height, target, spring.fast)
814
- }
815
- measure()
816
- const ro = new ResizeObserver(measure)
817
- ro.observe(el)
818
- roRef.current = ro
819
- },
820
- [height],
821
- )
822
- return { ref, height }
823
- }
824
-
825
- const ComboboxChips = forwardRef<HTMLInputElement, ComboboxChipsProps>(
826
- (
827
- {
828
- className,
829
- variant = 'bordered',
830
- icon: Icon,
831
- placeholder,
832
- error,
833
- invalid,
834
- hideTrigger = false,
835
- clearable = false,
836
- size,
837
- ...props
838
- },
839
- ref,
840
- ) => {
841
- const XIcon = useIcon('x')
842
- const shape = useShape()
843
- const { anchorRef, open, disabled, inputValue, values, readOnly, labels } = useComboboxContext()
844
- const box = useFieldBox({ variant, invalid: error ? true : invalid, disabled, readOnly, size })
845
- const { size: sizeClasses, props: boxProps } = box
846
- const compact = sizeClasses.variant === 'compact'
847
- const rowHeight = useChipRowHeight(compact ? 8 : 12)
848
-
849
- return (
850
- <div className="flex flex-col gap-1">
851
- <ComboboxPrimitive.Chips
852
- ref={anchorRef}
853
- // The field springs to the chip rows' measured height as they
854
- // wrap and unwrap, instead of snapping a row taller or shorter.
855
- render={<motion.div style={{ height: rowHeight.height }} />}
856
- // Chips stamps no state attributes of its own (InputGroup does);
857
- // the field ladder (`lib/field-classes.ts`) and the chevron read these.
858
- {...boxProps}
859
- data-popup-open={open || undefined}
860
- className={cn(
861
- boxProps.className,
862
- 'flex items-center',
863
- // The spring above owns the height; leaving it in transition-all
864
- // would have the 80ms CSS ease drag behind every spring frame.
865
- 'transition-[color,background-color,border-radius,box-shadow,opacity]',
866
- // Grows with its chips. Rows top-align (`items-start`) so the
867
- // icon and controls hold the first line while chips wrap; the
868
- // vertical padding is exactly what centers one 24px (20px
869
- // compact) row inside the ladder height.
870
- '!items-start',
871
- compact ? 'min-h-7 py-1' : 'min-h-9 py-1.5',
872
- sizeClasses.gap,
873
- compact ? 'min-w-[128px]' : 'min-w-[160px]',
874
- className,
875
- )}
876
- >
877
- {Icon && (
878
- // A row-height box keeps the icon centered on the first line.
879
- <span className={cn('flex shrink-0 items-center', compact ? 'h-5' : 'h-6')}>
880
- <Icon size={sizeClasses.icon} strokeWidth={1.5} className={fieldIconClasses} />
881
- </span>
882
- )}
883
- <div
884
- ref={rowHeight.ref}
885
- className={cn(
886
- 'relative flex min-w-0 flex-1 flex-wrap items-center gap-1',
887
- // A chip leads the field: the row pulls left so the chip sits
888
- // an even distance from every edge. The offset lives here, not
889
- // on the container's padding — the container's transition-all
890
- // would ease a padding change against the chip's spring and
891
- // drag the caret with it; the row snaps instead, in the same
892
- // frame as the chip. An icon keeps the full inset — it leads,
893
- // not the chip.
894
- !Icon && values.length > 0 && '-ml-1',
895
- )}
896
- >
897
- <ComboboxPrimitive.Value>
898
- {(selected: ComboboxItemData[] | null) => (
899
- <>
900
- {/* Chips pop in and out on the fast tier and slide into
901
- their new slots; an exiting chip is inert while it
902
- fades, since the primitive has already dropped it.
903
- popLayout lifts the exiting chip out of the flow at
904
- once, so the field reflows immediately rather than
905
- after the fade. */}
906
- <AnimatePresence initial={false} mode="popLayout">
907
- {(selected ?? []).map(item => {
908
- const label = itemLabel(item)
909
- return (
910
- <motion.span
911
- key={itemValue(item)}
912
- layout
913
- initial={{ opacity: 0, scale: 0.9 }}
914
- animate={{ opacity: 1, scale: 1 }}
915
- exit={{
916
- opacity: 0,
917
- scale: 0.9,
918
- pointerEvents: 'none',
919
- transition: spring.fast.exit,
920
- }}
921
- transition={spring.fast}
922
- // Grow from the left edge, where the typed text
923
- // just was — not from the middle of the chip.
924
- style={{ originX: 0 }}
925
- className="inline-flex max-w-full shrink-0"
926
- >
927
- <ComboboxPrimitive.Chip
928
- aria-label={label}
929
- className={cn(
930
- 'inline-flex max-w-full shrink-0 items-center gap-0.5 bg-hover pl-2 pr-0.5 text-foreground outline-none',
931
- shape.variant === 'pill' ? 'rounded-full' : 'rounded-md',
932
- 'focus-visible:ring-1 focus-visible:ring-focus-ring',
933
- compact ? 'h-5 text-caption-compact' : 'h-6 text-caption',
934
- )}
935
- >
936
- <span
937
- className={cn(
938
- typeof itemNode(item) === 'string' ? 'truncate' : 'flex min-w-0 items-center',
939
- readOnly && 'pr-1.5',
940
- )}
941
- >
942
- {itemNode(item)}
943
- </span>
944
- {!readOnly && (
945
- <ComboboxPrimitive.ChipRemove
946
- aria-label={labels.remove ? labels.remove(label) : `Remove ${label}`}
947
- className={cn(
948
- chipRemoveClass,
949
- shape.variant === 'pill' && 'rounded-full',
950
- compact ? 'size-4' : 'size-5',
951
- )}
952
- >
953
- <XIcon size={compact ? 10 : 12} strokeWidth={2} />
954
- </ComboboxPrimitive.ChipRemove>
955
- )}
956
- </ComboboxPrimitive.Chip>
957
- </motion.span>
958
- )
959
- })}
960
- </AnimatePresence>
961
- {/* The field sizes to what is typed (`size` is the
962
- intrinsic width; flex-auto grows it across the rest
963
- of its row) so it stays beside the chips while it
964
- fits and wraps only once the text no longer does.
965
- It never animates: chips slide, the field snaps.
966
- Animating it read as the placeholder sliding in from
967
- the right when the last chip went. */}
968
- <span className="flex min-w-6 flex-auto">
969
- <ComboboxPrimitive.Input
970
- ref={ref}
971
- size={Math.max(1, inputValue.length + 1)}
972
- placeholder={selected?.length ? undefined : (placeholder ?? labels.placeholder ?? 'Search…')}
973
- aria-invalid={box.invalid || undefined}
974
- className={cn(
975
- 'w-full min-w-0 rounded-none bg-transparent text-foreground placeholder:text-muted-foreground outline-none font-[inherit]',
976
- // Line box = row height, so the caret spans the chip row.
977
- compact ? 'h-5 leading-5' : 'h-6 leading-6',
978
- sizeClasses.text,
979
- )}
980
- {...props}
981
- />
982
- </span>
983
- </>
984
- )}
985
- </ComboboxPrimitive.Value>
986
- </div>
987
- <FieldControls
988
- clearable={clearable}
989
- hideTrigger={hideTrigger}
990
- compact={compact}
991
- iconSize={sizeClasses.icon}
992
- />
993
- </ComboboxPrimitive.Chips>
994
- {error && <span className="text-caption text-destructive pl-3">{error}</span>}
995
- </div>
996
- )
997
- },
998
- )
999
-
1000
- ComboboxChips.displayName = 'ComboboxChips'
1001
-
1002
- // ---------------------------------------------------------------------------
1003
- // ComboboxContent — the popup surface. Holds ComboboxEmpty and ComboboxList.
1004
- // ---------------------------------------------------------------------------
1005
-
1006
- interface ComboboxContentProps {
1007
- /** Classes for the popup surface — merged after the radius and max-height. */
1008
- className?: string
1009
- /** `Combobox.Empty` and `Combobox.List`. */
1010
- children: ReactNode
1011
- /** Side of the field the popup prefers. Defaults to `"bottom"`. */
1012
- side?: 'top' | 'bottom' | 'left' | 'right'
1013
- /** Alignment against the field. Defaults to `"start"`. */
1014
- align?: 'start' | 'center' | 'end'
1015
- /** Gap to the field, in px. Ignored when the list opens in place over the field (`effect="morph"` with `hideSource`). Defaults to `6`. */
1016
- sideOffset?: number
1017
- /** Where the list grows from (see Morph): the field, the press point, its own center, a viewport edge, or a ref to any element. Defaults to `'trigger'` (the field). */
1018
- from?: 'trigger' | 'pointer' | 'center' | 'top' | 'right' | 'bottom' | 'left' | RefObject<HTMLElement | null>
1019
- /** How it grows (see Morph): with the liquid goo neck, a plain morph, a slide or a fade. Defaults to `'goo'`. */
1020
- effect?: 'goo' | 'morph' | 'slide' | 'fade'
1021
- /** Hide the field while open, so it reads as turning into the list. Defaults to `false`. */
1022
- hideSource?: boolean
1023
- /** Spring tier of a plain morph, slide or fade; goo runs on its own `spring.goo`. Defaults to `'moderate'`. */
1024
- tier?: 'moderate' | 'slow'
1025
- /** Pinned under the list, past a hairline: a "load more" or "add new" action. A function gets `{ close, query }`. */
1026
- footer?: ComboboxFooter
1027
- }
1028
-
1029
- const ComboboxContent = forwardRef<HTMLDivElement, ComboboxContentProps>(
1030
- (
1031
- {
1032
- className,
1033
- children,
1034
- side = 'bottom',
1035
- align = 'start',
1036
- sideOffset = 6,
1037
- from,
1038
- effect,
1039
- hideSource,
1040
- tier = 'moderate',
1041
- footer,
1042
- },
1043
- ref,
1044
- ) => {
1045
- const { open, origin, anchorRef, setOpen, inputValue } = useComboboxContext()
1046
- const morph = useMorph(open, origin, { from, effect, hideSource, tier })
1047
- useImperativeHandle(ref, () => morph.popup as HTMLDivElement, [morph.popup])
1048
- // Lifts 2 levels off its substrate with a fixed shadow (see Elevated).
1049
- const level = Math.min(useSurface() + 2, 8)
1050
- const shape = popupShape
1051
-
1052
- return (
1053
- <ComboboxPrimitive.Portal>
1054
- <ComboboxPrimitive.Positioner
1055
- // The whole field (input group or chips container) is the anchor,
1056
- // so the list spans it edge to edge.
1057
- anchor={anchorRef}
1058
- side={side}
1059
- align={align}
1060
- sideOffset={opensInPlace({ effect, hideSource }) ? inPlaceOffset : sideOffset}
1061
- className="z-50 outline-none"
1062
- >
1063
- <ComboboxPrimitive.Popup ref={morph.popupRef} className="relative select-none outline-none">
1064
- <SurfaceProvider value={level}>
1065
- <MorphSurface
1066
- morph={morph}
1067
- bg={SURFACE_BG[level]}
1068
- shadow={SURFACE_SHADOW[3]}
1069
- radius={shape.container}
1070
- className={cn(
1071
- // min-w tracks the field via the Positioner's --anchor-width
1072
- // var; the list inside owns padding and scrolling.
1073
- `flex flex-col min-w-[var(--anchor-width)] max-h-[min(300px,var(--available-height))] overflow-hidden ${shape.container}`,
1074
- className,
1075
- )}
1076
- >
1077
- {children}
1078
- {footer !== undefined && (
1079
- // Outside the scrolling list, so it stays put while the rows scroll.
1080
- <div className="shrink-0 border-t border-border p-1">
1081
- {typeof footer === 'function'
1082
- ? footer({ close: () => setOpen(false), query: inputValue.trim() })
1083
- : footer}
1084
- </div>
1085
- )}
1086
- </MorphSurface>
1087
- </SurfaceProvider>
1088
- </ComboboxPrimitive.Popup>
1089
- </ComboboxPrimitive.Positioner>
1090
- </ComboboxPrimitive.Portal>
1091
- )
1092
- },
1093
- )
1094
-
1095
- ComboboxContent.displayName = 'ComboboxContent'
1096
-
1097
- // ---------------------------------------------------------------------------
1098
- // ComboboxList — renders a row per matching item, carrying the fluid hover
1099
- // hover overlays and the animated selected background / focus ring.
1100
- // ---------------------------------------------------------------------------
1101
-
1102
- interface ComboboxListProps {
1103
- /** Classes for the rows' container. */
1104
- className?: string
1105
- /** Render one row per matching item: `(item, index) => <Combobox.Item …/>`. Rows get their index from the list. */
1106
- children: (item: ComboboxItemData, index: number) => ReactNode
1107
- }
1108
-
1109
- const ComboboxList = forwardRef<HTMLDivElement, ComboboxListProps>(({ className, children }, ref) => {
1110
- const { open, values, multiple, inputValue, createRow, loading, loadingLabel } = useComboboxContext()
1111
- const Loader = useIcon('loader-circle')
1112
- const sizeClasses = useSize()
1113
- const highlight = useContext(ComboboxHighlightContext)
1114
- const PlusIcon = useIcon('plus')
1115
- const shape = popupShape
1116
- const containerRef = useRef<HTMLDivElement>(null)
1117
-
1118
- const hover = useFluidHover(containerRef, { isItemDisabled: isDisabledRow })
1119
- const { activeIndex, setActiveIndex, itemRects, isMeasured, handlers, registerItem, remeasure } = hover
1120
-
1121
- // Checked rows by index — one in single mode, any number in multiple.
1122
- const [checkedIndices, setCheckedIndices] = useState<number[]>([])
1123
-
1124
- // Fresh rects once per open — the popup keeps its rows registered while
1125
- // it sits hidden between opens, so registration alone never triggers a
1126
- // pass on reopen. Filtering re-registers rows, which the hook coalesces
1127
- // into its own measurement.
1128
- useEffect(() => {
1129
- if (!open) return
1130
- remeasure()
1131
- }, [open, remeasure])
1132
-
1133
- // Typing re-filters the rows underneath the overlays, and those frames
1134
- // must snap, not glide — an overlay springing to wherever its row LANDED
1135
- // reads as the list smearing on every keystroke. The primitive filters
1136
- // internally, so the query is the reflow signal (picks don't change it,
1137
- // keeping multiple mode's merge/split glide). The flag arms on the
1138
- // query's render and holds until the rects measured from the new row
1139
- // set land — measurement coalesces on a rAF, one render behind.
1140
- const reflowRef = useRef(false)
1141
- const prevQueryRef = useRef(inputValue)
1142
- const armedRectsRef = useRef(itemRects)
1143
- if (prevQueryRef.current !== inputValue) {
1144
- prevQueryRef.current = inputValue
1145
- armedRectsRef.current = itemRects
1146
- reflowRef.current = true
1147
- }
1148
- const reflowSnap = reflowRef.current
1149
- useEffect(() => {
1150
- if (reflowRef.current && itemRects !== armedRectsRef.current) reflowRef.current = false
1151
- })
1152
-
1153
- // Detect the checked rows. Their indices shift as the query filters the
1154
- // list, so the typed value is a dependency too.
1155
- useEffect(() => {
1156
- if (!open) return
1157
- // Double rAF: first waits for React commit, second for layout
1158
- let inner: number
1159
- const outer = requestAnimationFrame(() => {
1160
- inner = requestAnimationFrame(() => {
1161
- const container = containerRef.current
1162
- if (container) {
1163
- const rows = Array.from(container.querySelectorAll('[data-fluid-hover-index]')) as HTMLElement[]
1164
- const next: number[] = []
1165
- rows.forEach((el, i) => {
1166
- if (values.includes(el.getAttribute('data-value') ?? '')) next.push(i)
1167
- })
1168
- setCheckedIndices(next)
1169
- }
1170
- })
1171
- })
1172
- return () => {
1173
- cancelAnimationFrame(outer)
1174
- cancelAnimationFrame(inner)
1175
- }
1176
- }, [open, values, inputValue])
1177
-
1178
- // A keyboard (or auto) highlight drives the hover background — the only
1179
- // indicator: the row Enter would pick reads like the row the pointer
1180
- // would pick, with no ring.
1181
- useEffect(() => {
1182
- // A dropped highlight (the input is the stop again) takes the hover
1183
- // background with it — otherwise it would linger on the last row.
1184
- if (!highlight) setActiveIndex(null)
1185
- else if (highlight.keyboard) setActiveIndex(highlight.index)
1186
- }, [highlight, setActiveIndex])
1187
-
1188
- // Reset every overlay index as the close begins, so the reopen doesn't
1189
- // spring an overlay from a stale row.
1190
- useEffect(() => {
1191
- if (open) return
1192
- setCheckedIndices([])
1193
- setActiveIndex(null)
1194
- }, [open, setActiveIndex])
1195
-
1196
- // Overlays read rects only once the hook reports the row set fully
1197
- // measured — positioning one from an incomplete pass mounts it at the
1198
- // wrong row, and the correcting pass then springs it across the list.
1199
- // Single mode glides ONE marker between rows (a value change springs it
1200
- // to the picked row). Multiple mode paints one block per contiguous run
1201
- // of checked rows — merging and splitting like Checkbox.Group as picks
1202
- // bridge or break a run.
1203
- const firstChecked = checkedIndices[0]
1204
- const checkedRect = isMeasured && !multiple && firstChecked !== undefined ? (itemRects[firstChecked] ?? null) : null
1205
- const runs = useSelectionRuns(multiple ? checkedIndices : [])
1206
- const blocks = useMergeSplitBlocks(runs, isMeasured && open ? itemRects : [], shape.bgRadius)
1207
-
1208
- const contentCtx = useMemo(() => ({ registerItem, activeIndex }), [registerItem, activeIndex])
1209
-
1210
- return (
1211
- <ComboboxContentContext.Provider value={contentCtx}>
1212
- <ScrollArea className={popupScrollAreaClass} viewportClassName={cn(popupViewportClass, 'scroll-fade')}>
1213
- <ComboboxPrimitive.List
1214
- ref={(node: HTMLDivElement | null) => {
1215
- ;(containerRef as MutableRefObject<HTMLDivElement | null>).current = node
1216
- if (typeof ref === 'function') ref(node)
1217
- else if (ref) (ref as MutableRefObject<HTMLDivElement | null>).current = node
1218
- }}
1219
- onMouseEnter={handlers.onMouseEnter}
1220
- onMouseMove={handlers.onMouseMove}
1221
- onMouseLeave={handlers.onMouseLeave}
1222
- onClick={handlers.onClick}
1223
- className={cn(
1224
- // The list is the overlays' offsetParent, so rows and overlays
1225
- // scroll together inside the ScrollArea. Padding collapses when
1226
- // the list is empty (ComboboxEmpty takes over).
1227
- 'relative flex flex-col p-1 outline-none data-[empty]:p-0',
1228
- className,
1229
- )}
1230
- >
1231
- {/* The three overlays are torn down as the close begins rather
1232
- than exit-animated: an overlay still mounted when the popup
1233
- reopens is one AnimatePresence re-adopts under its old key and
1234
- animates from the row it had before. */}
1235
- {/* Selected background */}
1236
- {open && multiple && (
1237
- <SelectionBackgrounds blocks={reflowSnap ? blocks.map(b => ({ ...b, instant: true })) : blocks} />
1238
- )}
1239
- {open && !multiple && (
1240
- <AnimatePresence>
1241
- {checkedRect && (
1242
- <motion.div
1243
- key="checked"
1244
- aria-hidden
1245
- className={`absolute ${shape.bg} bg-active pointer-events-none`}
1246
- initial={false}
1247
- animate={{
1248
- top: checkedRect.top,
1249
- left: checkedRect.left,
1250
- width: checkedRect.width,
1251
- height: checkedRect.height,
1252
- opacity: 1,
1253
- }}
1254
- exit={{ opacity: 0, transition: spring.moderate.exit }}
1255
- transition={
1256
- reflowSnap ? { duration: 0 } : { ...spring.moderate, opacity: { duration: spring.fast.duration } }
1257
- }
1258
- />
1259
- )}
1260
- </AnimatePresence>
1261
- )}
1262
-
1263
- {/* Hover background. Snaps (no travel) while a filter reflow is
1264
- moving the rows underneath. */}
1265
- {open && (
1266
- <FluidHoverHighlight hover={hover} className={shape.bg} transition={reflowSnap ? false : undefined} />
1267
- )}
1268
-
1269
- {/* The overlays above are plain children, so the rows come from
1270
- an explicit Collection (a function child on List would have
1271
- to be its only child). */}
1272
- <ComboboxPrimitive.Collection>
1273
- {(item: ComboboxItemData, index: number) => (
1274
- <ComboboxItemIndexContext.Provider key={itemValue(item)} value={index}>
1275
- {/* The create row is the list's, not the consumer's: it
1276
- reads the label from the root and picks like any row. */}
1277
- {isCreateItem(item) ? (
1278
- <ComboboxItem value={CREATE_VALUE} icon={PlusIcon}>
1279
- {createRow}
1280
- </ComboboxItem>
1281
- ) : (
1282
- children(item, index)
1283
- )}
1284
- </ComboboxItemIndexContext.Provider>
1285
- )}
1286
- </ComboboxPrimitive.Collection>
1287
- </ComboboxPrimitive.List>
1288
- {loading && (
1289
- // Not a row: no hover, no pick. Under the rows, so items already
1290
- // here stay where they are while more arrive.
1291
- <div
1292
- role="status"
1293
- className={cn(
1294
- 'flex shrink-0 items-center justify-center text-muted-foreground',
1295
- sizeClasses.control,
1296
- sizeClasses.itemPx,
1297
- sizeClasses.gap,
1298
- sizeClasses.text,
1299
- )}
1300
- >
1301
- <Loader
1302
- size={sizeClasses.icon}
1303
- strokeWidth={1.5}
1304
- className="shrink-0 animate-spin motion-reduce:animate-none"
1305
- />
1306
- <span className="truncate [text-box:trim-both_cap_alphabetic]">{loadingLabel}</span>
1307
- </div>
1308
- )}
1309
- </ScrollArea>
1310
- </ComboboxContentContext.Provider>
1311
- )
1312
- })
1313
-
1314
- ComboboxList.displayName = 'ComboboxList'
1315
-
1316
- // ---------------------------------------------------------------------------
1317
- // ComboboxItem
1318
- // ---------------------------------------------------------------------------
1319
-
1320
- interface ComboboxItemProps extends HTMLAttributes<HTMLDivElement> {
1321
- /** Leading icon in the row. */
1322
- icon?: IconComponent
1323
- /** The item's value — a string item itself, or an object item's `value`. */
1324
- value: string
1325
- /** Disables the row. Defaults to `false`. */
1326
- disabled?: boolean
1327
- /** The row label. */
1328
- children?: ReactNode
1329
- }
1330
-
1331
- const ComboboxItem = forwardRef<HTMLDivElement, ComboboxItemProps>(
1332
- ({ className, children, icon: Icon, value, disabled = false, ...props }, ref) => {
1333
- const comboboxCtx = useComboboxContext()
1334
- const contentCtx = useContext(ComboboxContentContext)
1335
- const index = useContext(ComboboxItemIndexContext)
1336
- const internalRef = useRef<HTMLDivElement>(null)
1337
- const shape = popupShape
1338
- const sizeClasses = useSize()
1339
- const compact = sizeClasses.variant === 'compact'
1340
- const hasMounted = useRef(false)
1341
-
1342
- useEffect(() => {
1343
- hasMounted.current = true
1344
- }, [])
1345
-
1346
- // Register with fluid hover. Depends on the (stable) registerItem
1347
- // rather than the content context, which is rebuilt on every activeIndex
1348
- // change.
1349
- const registerItem = contentCtx?.registerItem
1350
- useRegisterFluidHoverItem(registerItem, index, internalRef)
1351
-
1352
- const isActive = contentCtx?.activeIndex === index
1353
- const isChecked = comboboxCtx.values.includes(value)
1354
- const skipAnimation = !hasMounted.current
1355
- // Base UI matches rows to `items` by value, so the row hands back the
1356
- // item it was rendered from.
1357
- const item = comboboxCtx.itemsByValue.get(value) ?? value
1358
-
1359
- return (
1360
- <ComboboxPrimitive.Item
1361
- value={item}
1362
- index={index}
1363
- disabled={disabled}
1364
- render={
1365
- <div
1366
- ref={(node: HTMLDivElement | null) => {
1367
- ;(internalRef as MutableRefObject<HTMLDivElement | null>).current = node
1368
- if (typeof ref === 'function') ref(node)
1369
- else if (ref) (ref as MutableRefObject<HTMLDivElement | null>).current = node
1370
- }}
1371
- data-fluid-hover-index={index}
1372
- data-value={value}
1373
- className={cn(
1374
- // Fixed height so the text-box trim on the label doesn't
1375
- // shrink the row; shrink-0 because the list is a max-height
1376
- // flex column.
1377
- `relative z-10 flex ${sizeClasses.control} shrink-0 items-center ${sizeClasses.gap} ${shape.item} ${sizeClasses.itemPx} ${sizeClasses.text} cursor-pointer outline-none select-none`,
1378
- 'transition-[color] duration-fast',
1379
- isActive || isChecked ? 'text-foreground' : 'text-muted-foreground',
1380
- disabled && 'opacity-50 pointer-events-none',
1381
- className,
1382
- )}
1383
- {...props}
1384
- />
1385
- }
1386
- >
1387
- {Icon && (
1388
- <Icon
1389
- size={sizeClasses.icon}
1390
- strokeWidth={isActive || isChecked ? 2 : 1.5}
1391
- className="shrink-0 transition-[color,stroke-width] duration-fast"
1392
- />
1393
- )}
1394
-
1395
- {/* py-1/-my-1 keeps truncate's overflow:hidden from clipping
1396
- ascenders/descenders outside the trimmed box. */}
1397
- {/* Nodes keep their own box; only text is trimmed and truncated. */}
1398
- <span
1399
- className={cn(
1400
- 'flex-1 min-w-0',
1401
- typeof children === 'string' || typeof children === 'number'
1402
- ? 'truncate [text-box:trim-both_cap_alphabetic] py-1 -my-1'
1403
- : 'flex items-center',
1404
- )}
1405
- >
1406
- {children}
1407
- </span>
1408
-
1409
- {/* Always-rendered fixed slot so the check appearing/disappearing
1410
- never changes the row's intrinsic width. */}
1411
- <span aria-hidden className={cn('shrink-0', compact ? 'w-3.5 h-3.5' : 'w-4 h-4')}>
1412
- <AnimatePresence>
1413
- {isChecked && (
1414
- <motion.svg
1415
- key="check"
1416
- width={sizeClasses.icon}
1417
- height={sizeClasses.icon}
1418
- viewBox="0 0 24 24"
1419
- fill="none"
1420
- stroke="currentColor"
1421
- strokeWidth={2}
1422
- strokeLinecap="round"
1423
- strokeLinejoin="round"
1424
- className="text-foreground"
1425
- initial={{ opacity: 1 }}
1426
- animate={{ opacity: 1 }}
1427
- exit={{ opacity: 1 }}
1428
- >
1429
- <motion.path
1430
- d="M4 12L9 17L20 6"
1431
- initial={{ pathLength: skipAnimation ? 1 : 0 }}
1432
- animate={{
1433
- pathLength: 1,
1434
- transition: { duration: spring.fast.duration, ease: 'easeOut' },
1435
- }}
1436
- exit={{
1437
- pathLength: 0,
1438
- transition: { duration: 0.04, ease: 'easeIn' },
1439
- }}
1440
- />
1441
- </motion.svg>
1442
- )}
1443
- </AnimatePresence>
1444
- </span>
1445
- </ComboboxPrimitive.Item>
1446
- )
1447
- },
1448
- )
1449
-
1450
- ComboboxItem.displayName = 'ComboboxItem'
1451
-
1452
- // ---------------------------------------------------------------------------
1453
- // ComboboxEmpty — shown in place of the list when nothing matches.
1454
- // ---------------------------------------------------------------------------
1455
-
1456
- interface ComboboxEmptyProps extends HTMLAttributes<HTMLDivElement> {
1457
- /** Shown instead of the children when `hideSelected` has emptied the
1458
- * list with nothing typed: every item is a chip already. */
1459
- allSelected?: ReactNode
1460
- /** Shown when nothing matches the query. */
1461
- children?: ReactNode
1462
- }
1463
-
1464
- const ComboboxEmpty = forwardRef<HTMLDivElement, ComboboxEmptyProps>(
1465
- ({ className, children, allSelected, ...props }, ref) => {
1466
- const sizeClasses = useSize()
1467
- const ctx = useComboboxContext()
1468
- return (
1469
- // Base UI keeps this element mounted (it is a live region) and only
1470
- // renders the children while the list is empty — so the padding is
1471
- // gated on having content.
1472
- <ComboboxPrimitive.Empty
1473
- ref={ref}
1474
- className={cn('px-3 text-center text-muted-foreground [&:not(:empty)]:py-6', sizeClasses.text, className)}
1475
- {...props}
1476
- >
1477
- {ctx.loading ? null : ctx.allSelected && allSelected !== undefined ? allSelected : children}
1478
- </ComboboxPrimitive.Empty>
1479
- )
1480
- },
1481
- )
1482
-
1483
- ComboboxEmpty.displayName = 'ComboboxEmpty'
1484
-
1485
- // ---------------------------------------------------------------------------
1486
- // Statics — the compound API, typed by assignment so `<Combobox.Item>` and
1487
- // the named exports stay the same component.
1488
- // ---------------------------------------------------------------------------
1489
-
1490
- Combobox.Input = ComboboxInput
1491
- Combobox.Chips = ComboboxChips
1492
- Combobox.Content = ComboboxContent
1493
- Combobox.List = ComboboxList
1494
- Combobox.Item = ComboboxItem
1495
- Combobox.Empty = ComboboxEmpty
1496
-
1497
- // ---------------------------------------------------------------------------
1498
- // Exports
1499
- // ---------------------------------------------------------------------------
1500
-
1501
- export type {
1502
- ComboboxChipsProps,
1503
- ComboboxContentProps,
1504
- ComboboxEmptyProps,
1505
- ComboboxFooter,
1506
- ComboboxFooterContext,
1507
- ComboboxInputProps,
1508
- ComboboxItemData,
1509
- ComboboxItemObject,
1510
- ComboboxItemProps,
1511
- ComboboxLabels,
1512
- ComboboxListProps,
1513
- ComboboxProps,
1514
- ComboboxValue,
1515
- }
1516
- export {
1517
- Combobox,
1518
- ComboboxChips,
1519
- ComboboxContent,
1520
- ComboboxEmpty,
1521
- ComboboxInput,
1522
- ComboboxItem,
1523
- ComboboxList,
1524
- fieldVariants as comboboxFieldVariants,
1525
- }
1526
-
1527
- export default Combobox