@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,1241 +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/dropdown.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
5
- * MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
6
- * Local modifications:
7
- * - `"use client"` dropped; `framer-motion` → `motion/react`; `@/lib/*` and
8
- * `@/hooks/*` → `../../../lib/*` (`SizeProvider` → `../../../primitives/sizes`);
9
- * `@/lib/elevated` → `../../../primitives/surface`; `@/components/ui/scroll-area`
10
- * → `../../../primitives/scroll-area`; `@/components/ui/fluid-hover-highlight` →
11
- * `../../../lib/fluid-hover-highlight`; `@/components/ui/menu-item` and
12
- * `@/components/ui/dropdown-search` → `./menu-item`, `./dropdown-search`
13
- * (both vendored into this folder).
14
- * - Uncontrolled selection (Base UI's contract) on `Dropdown` and
15
- * `Dropdown.Content`: `defaultCheckedIndex` / `defaultCheckedIndices` plus
16
- * `onCheckedIndexChange` / `onCheckedIndicesChange`, through
17
- * `useControllableState`. With a `default*` the panel owns the selection,
18
- * so rows need no `checked` or `onSelect` of their own. `checkedIndex` /
19
- * `checkedIndices` keep working exactly as upstream.
20
- * - Uncontrolled search: `Dropdown.Search` with `filter` hands the query to
21
- * the panel, which drops the `Dropdown.Item` rows whose `label` doesn't
22
- * match, re-indexes the survivors for fluid hover, and shows
23
- * `Dropdown.Empty` only when nothing matched — the work the controlled
24
- * form asks the consumer to do. Selection stays keyed on the authored
25
- * index (`sourceIndex` on the row), so it survives re-indexing.
26
- * - `DropdownProps` members re-declared one per line with the FF docs
27
- * API-table descriptions (modo's props table lists only members declared in
28
- * the interface body).
29
- * - `items[next].focus()` guarded for `noUncheckedIndexedAccess`.
30
- * - Compound statics `Dropdown.Menu/.Trigger/.Content/.Item/.Label/
31
- * .Separator/.Search/.Empty` attached with `Object.assign`, typed through a
32
- * `DropdownComponent` cast on the forwardRef. Upstream's named exports are
33
- * kept.
34
- * - modo item: TSDoc + examples from the FF "Dropdown" docs page (its
35
- * "Create from the query" section is skipped — adding a row to the list is
36
- * real consumer state, not something an uncontrolled panel can stand in
37
- * for), and a default export.
38
- * - Styling reads DS tokens (AGENTS.md styling): `text-[Npx]` →
39
- * `text-<role>[-compact]`; the hex focus-ring fallback →
40
- * `ring-focus-ring` / `border-focus-ring`; `duration-80|120|160` and
41
- * tier-length JS durations → `duration-<tier>` / `spring.*`.
42
- * - The popup menu morphs out of its trigger through the shared morph layer
43
- * (`lib/use-morph.ts` + `MorphSurface`, goo by default on
44
- * `spring.goo`; `Dropdown.Content` takes `from` / `effect` /
45
- * `hideSource` / `tier`), the origin captured from `Menu.Root`'s
46
- * `onOpenChange`. The `scaleY` motion wrapper and the `actionsRef` deferred
47
- * unmount are gone: Menu ignores `actionsRef`, and the wrapper animated
48
- * outside `Menu.Popup`, so Base UI unmounted the menu before its exit
49
- * played. The popup paints its level through the morph's surface layers
50
- * instead of `render={<Elevated/>}`, re-providing it with `SurfaceProvider`.
51
- * - `renderMenuItem` passes the row's activation to the Menu item primitive's
52
- * `onClick` (see menu-item.tsx), so Enter and Space pick a row instead of
53
- * only closing the menu.
54
- * - `effect="morph"` with `hideSource` opens the menu in place over its
55
- * trigger: the positioner's `sideOffset` becomes the engine's `inPlaceOffset`.
56
- * - shadcn DropdownMenu parity (local parts in `menu-parts.tsx`, numbering in
57
- * `row-order.tsx`): children-based rows with an optional `index` (both
58
- * panels provide `RowOrderContext`), `Dropdown.Group`, `Dropdown.Shortcut`,
59
- * value-keyed `Dropdown.CheckboxItem` and `Dropdown.RadioGroup` /
60
- * `Dropdown.RadioItem` (their checked rows join the merged selection
61
- * background), and submenus (`Dropdown.Sub` / `.SubTrigger` /
62
- * `.SubContent`). `Dropdown.Label` takes `inset` and names a surrounding
63
- * group (`Menu.GroupLabel`); labels and separators take a place in the row
64
- * order so selection runs never melt across them.
65
- * - `Dropdown.Trigger` also takes Radix-style `asChild` (`lib/slot.ts`).
66
- * - Radix-parity options, all optional: `Dropdown.Menu` forwards Base UI's
67
- * change details (`reason`, `cancel()`) to `onOpenChange` and takes `modal`;
68
- * `Dropdown.Content` / `Dropdown.SubContent` take `finalFocus`,
69
- * `initialFocus` (modo's own: Base UI 1.4.1's Menu.Popup has none),
70
- * `collisionPadding`, `positionerClassName`, `container`, and spread HTML
71
- * attributes onto the popup (the panel's own handlers run after theirs);
72
- * `Dropdown.Content` also takes `alignOffset` and `collisionBoundary`;
73
- * `Dropdown.Item` takes `render` / `asChild` and `textValue`, the other row
74
- * parts `textValue`.
75
- * - `initialFocus={false}` only drops modo's own first-row focus. Base UI still
76
- * focuses the first row on a keyboard open and the popup on a pointer open;
77
- * that initial focus can't be suppressed.
78
- * - `onKeyDownCapture` on `Dropdown.Content` / `Dropdown.SubContent` runs first.
79
- * `preventDefault` there is harmless: navigation and typeahead still run.
80
- * `stopPropagation` there stops Base UI's key handling and the panel's own
81
- * arrow navigation (both bubble-phase); the panel's capture-phase bookkeeping
82
- * (the keyboard ring, typing redirected into `Dropdown.Search`) still runs.
83
- * - The popup panel shares `useMenuPanel` with `Dropdown.SubContent`: a
84
- * keyboard focus ring gated on keyboard use (as in Select), the highlight
85
- * pinned on a submenu trigger while its submenu is open, and handlers that
86
- * ignore events bubbling up (through the portal) from a submenu. Its
87
- * `renderMenuItem` is the shared `renderPopupItem`.
88
- */
89
-
90
- import type { MenuRootChangeEventDetails, MenuTriggerProps } from '@base-ui/react/menu'
91
- // Aliased: a bare `Menu` import makes the .d.ts bundler rename the `Menu` static too
92
- import { Menu as BaseMenu } from '@base-ui/react/menu'
93
- import { AnimatePresence, motion } from 'motion/react'
94
- import {
95
- Children,
96
- type ComponentProps,
97
- cloneElement,
98
- createContext,
99
- type ForwardRefExoticComponent,
100
- forwardRef,
101
- type HTMLAttributes,
102
- isValidElement,
103
- type ReactElement,
104
- type ReactNode,
105
- type RefAttributes,
106
- type RefObject,
107
- useCallback,
108
- useContext,
109
- useEffect,
110
- useImperativeHandle,
111
- useMemo,
112
- useRef,
113
- useState,
114
- } from 'react'
115
- import { FluidHoverHighlight } from '../../../lib/fluid-hover-highlight'
116
- import { FocusRing } from '../../../lib/focus-ring'
117
- import { MorphSurface } from '../../../lib/morph-layers'
118
- import { isDisabledRow, popupScrollAreaClass, popupViewportClass } from '../../../lib/popup'
119
- import { RowOrderContext, useRowOrder, useRowSlot } from '../../../lib/row-order'
120
- import { shapeMap } from '../../../lib/shape-context'
121
- import { type SizeVariant, useSize } from '../../../lib/size-context'
122
- import { slotId } from '../../../lib/slot'
123
- import { spring } from '../../../lib/springs'
124
- import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
125
- import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
126
- import { useControllableState } from '../../../lib/use-controllable-state'
127
- import { type ItemRect, useFluidHover } from '../../../lib/use-fluid-hover'
128
- import { SelectionBackgrounds, useMergeSplitBlocks, useSelectionRuns } from '../../../lib/use-merge-split'
129
- import { inPlaceOffset, type MorphOrigin, opensInPlace, useMorph, useMorphOrigin } from '../../../lib/use-morph'
130
- import { cn } from '../../../lib/utils'
131
- import { ScrollArea } from '../../../primitives/scroll-area'
132
- import { SizeProvider } from '../../../primitives/sizes'
133
- import { Elevated } from '../../../primitives/surface'
134
- import {
135
- DropdownEmpty,
136
- DropdownFilterContext,
137
- type DropdownFilterContextValue,
138
- DropdownSearch,
139
- DropdownSearchHostContext,
140
- type DropdownSearchProps,
141
- useDropdownSearchHost,
142
- } from './dropdown-search'
143
- import {
144
- DropdownContext,
145
- type DropdownContextValue,
146
- DropdownShortcut,
147
- type DropdownShortcutProps,
148
- INSET_PX,
149
- MenuItem,
150
- type MenuItemProps,
151
- type MenuItemRenderOptions,
152
- useDropdown,
153
- useDropdownMaybe,
154
- } from './menu-item'
155
- import {
156
- chain,
157
- DropdownCheckboxItem,
158
- type DropdownCheckboxItemProps,
159
- DropdownGroup,
160
- DropdownGroupContext,
161
- type DropdownGroupProps,
162
- DropdownRadioGroup,
163
- type DropdownRadioGroupProps,
164
- DropdownRadioItem,
165
- type DropdownRadioItemProps,
166
- DropdownSub,
167
- DropdownSubContent,
168
- type DropdownSubContentProps,
169
- type DropdownSubProps,
170
- DropdownSubTrigger,
171
- type DropdownSubTriggerProps,
172
- type MenuPopupProps,
173
- type MenuPortalProps,
174
- mergeRefs,
175
- ownEvent,
176
- renderPopupItem,
177
- useMenuPanel,
178
- useValueChecked,
179
- } from './menu-parts'
180
-
181
- // Dropdown opts out of the global pill/rounded shape context — popover surfaces
182
- // look cleaner with the smaller "rounded" radii regardless of how the rest of
183
- // the UI is shaped (the heavy pill bubbling distorts perceived padding at this
184
- // scale and produces the corner-shadow asymmetry).
185
- const shape = shapeMap.rounded
186
-
187
- // ---------------------------------------------------------------------------
188
- // Panel context — shared by the inline Dropdown and the popup DropdownContent.
189
- //
190
- // The context object itself lives in menu-item.tsx so MenuItem resolves
191
- // whichever dropdown provider actually wraps it, even when dropdowns built
192
- // on different primitives render side by side. Re-exported here so the
193
- // public dropdown API is unchanged.
194
- // ---------------------------------------------------------------------------
195
-
196
- export type { DropdownContextValue, MenuItemRenderOptions }
197
- export { useDropdown, useDropdownMaybe }
198
-
199
- /* "Nothing checked", so useControllableState always sees a defined value
200
- while the caller drives the selection (`undefined` is what it reads as
201
- "uncontrolled"). */
202
- const NONE = -1
203
- const NO_INDICES: number[] = []
204
- /* Stable empty rect list, so a closed popup's merge/split pass gets the same
205
- array every render instead of a fresh literal. */
206
- const NO_RECTS: ItemRect[] = []
207
-
208
- interface PanelSelectionProps {
209
- checkedIndex?: number
210
- defaultCheckedIndex?: number
211
- onCheckedIndexChange?: (index: number | undefined) => void
212
- checkedIndices?: number[]
213
- defaultCheckedIndices?: number[]
214
- onCheckedIndicesChange?: (indices: number[]) => void
215
- }
216
-
217
- /*
218
- * The panel's selection, in the two index spaces it lives in: every prop and
219
- * callback speaks the AUTHORED index (what a row was written with), while the
220
- * overlays and the row context speak the RENDERED index (what a filtering
221
- * panel re-indexed the visible rows to). They are the same list unless a
222
- * `Dropdown.Search filter` is dropping rows.
223
- */
224
- function usePanelSelection(
225
- {
226
- checkedIndex,
227
- defaultCheckedIndex,
228
- onCheckedIndexChange,
229
- checkedIndices,
230
- defaultCheckedIndices,
231
- onCheckedIndicesChange,
232
- }: PanelSelectionProps,
233
- sourceOf: number[],
234
- filtering: boolean,
235
- ) {
236
- const [single, setSingle] = useControllableState<number>(
237
- defaultCheckedIndex === undefined ? (checkedIndex ?? NONE) : undefined,
238
- defaultCheckedIndex ?? NONE,
239
- index => onCheckedIndexChange?.(index === NONE ? undefined : index),
240
- )
241
- const [many, setMany] = useControllableState<number[]>(
242
- defaultCheckedIndices === undefined ? (checkedIndices ?? NO_INDICES) : undefined,
243
- defaultCheckedIndices ?? NO_INDICES,
244
- indices => onCheckedIndicesChange?.(indices),
245
- )
246
-
247
- const multiple = checkedIndices != null || defaultCheckedIndices != null
248
- // Only a `default*` makes the panel answer for rows that brought no
249
- // `checked` of their own.
250
- const selfManaged = defaultCheckedIndex !== undefined || defaultCheckedIndices !== undefined
251
-
252
- const toggleIndex = useCallback(
253
- (source: number) => {
254
- if (multiple) {
255
- setMany(prev =>
256
- prev.includes(source) ? prev.filter(i => i !== source) : [...prev, source].sort((a, b) => a - b),
257
- )
258
- return
259
- }
260
- setSingle(source)
261
- },
262
- [multiple, setMany, setSingle],
263
- )
264
-
265
- const renderedIndex = useMemo(() => {
266
- if (single === NONE) return undefined
267
- const i = filtering ? sourceOf.indexOf(single) : single
268
- return i === -1 ? undefined : i
269
- }, [single, filtering, sourceOf])
270
-
271
- const renderedIndices = useMemo(() => {
272
- if (!multiple) return undefined
273
- if (!filtering) return many
274
- return many.map(s => sourceOf.indexOf(s)).filter(i => i !== -1)
275
- }, [multiple, many, filtering, sourceOf])
276
-
277
- return {
278
- checkedIndex: renderedIndex,
279
- checkedIndices: renderedIndices,
280
- multiple,
281
- selfManaged,
282
- toggleIndex,
283
- }
284
- }
285
-
286
- interface PanelRows {
287
- /** What to render: the rows re-indexed and `Dropdown.Empty` dropped while
288
- * anything matched, or the children untouched when nothing filters. */
289
- content: ReactNode
290
- /** Rendered index → authored index. Empty while nothing filters. */
291
- sourceOf: number[]
292
- filtering: boolean
293
- /** The query to publish to a `Dropdown.Search filter`, or null. */
294
- filterCtx: DropdownFilterContextValue | null
295
- }
296
-
297
- /*
298
- * Uncontrolled search. A `Dropdown.Search` with `filter` among the panel's
299
- * direct children hands the panel the query; the panel then keeps only the
300
- * rows (anything with a string `label` and a numeric `index` — MenuItem, or a
301
- * wrapper that forwards both) whose label contains it, re-indexes them from 0
302
- * for fluid hover, and stamps each with the index it was authored with so the
303
- * selection survives. `Dropdown.Empty` children are held back and rendered
304
- * only when no row matched.
305
- */
306
- function usePanelRows(children: ReactNode): PanelRows {
307
- const [query, setQuery] = useState('')
308
-
309
- const filtering = useMemo(
310
- () =>
311
- Children.toArray(children).some(
312
- child => isValidElement(child) && child.type === DropdownSearch && (child.props as { filter?: boolean }).filter,
313
- ),
314
- [children],
315
- )
316
-
317
- const filterCtx = useMemo(() => ({ query, setQuery }), [query])
318
-
319
- const { content, sourceOf } = useMemo(() => {
320
- if (!filtering) return { content: children, sourceOf: NO_INDICES }
321
- const needle = query.trim().toLowerCase()
322
- const rendered: ReactNode[] = []
323
- const empties: ReactNode[] = []
324
- const sources: number[] = []
325
- for (const child of Children.toArray(children)) {
326
- if (!isValidElement(child)) {
327
- rendered.push(child)
328
- continue
329
- }
330
-
331
- if (child.type === DropdownEmpty) {
332
- empties.push(child)
333
- continue
334
- }
335
-
336
- const rowProps = child.props as { label?: unknown; index?: unknown }
337
- if (typeof rowProps.label !== 'string' || typeof rowProps.index !== 'number') {
338
- rendered.push(child)
339
- continue
340
- }
341
-
342
- if (needle && !rowProps.label.toLowerCase().includes(needle)) continue
343
-
344
- rendered.push(
345
- cloneElement(child as ReactElement<Record<string, unknown>>, {
346
- index: sources.length,
347
- sourceIndex: rowProps.index,
348
- }),
349
- )
350
- sources.push(rowProps.index)
351
- }
352
- if (sources.length === 0) rendered.push(...empties)
353
- return { content: rendered as ReactNode, sourceOf: sources }
354
- }, [children, filtering, query])
355
-
356
- return { content, sourceOf, filtering, filterCtx: filtering ? filterCtx : null }
357
- }
358
-
359
- // ---------------------------------------------------------------------------
360
- // Dropdown (inline panel)
361
- //
362
- // An always-rendered panel — no trigger, positioning, or dismissal. Because it
363
- // sits statically in the page it does NOT claim popup menu semantics: the
364
- // container is a plain role="group" (pass `aria-label` to name it). The real
365
- // role="menu" lives on the popup DropdownContent below, which Base UI wires to
366
- // a trigger. Consumers who hand-roll a trigger around the inline panel get
367
- // grouping semantics rather than a falsely-announced popup menu.
368
- // ---------------------------------------------------------------------------
369
-
370
- interface DropdownProps extends HTMLAttributes<HTMLDivElement> {
371
- /** `Dropdown.Item` rows, plus `Dropdown.Label` and `Dropdown.Separator`. */
372
- children: ReactNode
373
- /** The checked row: drives the animated selected background and the rows' radio semantics. */
374
- checkedIndex?: number
375
- /** Uncontrolled twin of `checkedIndex`: the panel owns the selection, so rows need no `checked` or `onSelect` of their own. */
376
- defaultCheckedIndex?: number
377
- /** Called with the newly checked row, or `undefined` when nothing is checked. */
378
- onCheckedIndexChange?: (index: number | undefined) => void
379
- /** Multiple selection: the checked rows. Rows become checkbox items and contiguous runs share one merged background (see Checkbox.Group). */
380
- checkedIndices?: number[]
381
- /** Uncontrolled twin of `checkedIndices`: the panel toggles rows itself. */
382
- defaultCheckedIndices?: number[]
383
- /** Called with every checked row after a toggle. */
384
- onCheckedIndicesChange?: (indices: number[]) => void
385
- /** Pins the panel's rows to one step of the size ladder (default 36px, compact 28px — see Sizes). Omitted, they follow the surrounding SizeProvider. */
386
- size?: SizeVariant
387
- /** Extra classes for the panel surface — it is 18rem wide by default. */
388
- className?: string
389
- }
390
-
391
- type DropdownComponent = ForwardRefExoticComponent<DropdownProps & RefAttributes<HTMLDivElement>> & {
392
- Menu: typeof DropdownMenu
393
- Trigger: typeof DropdownTrigger
394
- Content: typeof DropdownContent
395
- Item: typeof MenuItem
396
- Label: typeof DropdownLabel
397
- Separator: typeof DropdownSeparator
398
- Search: typeof DropdownSearch
399
- Empty: typeof DropdownEmpty
400
- Group: typeof DropdownGroup
401
- Shortcut: typeof DropdownShortcut
402
- CheckboxItem: typeof DropdownCheckboxItem
403
- RadioGroup: typeof DropdownRadioGroup
404
- RadioItem: typeof DropdownRadioItem
405
- Sub: typeof DropdownSub
406
- SubTrigger: typeof DropdownSubTrigger
407
- SubContent: typeof DropdownSubContent
408
- }
409
-
410
- /**
411
- * A menu in two forms: an inline panel that sits in the page, or a popup on
412
- * any trigger you like.
413
- *
414
- * `Dropdown` is the inline panel — an elevated surface with the fluid hover
415
- * background gliding between rows, a selected background that springs to the
416
- * picked one, contiguous picks merging into a single block in multiple mode,
417
- * and an animated focus ring. `Dropdown.Menu` + `Dropdown.Trigger` +
418
- * `Dropdown.Content` is the same panel as a popup on Base UI's Menu, which
419
- * brings the positioning, dismissal, roving highlight, typeahead and
420
- * close-on-select; the popup oozes out of its trigger and back through the
421
- * shared morph (see Morph), goo by default. Both take `checkedIndex` / `checkedIndices` to be driven
422
- * from outside, or `defaultCheckedIndex` / `defaultCheckedIndices` to run
423
- * themselves — and a `Dropdown.Search` marked `filter` makes the panel own
424
- * the query too, keeping only the rows whose `label` matches and showing
425
- * `Dropdown.Empty` when none do.
426
- *
427
- * Rows also come shadcn-style: children instead of a `label`, no `index` (a
428
- * row takes its place in document order, so the hover, the keyboard ring and
429
- * the selection backgrounds still find it, inside groups and behind
430
- * conditions alike), `inset` to line up with rows that have an icon and a
431
- * trailing `shortcut`. `Dropdown.CheckboxItem` and `Dropdown.RadioGroup` /
432
- * `Dropdown.RadioItem` select by their own state and value instead of by
433
- * index, their checked rows melting into one background like a multiple
434
- * selection. A `Dropdown.Sub` opens a submenu from its trigger row (ArrowRight,
435
- * a press, or a resting pointer) that grows out of that row and lifts above
436
- * the menu, with a highlight and a keyboard ring of its own. Every popup
437
- * lifts 2 surface levels off whatever it opens over, a Dialog included.
438
- *
439
- * Statics:
440
- * - `Dropdown.Menu` — the popup root: `open`, `defaultOpen`, `onOpenChange`, `disabled`, `size`.
441
- * - `Dropdown.Trigger` — the trigger: `render` or `asChild`.
442
- * - `Dropdown.Content` — the popup panel: `side`, `align`, `sideOffset`, the
443
- * index selection, and the morph options `from`, `effect`, `hideSource`, `tier`.
444
- * - `Dropdown.Item` — a row: `label` or `children`, `index`, `icon`, `inset`,
445
- * `shortcut`, `checked`, `onSelect`, `closeOnClick`, `disabled`.
446
- * - `Dropdown.CheckboxItem` — a row toggling itself: `checked`, `defaultChecked`, `onCheckedChange`.
447
- * - `Dropdown.RadioGroup` / `Dropdown.RadioItem` — one choice by value: `value`,
448
- * `defaultValue`, `onValueChange` on the group, `value` on each row.
449
- * - `Dropdown.Group` / `Dropdown.Label` — a named run of rows and its caption (`inset`).
450
- * - `Dropdown.Separator` — a rule between runs.
451
- * - `Dropdown.Shortcut` — a muted caption at a row's end.
452
- * - `Dropdown.Sub` / `Dropdown.SubTrigger` / `Dropdown.SubContent` — a submenu:
453
- * its state, its trigger row (`openOnHover`, `delay`), its panel (`side`,
454
- * `align`, `sideOffset`, `alignOffset`, `from`, `effect`, `tier`).
455
- * - `Dropdown.Search` / `Dropdown.Empty` — a filter field and its no-match row.
456
- *
457
- * @example {@include ./examples.mdx}
458
- */
459
- const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(
460
- (
461
- {
462
- children,
463
- checkedIndex,
464
- defaultCheckedIndex,
465
- onCheckedIndexChange,
466
- checkedIndices,
467
- defaultCheckedIndices,
468
- onCheckedIndicesChange,
469
- size,
470
- className,
471
- ...props
472
- },
473
- ref,
474
- ) => {
475
- const containerRef = useRef<HTMLDivElement>(null)
476
- const hover = useFluidHover(containerRef, { isItemDisabled: isDisabledRow })
477
- const { activeIndex, setActiveIndex, itemRects, handlers, registerItem } = hover
478
-
479
- const [focusedIndex, setFocusedIndex] = useState<number | null>(null)
480
- const rowOrder = useRowOrder()
481
- const { valueChecked, reportChecked } = useValueChecked()
482
-
483
- const rows = usePanelRows(children)
484
- const selection = usePanelSelection(
485
- {
486
- checkedIndex,
487
- defaultCheckedIndex,
488
- onCheckedIndexChange,
489
- checkedIndices,
490
- defaultCheckedIndices,
491
- onCheckedIndicesChange,
492
- },
493
- rows.sourceOf,
494
- rows.filtering,
495
- )
496
-
497
- const multiple = selection.multiple
498
- const checkedRect = !multiple && selection.checkedIndex != null ? itemRects[selection.checkedIndex] : null
499
- const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null
500
- // Multiple: one merged block per contiguous run of checked rows, value-
501
- // checked rows (CheckboxItem, RadioItem) included.
502
- const runs = useSelectionRuns(
503
- useMemo(
504
- () => [...(selection.checkedIndices ?? NO_INDICES), ...valueChecked],
505
- [selection.checkedIndices, valueChecked],
506
- ),
507
- )
508
- const blocks = useMergeSplitBlocks(runs, itemRects, shape.bgRadius)
509
- const panelCtx = useMemo(
510
- () => ({
511
- registerItem,
512
- activeIndex,
513
- checkedIndex: selection.checkedIndex,
514
- multiple,
515
- checkedIndices: selection.checkedIndices,
516
- selfManaged: selection.selfManaged,
517
- toggleIndex: selection.toggleIndex,
518
- reportChecked,
519
- }),
520
- [
521
- registerItem,
522
- activeIndex,
523
- selection.checkedIndex,
524
- multiple,
525
- selection.checkedIndices,
526
- selection.selfManaged,
527
- selection.toggleIndex,
528
- reportChecked,
529
- ],
530
- )
531
- const panel = (
532
- <RowOrderContext.Provider value={rowOrder}>
533
- <DropdownContext.Provider value={panelCtx}>
534
- <DropdownFilterContext.Provider value={rows.filterCtx}>
535
- <Elevated
536
- offset={2}
537
- shadowLevel={3}
538
- ref={node => {
539
- ;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
540
- if (typeof ref === 'function') ref(node)
541
- else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
542
- }}
543
- onMouseEnter={handlers.onMouseEnter}
544
- onMouseMove={handlers.onMouseMove}
545
- onMouseLeave={handlers.onMouseLeave}
546
- onClick={handlers.onClick}
547
- onFocus={e => {
548
- const indexAttr = (e.target as HTMLElement)
549
- .closest('[data-fluid-hover-index]')
550
- ?.getAttribute('data-fluid-hover-index')
551
- if (indexAttr != null) {
552
- const idx = Number(indexAttr)
553
- setActiveIndex(idx)
554
- setFocusedIndex((e.target as HTMLElement).matches(':focus-visible') ? idx : null)
555
- }
556
- }}
557
- onBlur={e => {
558
- if (containerRef.current?.contains(e.relatedTarget as Node)) return
559
- setFocusedIndex(null)
560
- setActiveIndex(null)
561
- }}
562
- onKeyDown={e => {
563
- const items = Array.from(
564
- containerRef.current?.querySelectorAll(
565
- '[role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"]',
566
- ) ?? [],
567
- ) as HTMLElement[]
568
- const currentIdx = items.indexOf(e.target as HTMLElement)
569
- if (currentIdx === -1) return
570
-
571
- if (['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft'].includes(e.key)) {
572
- e.preventDefault()
573
- const next = ['ArrowDown', 'ArrowRight'].includes(e.key)
574
- ? (currentIdx + 1) % items.length
575
- : (currentIdx - 1 + items.length) % items.length
576
- items[next]?.focus()
577
- } else if (e.key === 'Home') {
578
- e.preventDefault()
579
- items[0]?.focus()
580
- } else if (e.key === 'End') {
581
- e.preventDefault()
582
- items[items.length - 1]?.focus()
583
- }
584
- }}
585
- role="group"
586
- className={cn(`relative flex flex-col w-72 max-w-full p-1 select-none`, className)}
587
- {...props}
588
- >
589
- {/* Selected backgrounds — merged runs in multiple mode and value-checked rows */}
590
- <SelectionBackgrounds blocks={blocks} />
591
-
592
- {/* Selected background */}
593
- <AnimatePresence>
594
- {checkedRect && (
595
- <motion.div
596
- className={`absolute ${shape.bg} bg-active pointer-events-none`}
597
- initial={false}
598
- animate={{
599
- top: checkedRect.top,
600
- left: checkedRect.left,
601
- width: checkedRect.width,
602
- height: checkedRect.height,
603
- opacity: 1,
604
- }}
605
- exit={{ opacity: 0, transition: spring.moderate.exit }}
606
- transition={{
607
- ...spring.moderate,
608
- opacity: { duration: spring.fast.duration },
609
- }}
610
- />
611
- )}
612
- </AnimatePresence>
613
-
614
- {/* Hover background */}
615
- <FluidHoverHighlight hover={hover} from={checkedRect} className={shape.bg} />
616
-
617
- {/* Focus ring */}
618
- <AnimatePresence>
619
- {focusRect && (
620
- <motion.div
621
- className={`absolute ${shape.focusRing} pointer-events-none z-20 border border-focus-ring`}
622
- initial={false}
623
- animate={{
624
- left: focusRect.left - 2,
625
- top: focusRect.top - 2,
626
- width: focusRect.width + 4,
627
- height: focusRect.height + 4,
628
- }}
629
- exit={{ opacity: 0, transition: spring.fast.exit }}
630
- transition={{
631
- ...spring.fast,
632
- opacity: { duration: spring.fast.duration },
633
- }}
634
- />
635
- )}
636
- </AnimatePresence>
637
-
638
- {rows.content}
639
- </Elevated>
640
- </DropdownFilterContext.Provider>
641
- </DropdownContext.Provider>
642
- </RowOrderContext.Provider>
643
- )
644
-
645
- // A size prop pins every row in the panel to one ladder step.
646
- return size ? <SizeProvider size={size}>{panel}</SizeProvider> : panel
647
- },
648
- ) as DropdownComponent
649
-
650
- Dropdown.displayName = 'Dropdown'
651
-
652
- // ---------------------------------------------------------------------------
653
- // DropdownMenu (popup root)
654
- //
655
- // Built on Base UI's Menu primitive, which owns the trigger wiring,
656
- // positioning (collision flipping, anchor tracking), dismissal (outside
657
- // press, focus-out, Escape), roving highlight, typeahead, and close-on-select.
658
- // This layer keeps the fluid-hover overlays and the morph open/close
659
- // (lib/use-morph.ts) — the same pattern as select.tsx.
660
- // ---------------------------------------------------------------------------
661
-
662
- interface DropdownMenuContextValue {
663
- open: boolean
664
- origin: RefObject<MorphOrigin>
665
- }
666
-
667
- const DropdownMenuContext = createContext<DropdownMenuContextValue | null>(null)
668
-
669
- function useDropdownMenuContext() {
670
- const ctx = useContext(DropdownMenuContext)
671
- if (!ctx) throw new Error('DropdownMenu compound components must be inside <DropdownMenu>')
672
- return ctx
673
- }
674
-
675
- interface DropdownMenuProps {
676
- /** `Dropdown.Trigger` and `Dropdown.Content`. */
677
- children: ReactNode
678
- /** Controlled open state. Pair with `onOpenChange`. */
679
- open?: boolean
680
- /** Initial open state — uncontrolled. Defaults to `false`. */
681
- defaultOpen?: boolean
682
- /** Called when the menu opens or closes, with Base UI's event details: `reason` (why: `'escape-key'`, `'outside-press'`, `'focus-out'`, `'item-press'`, `'trigger-press'`…), `event`, and `cancel()`, which keeps the menu as it is. A callback that takes only `open` keeps working. */
683
- onOpenChange?: (open: boolean, details: MenuRootChangeEventDetails) => void
684
- /** Disables the menu. Defaults to `false`. */
685
- disabled?: boolean
686
- /** Locks the page scroll while the menu is open and keeps the pointer off everything outside it. Non-modal, the page keeps scrolling and the popup follows its trigger. Defaults to `false`. */
687
- modal?: boolean
688
- /** Pins trigger-side content and the portalled popup rows to one step of the size ladder (default 36px, compact 28px — see Sizes). Omitted, they follow the surrounding SizeProvider. */
689
- size?: SizeVariant
690
- }
691
-
692
- function DropdownMenu({
693
- children,
694
- open: openProp,
695
- defaultOpen = false,
696
- onOpenChange,
697
- disabled = false,
698
- modal = false,
699
- size,
700
- }: DropdownMenuProps) {
701
- const [internalOpen, setInternalOpen] = useState(defaultOpen)
702
- const open = openProp !== undefined ? openProp : internalOpen
703
- const { origin, capture } = useMorphOrigin()
704
-
705
- const handleOpenChange = useCallback(
706
- (next: boolean, details: MenuRootChangeEventDetails) => {
707
- onOpenChange?.(next, details)
708
- if (details.isCanceled) return
709
- if (next) capture(details)
710
- if (openProp === undefined) setInternalOpen(next)
711
- },
712
- [openProp, onOpenChange, capture],
713
- )
714
-
715
- const ctx = useMemo(() => ({ open, origin }), [open, origin])
716
-
717
- // A size prop pins the whole compound (trigger content + portalled popup —
718
- // React context crosses portals) to one ladder step.
719
- const root = (
720
- <DropdownMenuContext.Provider value={ctx}>
721
- <BaseMenu.Root
722
- open={open}
723
- onOpenChange={handleOpenChange}
724
- disabled={disabled}
725
- // Non-modal by default: the page keeps scrolling and the Positioner
726
- // tracks the anchor, so the popup follows its trigger instead of detaching.
727
- modal={modal}
728
- >
729
- {children}
730
- </BaseMenu.Root>
731
- </DropdownMenuContext.Provider>
732
- )
733
-
734
- return size ? <SizeProvider size={size}>{root}</SizeProvider> : root
735
- }
736
-
737
- DropdownMenu.displayName = 'DropdownMenu'
738
-
739
- // ---------------------------------------------------------------------------
740
- // DropdownTrigger
741
- //
742
- // Base UI's Menu.Trigger under the library name. Composes via the `render`
743
- // prop, or Radix-style `asChild` onto its single child, so any element can
744
- // be the trigger:
745
- //
746
- // <DropdownTrigger render={<Button variant="secondary">Open</Button>} />
747
- // <DropdownTrigger asChild><Button variant="secondary">Open</Button></DropdownTrigger>
748
- // ---------------------------------------------------------------------------
749
-
750
- type DropdownTriggerProps = MenuTriggerProps & {
751
- /** Compose onto the single child instead of `render`. Defaults to `false`. */
752
- asChild?: boolean
753
- }
754
-
755
- const DropdownTrigger = forwardRef<HTMLButtonElement, DropdownTriggerProps>(
756
- ({ asChild, render, children, ...props }, ref) =>
757
- asChild && isValidElement(children) ? (
758
- <BaseMenu.Trigger
759
- ref={ref}
760
- render={children as ReactElement<Record<string, unknown>>}
761
- {...props}
762
- id={slotId(children, props.id)}
763
- />
764
- ) : (
765
- <BaseMenu.Trigger ref={ref} render={render} {...props} id={slotId(render, props.id)}>
766
- {children}
767
- </BaseMenu.Trigger>
768
- ),
769
- )
770
-
771
- DropdownTrigger.displayName = 'DropdownTrigger'
772
-
773
- // ---------------------------------------------------------------------------
774
- // DropdownContent (popup panel)
775
- //
776
- // Portal > Positioner > Popup carrying the exact inline-panel visuals: the
777
- // surface (painted by the morph layer), fluid-hover overlays, animated
778
- // selected background, and animated focus ring. Children are wrapped in a Menu.RadioGroup so
779
- // radio-style MenuItems (boolean `checked`) get correct aria-checked from
780
- // `checkedIndex`.
781
- // ---------------------------------------------------------------------------
782
-
783
- type MenuPositionerProps = ComponentProps<typeof BaseMenu.Positioner>
784
-
785
- interface DropdownContentProps extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
786
- /** `Dropdown.Item` rows, plus `Dropdown.Label`, `Dropdown.Separator`, `Dropdown.Search`, `Dropdown.Empty`. */
787
- children: ReactNode
788
- /** Extra classes for the popup surface. */
789
- className?: string
790
- /** Classes for the portalled positioner element: a z utility here lifts the menu above other fixed layers (the positioner is `z-50`), e.g. `z-[60]`. */
791
- positionerClassName?: string
792
- /** Index of the checked item. Drives the animated selected background and
793
- * the radio-group value announced to assistive tech. */
794
- checkedIndex?: number
795
- /** Uncontrolled twin of `checkedIndex`: the popup owns the selection, so rows need no `checked` or `onSelect` of their own. */
796
- defaultCheckedIndex?: number
797
- /** Called with the newly checked row, or `undefined` when nothing is checked. */
798
- onCheckedIndexChange?: (index: number | undefined) => void
799
- /** Multiple selection: the checked rows. Rows become checkbox items that
800
- * keep the menu open when toggled, and contiguous runs share one merged
801
- * background (see Checkbox.Group). */
802
- checkedIndices?: number[]
803
- /** Uncontrolled twin of `checkedIndices`: the popup toggles rows itself. */
804
- defaultCheckedIndices?: number[]
805
- /** Called with every checked row after a toggle. */
806
- onCheckedIndicesChange?: (indices: number[]) => void
807
- /** Side of the trigger the popup prefers. Defaults to `"bottom"`. */
808
- side?: MenuPositionerProps['side']
809
- /** Alignment against the trigger. Defaults to `"start"`. */
810
- align?: MenuPositionerProps['align']
811
- /** Gap to the trigger, in px. Ignored when the menu opens in place over its trigger (`effect="morph"` with `hideSource`). Defaults to `6`. */
812
- sideOffset?: number
813
- /** Shift along the trigger's edge, in px. Defaults to `0`. */
814
- alignOffset?: MenuPositionerProps['alignOffset']
815
- /** Space to keep between the menu and the edge of the collision boundary, in px (a number, or per side). Defaults to `5`. */
816
- collisionPadding?: MenuPositionerProps['collisionPadding']
817
- /** The element or rectangle the menu stays inside when it flips and shifts. Defaults to `'clipping-ancestors'`. */
818
- collisionBoundary?: MenuPositionerProps['collisionBoundary']
819
- /** The element the portalled menu mounts into, e.g. a Dialog's popup so it stays inside its focus trap. Defaults to `document.body`. */
820
- container?: MenuPortalProps['container']
821
- /** Where focus goes once the menu has closed: `false` leaves it, a ref moves it there (a text field after a pick), a function picks by how it closed. Defaults to `undefined`: Base UI's own return to the trigger. */
822
- finalFocus?: MenuPopupProps['finalFocus']
823
- /** An element to focus once the menu has opened, in place of the first row, or `false` to drop modo's own first-row focus. Base UI 1.4.1's Menu.Popup has no such option, so this is modo's: it acts a frame after the open. `false` does not silence Base UI's own initial focus, which still lands on the first row after a keyboard open and on the popup after a pointer open. The ref may be a fresh object each render: it is read when the menu opens, and only a change to or from `false` re-runs it. Defaults to `undefined`: the first row (the search field when there is one). */
824
- initialFocus?: boolean | RefObject<HTMLElement | null>
825
- /** Key handler on the popup, in the capture phase, ahead of the panel's own. `preventDefault` is harmless: navigation and typeahead still run. `stopPropagation` stops Base UI's key handling and the panel's arrow navigation; the keyboard ring and typing redirected into `Dropdown.Search` still run. Defaults to `undefined`. */
826
- onKeyDownCapture?: HTMLAttributes<HTMLDivElement>['onKeyDownCapture']
827
- /** Where the menu grows from (see Morph): the trigger, the pointer, its own center, a viewport edge, or a ref to any element. Defaults to `'trigger'`. */
828
- from?: 'trigger' | 'pointer' | 'center' | 'top' | 'right' | 'bottom' | 'left' | RefObject<HTMLElement | null>
829
- /** How it grows (see Morph): with the liquid goo neck, a plain morph, a slide or a fade. Defaults to `'goo'`. */
830
- effect?: 'goo' | 'morph' | 'slide' | 'fade'
831
- /** Hide the trigger while open, so it reads as turning into the menu. Defaults to `false`. */
832
- hideSource?: boolean
833
- /** Spring tier of a plain morph, slide or fade; goo runs on its own `spring.goo`. Defaults to `'moderate'`. */
834
- tier?: 'moderate' | 'slow'
835
- /** Keep the rows mounted (hidden) while the menu is closed, so rows that run their own state — `Dropdown.CheckboxItem defaultChecked`, `Dropdown.RadioGroup defaultValue`, an uncontrolled `Dropdown.Sub` — keep it across opens. Defaults to `false`. */
836
- keepMounted?: boolean
837
- }
838
-
839
- const DropdownContent = forwardRef<HTMLDivElement, DropdownContentProps>(
840
- (
841
- {
842
- className,
843
- children,
844
- checkedIndex,
845
- defaultCheckedIndex,
846
- onCheckedIndexChange,
847
- checkedIndices,
848
- defaultCheckedIndices,
849
- onCheckedIndicesChange,
850
- side = 'bottom',
851
- align = 'start',
852
- sideOffset = 6,
853
- alignOffset,
854
- collisionPadding,
855
- collisionBoundary,
856
- container,
857
- finalFocus,
858
- initialFocus,
859
- positionerClassName,
860
- from,
861
- effect,
862
- hideSource,
863
- tier = 'moderate',
864
- keepMounted = false,
865
- onKeyDownCapture,
866
- onMouseEnter,
867
- onMouseMove,
868
- onMouseLeave,
869
- onClick,
870
- onFocus,
871
- onBlur,
872
- ...popupProps
873
- },
874
- ref,
875
- ) => {
876
- const { open, origin } = useDropdownMenuContext()
877
- const morph = useMorph(open, origin, { from, effect, hideSource, tier })
878
- useImperativeHandle(ref, () => morph.popup as HTMLDivElement, [morph.popup])
879
- // Lifts 2 levels off its substrate with a fixed shadow (see Elevated).
880
- const level = Math.min(useSurface() + 2, 8)
881
- const containerRef = useRef<HTMLDivElement>(null)
882
-
883
- const panel = useMenuPanel(containerRef, open)
884
- const { hover, focusRow, setFocusedIndex } = panel
885
- const { setActiveIndex, itemRects, handlers, registerItem, remeasure } = hover
886
-
887
- const rows = usePanelRows(children)
888
- const selection = usePanelSelection(
889
- {
890
- checkedIndex,
891
- defaultCheckedIndex,
892
- onCheckedIndexChange,
893
- checkedIndices,
894
- defaultCheckedIndices,
895
- onCheckedIndicesChange,
896
- },
897
- rows.sourceOf,
898
- rows.filtering,
899
- )
900
-
901
- // An optional DropdownSearch child: typing on a focused row is
902
- // redirected into the field. (The field takes focus itself, a frame
903
- // after the primitive's own open autofocus.)
904
- const {
905
- host: searchHost,
906
- hasSearch,
907
- searchMounted,
908
- onKeyDownCapture: redirectTypingToSearch,
909
- isSearchField,
910
- highlightFirst,
911
- } = useDropdownSearchHost(open, { containerRef, setActiveIndex })
912
-
913
- // Open ready to act: focus the first enabled row (a mounted search field
914
- // takes focus itself instead). A frame after the primitive's own open
915
- // autofocus, which lands on the popup for pointer opens.
916
- // Read through a ref: a fresh `{ current }` object each render must not re-run the effect.
917
- const initialFocusRef = useRef(initialFocus)
918
- initialFocusRef.current = initialFocus
919
- const keepsFocus = initialFocus === false
920
- useEffect(() => {
921
- if (!open || keepsFocus) return
922
- let inner: number | undefined
923
- const outer = requestAnimationFrame(() => {
924
- inner = requestAnimationFrame(() => {
925
- const target = initialFocusRef.current
926
- if (typeof target === 'object') return target.current?.focus()
927
- if (hasSearch()) return
928
- const rows = containerRef.current
929
- if (!rows || (rows.contains(document.activeElement) && document.activeElement !== rows)) return
930
- const first = rows.querySelector<HTMLElement>(
931
- '[role="menuitem"]:not([aria-disabled="true"]), [role="menuitemradio"]:not([aria-disabled="true"]), [role="menuitemcheckbox"]:not([aria-disabled="true"])',
932
- )
933
- first?.focus()
934
- })
935
- })
936
- return () => {
937
- cancelAnimationFrame(outer)
938
- if (inner !== undefined) cancelAnimationFrame(inner)
939
- }
940
- }, [open, hasSearch, keepsFocus])
941
-
942
- // The popup keeps its rows registered between opens, so their rects
943
- // were taken while it was hidden: re-measure once it is open and laid out.
944
- useEffect(() => {
945
- if (!open) return
946
- remeasure()
947
- }, [open, remeasure])
948
-
949
- const multiple = selection.multiple
950
- const checkedRect = !multiple && selection.checkedIndex != null ? itemRects[selection.checkedIndex] : null
951
- // Multiple: one merged block per contiguous run of checked rows, value-
952
- // checked rows (CheckboxItem, RadioItem) included.
953
- const runs = useSelectionRuns(
954
- useMemo(
955
- () => [...(selection.checkedIndices ?? NO_INDICES), ...panel.valueChecked],
956
- [selection.checkedIndices, panel.valueChecked],
957
- ),
958
- )
959
- const blocks = useMergeSplitBlocks(runs, open ? itemRects : NO_RECTS, shape.bgRadius)
960
- // Inside the popup, Base UI's Menu.Item / Menu.RadioItem own the role,
961
- // aria-checked, tabIndex, roving highlight, typeahead, and Enter/Space/
962
- // click activation. Keyboard activation reaches only the primitive's own
963
- // onClick, so the row's activation rides there. The render div carries the
964
- // Fluid Functionalism visuals and the fluid-hover registration.
965
- // (`renderPopupItem`, shared with Dropdown.SubContent.)
966
-
967
- const contentCtx = useMemo(
968
- () => ({
969
- registerItem,
970
- activeIndex: panel.lit,
971
- checkedIndex: selection.checkedIndex,
972
- multiple,
973
- checkedIndices: selection.checkedIndices,
974
- inMenu: true,
975
- renderMenuItem: renderPopupItem,
976
- selfManaged: selection.selfManaged,
977
- toggleIndex: selection.toggleIndex,
978
- pin: panel.pin,
979
- reportChecked: panel.reportChecked,
980
- }),
981
- [
982
- registerItem,
983
- panel.lit,
984
- selection.checkedIndex,
985
- multiple,
986
- selection.checkedIndices,
987
- selection.selfManaged,
988
- selection.toggleIndex,
989
- panel.pin,
990
- panel.reportChecked,
991
- ],
992
- )
993
-
994
- return (
995
- <BaseMenu.Portal keepMounted={keepMounted} container={container}>
996
- <BaseMenu.Positioner
997
- side={side}
998
- align={align}
999
- sideOffset={opensInPlace({ effect, hideSource }) ? inPlaceOffset : sideOffset}
1000
- alignOffset={alignOffset}
1001
- collisionPadding={collisionPadding}
1002
- collisionBoundary={collisionBoundary}
1003
- className={cn('z-50 outline-none', positionerClassName)}
1004
- >
1005
- <DropdownContext.Provider value={contentCtx}>
1006
- <DropdownFilterContext.Provider value={rows.filterCtx}>
1007
- <DropdownSearchHostContext.Provider value={searchHost}>
1008
- <BaseMenu.Popup
1009
- {...popupProps}
1010
- ref={morph.popupRef}
1011
- finalFocus={finalFocus}
1012
- onKeyDownCapture={chain(onKeyDownCapture, e => {
1013
- // A submenu's keys bubble here through the portal.
1014
- if (!ownEvent(e)) return
1015
- panel.trackKeyboardNav(e)
1016
- redirectTypingToSearch(e)
1017
- })}
1018
- onMouseEnter={chain(onMouseEnter, () => {
1019
- handlers.onMouseEnter()
1020
- setFocusedIndex(null)
1021
- })}
1022
- onMouseMove={chain(onMouseMove, e => ownEvent(e) && handlers.onMouseMove(e))}
1023
- onClick={chain(onClick, e => ownEvent(e) && handlers.onClick(e))}
1024
- onMouseLeave={chain(onMouseLeave, () => {
1025
- handlers.onMouseLeave()
1026
- // The pointer's session is over; a focused search field
1027
- // gets its first-row highlight back.
1028
- if (isSearchField(document.activeElement)) highlightFirst()
1029
- })}
1030
- onFocus={chain(onFocus, e => {
1031
- // Keyboard navigation moves the hover background, and the
1032
- // ring once the keyboard earned it (useKeyboardNavGate).
1033
- if (!ownEvent(e) || focusRow(e)) return
1034
- if (isSearchField(e.target)) {
1035
- // The search field: the first row (what Enter picks)
1036
- // carries the highlight while it has focus.
1037
- highlightFirst()
1038
- } else if (e.target !== e.currentTarget) {
1039
- // Focus moved to some other non-row inside the popup: no
1040
- // row is highlighted any more. The popup focusing itself
1041
- // (pointer leaving a row) doesn't count.
1042
- setActiveIndex(null)
1043
- }
1044
- })}
1045
- onBlur={chain(onBlur, e => {
1046
- // The popup itself takes focus when the pointer leaves a row; only a
1047
- // departure from the whole popup ends the hover session.
1048
- if (!ownEvent(e) || e.currentTarget.contains(e.relatedTarget as Node)) return
1049
- setFocusedIndex(null)
1050
- setActiveIndex(null)
1051
- })}
1052
- className="relative select-none outline-none"
1053
- >
1054
- <SurfaceProvider value={level}>
1055
- <MorphSurface
1056
- morph={morph}
1057
- bg={SURFACE_BG[level]}
1058
- shadow={SURFACE_SHADOW[3]}
1059
- radius={shape.container}
1060
- className={cn(
1061
- // min-w tracks the trigger via the Positioner's
1062
- // --anchor-width var.
1063
- `flex flex-col w-72 max-w-full min-w-[var(--anchor-width)] max-h-[min(480px,var(--available-height))] overflow-hidden ${shape.container}`,
1064
- className,
1065
- )}
1066
- >
1067
- {/* The list scrolls inside a ScrollArea; this wrapper is the rows'
1068
- offsetParent, so the overlays scroll with them. */}
1069
- <ScrollArea
1070
- className={popupScrollAreaClass}
1071
- viewportClassName={cn(popupViewportClass, !searchMounted && 'scroll-fade')}
1072
- >
1073
- <div ref={containerRef} className="relative flex flex-col p-1">
1074
- {/* Selected backgrounds — merged runs in multiple mode and value-checked rows */}
1075
- <SelectionBackgrounds blocks={blocks} />
1076
-
1077
- {/* Selected background */}
1078
- <AnimatePresence>
1079
- {checkedRect && (
1080
- <motion.div
1081
- className={`absolute ${shape.bg} bg-active pointer-events-none`}
1082
- initial={false}
1083
- animate={{
1084
- top: checkedRect.top,
1085
- left: checkedRect.left,
1086
- width: checkedRect.width,
1087
- height: checkedRect.height,
1088
- opacity: 1,
1089
- }}
1090
- exit={{ opacity: 0, transition: spring.moderate.exit }}
1091
- transition={{
1092
- ...spring.moderate,
1093
- opacity: { duration: spring.fast.duration },
1094
- }}
1095
- />
1096
- )}
1097
- </AnimatePresence>
1098
-
1099
- {/* Hover background (pinned on an open submenu's trigger) */}
1100
- <FluidHoverHighlight hover={panel.shownHover} from={checkedRect} className={shape.bg} />
1101
-
1102
- {/* Focus ring, keyboard only */}
1103
- <FocusRing rect={open ? panel.focusRect : null} shape="rounded" />
1104
-
1105
- {/* display: contents keeps items direct flex children of the
1106
- wrapper so fluid hover measurement and gap layout still work,
1107
- while the group provides the radio value context. */}
1108
- <RowOrderContext.Provider value={panel.rowOrder}>
1109
- <BaseMenu.RadioGroup value={selection.checkedIndex ?? null} className="contents">
1110
- {rows.content}
1111
- </BaseMenu.RadioGroup>
1112
- </RowOrderContext.Provider>
1113
- </div>
1114
- </ScrollArea>
1115
- </MorphSurface>
1116
- </SurfaceProvider>
1117
- </BaseMenu.Popup>
1118
- </DropdownSearchHostContext.Provider>
1119
- </DropdownFilterContext.Provider>
1120
- </DropdownContext.Provider>
1121
- </BaseMenu.Positioner>
1122
- </BaseMenu.Portal>
1123
- )
1124
- },
1125
- )
1126
-
1127
- DropdownContent.displayName = 'DropdownContent'
1128
-
1129
- // ---------------------------------------------------------------------------
1130
- // DropdownLabel
1131
- // ---------------------------------------------------------------------------
1132
-
1133
- interface DropdownLabelProps extends HTMLAttributes<HTMLDivElement> {
1134
- /** The caption. */
1135
- children?: ReactNode
1136
- /** Indents the caption to the label column of rows with an icon. Defaults to `false`. */
1137
- inset?: boolean
1138
- /** Extra classes. */
1139
- className?: string
1140
- }
1141
-
1142
- const DropdownLabel = forwardRef<HTMLDivElement, DropdownLabelProps>(({ className, inset, ...props }, ref) => {
1143
- // Group labels are the caption role of the type scale — see /docs/sizes.
1144
- const size = useSize()
1145
- const own = useRef<HTMLDivElement>(null)
1146
- // A place in the row order, so a selection run never melts across it.
1147
- useRowSlot(own, undefined, true)
1148
- const classes = cn(
1149
- 'py-1.5 shrink-0 text-muted-foreground',
1150
- inset ? INSET_PX[size.variant] : 'px-2',
1151
- size.variant === 'compact' ? 'text-caption-compact' : 'text-caption',
1152
- className,
1153
- )
1154
- // Inside a Dropdown.Group it names the group.
1155
- return useContext(DropdownGroupContext) ? (
1156
- <BaseMenu.GroupLabel ref={mergeRefs(own, ref)} className={classes} {...props} />
1157
- ) : (
1158
- <div ref={mergeRefs(own, ref)} className={classes} {...props} />
1159
- )
1160
- })
1161
-
1162
- DropdownLabel.displayName = 'DropdownLabel'
1163
-
1164
- // ---------------------------------------------------------------------------
1165
- // DropdownSeparator
1166
- // ---------------------------------------------------------------------------
1167
-
1168
- const DropdownSeparator = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {
1169
- const own = useRef<HTMLDivElement>(null)
1170
- useRowSlot(own, undefined, true)
1171
- return (
1172
- <div
1173
- ref={mergeRefs(own, ref)}
1174
- role="separator"
1175
- className={cn('my-1 -mx-1 h-px shrink-0 bg-border/60', className)}
1176
- {...props}
1177
- />
1178
- )
1179
- })
1180
-
1181
- DropdownSeparator.displayName = 'DropdownSeparator'
1182
-
1183
- Object.assign(Dropdown, {
1184
- Menu: DropdownMenu,
1185
- Trigger: DropdownTrigger,
1186
- Content: DropdownContent,
1187
- Item: MenuItem,
1188
- Label: DropdownLabel,
1189
- Separator: DropdownSeparator,
1190
- Search: DropdownSearch,
1191
- Empty: DropdownEmpty,
1192
- Group: DropdownGroup,
1193
- Shortcut: DropdownShortcut,
1194
- CheckboxItem: DropdownCheckboxItem,
1195
- RadioGroup: DropdownRadioGroup,
1196
- RadioItem: DropdownRadioItem,
1197
- Sub: DropdownSub,
1198
- SubTrigger: DropdownSubTrigger,
1199
- SubContent: DropdownSubContent,
1200
- })
1201
-
1202
- // DropdownContextValue and MenuItemRenderOptions are already re-exported
1203
- // above next to their import — repeating them here is a duplicate-export
1204
- // build error.
1205
- export type {
1206
- DropdownCheckboxItemProps,
1207
- DropdownContentProps,
1208
- DropdownGroupProps,
1209
- DropdownLabelProps,
1210
- DropdownMenuProps,
1211
- DropdownProps,
1212
- DropdownRadioGroupProps,
1213
- DropdownRadioItemProps,
1214
- DropdownSearchProps,
1215
- DropdownShortcutProps,
1216
- DropdownSubContentProps,
1217
- DropdownSubProps,
1218
- DropdownSubTriggerProps,
1219
- DropdownTriggerProps,
1220
- MenuItemProps,
1221
- }
1222
- export {
1223
- Dropdown,
1224
- DropdownCheckboxItem,
1225
- DropdownContent,
1226
- DropdownEmpty,
1227
- DropdownGroup,
1228
- DropdownLabel,
1229
- DropdownMenu,
1230
- DropdownRadioGroup,
1231
- DropdownRadioItem,
1232
- DropdownSearch,
1233
- DropdownSeparator,
1234
- DropdownShortcut,
1235
- DropdownSub,
1236
- DropdownSubContent,
1237
- DropdownSubTrigger,
1238
- DropdownTrigger,
1239
- MenuItem,
1240
- }
1241
- export default Dropdown