@elabs-ai/components-ui 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (449) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/dist/index.d.ts +4497 -0
  4. package/dist/index.js +15192 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/cn.d.ts +10 -0
  7. package/dist/lib/cn.js +20 -0
  8. package/dist/lib/cn.js.map +1 -0
  9. package/package.json +106 -0
  10. package/src/blocks/sidebar-02/app-sidebar.tsx +160 -0
  11. package/src/blocks/sidebar-02/logo.tsx +18 -0
  12. package/src/blocks/sidebar-02/nav-main.tsx +13 -0
  13. package/src/blocks/sidebar-02/nav-notifications.tsx +7 -0
  14. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +23 -0
  15. package/src/blocks/sidebar-02/team-switcher.tsx +7 -0
  16. package/src/blocks/sidebar-04/app-sidebar.tsx +236 -0
  17. package/src/blocks/sidebar-04/mail-context.tsx +29 -0
  18. package/src/blocks/sidebar-04/nav-user.tsx +6 -0
  19. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +150 -0
  20. package/src/blocks/sidebar-05/app-sidebar.tsx +375 -0
  21. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +18 -0
  22. package/src/blocks/sidebar-05/team-switcher.tsx +6 -0
  23. package/src/blocks-comparison-table.stories.tsx +165 -0
  24. package/src/components/accordion/accordion.stories.tsx +71 -0
  25. package/src/components/accordion/accordion.test.tsx +18 -0
  26. package/src/components/accordion/accordion.tsx +49 -0
  27. package/src/components/accordion/index.ts +1 -0
  28. package/src/components/advanced-group/advanced-group.stories.tsx +143 -0
  29. package/src/components/advanced-group/advanced-group.test.tsx +81 -0
  30. package/src/components/advanced-group/advanced-group.tsx +111 -0
  31. package/src/components/advanced-group/index.ts +2 -0
  32. package/src/components/alert/alert.stories.tsx +156 -0
  33. package/src/components/alert/alert.test.tsx +57 -0
  34. package/src/components/alert/alert.tsx +91 -0
  35. package/src/components/alert/index.ts +8 -0
  36. package/src/components/alert-dialog/alert-dialog.stories.tsx +73 -0
  37. package/src/components/alert-dialog/alert-dialog.tsx +104 -0
  38. package/src/components/alert-dialog/index.ts +12 -0
  39. package/src/components/app-shell/app-shell.stories.tsx +61 -0
  40. package/src/components/app-shell/app-shell.tsx +31 -0
  41. package/src/components/app-shell/index.ts +1 -0
  42. package/src/components/app-sidebar/app-sidebar.stories.tsx +121 -0
  43. package/src/components/app-sidebar/app-sidebar.test.tsx +55 -0
  44. package/src/components/app-sidebar/app-sidebar.tsx +53 -0
  45. package/src/components/app-sidebar/index.ts +1 -0
  46. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +46 -0
  47. package/src/components/aspect-ratio/aspect-ratio.tsx +3 -0
  48. package/src/components/aspect-ratio/index.ts +1 -0
  49. package/src/components/attribution-panel/attribution-panel.stories.tsx +83 -0
  50. package/src/components/attribution-panel/attribution-panel.test.tsx +123 -0
  51. package/src/components/attribution-panel/attribution-panel.tsx +191 -0
  52. package/src/components/attribution-panel/attribution-types.ts +44 -0
  53. package/src/components/attribution-panel/attributions.generated.ts +1422 -0
  54. package/src/components/attribution-panel/index.ts +3 -0
  55. package/src/components/avatar/avatar.stories.tsx +45 -0
  56. package/src/components/avatar/avatar.test.tsx +13 -0
  57. package/src/components/avatar/avatar.tsx +46 -0
  58. package/src/components/avatar/index.ts +1 -0
  59. package/src/components/badge/badge.stories.tsx +44 -0
  60. package/src/components/badge/badge.test.tsx +10 -0
  61. package/src/components/badge/badge.tsx +28 -0
  62. package/src/components/badge/index.ts +1 -0
  63. package/src/components/bento-grid/bento-grid.stories.tsx +165 -0
  64. package/src/components/bento-grid/bento-grid.test.tsx +119 -0
  65. package/src/components/bento-grid/bento-grid.tsx +339 -0
  66. package/src/components/bento-grid/index.ts +9 -0
  67. package/src/components/bounded-number/bounded-number.stories.tsx +118 -0
  68. package/src/components/bounded-number/bounded-number.test.tsx +73 -0
  69. package/src/components/bounded-number/bounded-number.tsx +110 -0
  70. package/src/components/bounded-number/index.ts +1 -0
  71. package/src/components/breadcrumb/breadcrumb.stories.tsx +47 -0
  72. package/src/components/breadcrumb/breadcrumb.test.tsx +17 -0
  73. package/src/components/breadcrumb/breadcrumb.tsx +86 -0
  74. package/src/components/breadcrumb/index.ts +9 -0
  75. package/src/components/button/button.stories.tsx +70 -0
  76. package/src/components/button/button.test.tsx +50 -0
  77. package/src/components/button/button.tsx +63 -0
  78. package/src/components/button/index.ts +1 -0
  79. package/src/components/button-group/button-group.stories.tsx +45 -0
  80. package/src/components/button-group/button-group.tsx +70 -0
  81. package/src/components/button-group/index.ts +6 -0
  82. package/src/components/calendar/calendar.stories.tsx +95 -0
  83. package/src/components/calendar/calendar.test.tsx +140 -0
  84. package/src/components/calendar/calendar.tsx +70 -0
  85. package/src/components/calendar/index.ts +1 -0
  86. package/src/components/card/card.stories.tsx +438 -0
  87. package/src/components/card/card.test.tsx +153 -0
  88. package/src/components/card/card.tsx +332 -0
  89. package/src/components/card/index.ts +13 -0
  90. package/src/components/carousel/carousel.stories.tsx +102 -0
  91. package/src/components/carousel/carousel.tsx +225 -0
  92. package/src/components/carousel/index.ts +8 -0
  93. package/src/components/change-review/change-review.stories.tsx +288 -0
  94. package/src/components/change-review/change-review.test.tsx +261 -0
  95. package/src/components/change-review/change-review.tsx +637 -0
  96. package/src/components/change-review/index.ts +23 -0
  97. package/src/components/checkbox/checkbox.stories.tsx +119 -0
  98. package/src/components/checkbox/checkbox.test.tsx +40 -0
  99. package/src/components/checkbox/checkbox.tsx +34 -0
  100. package/src/components/checkbox/index.ts +1 -0
  101. package/src/components/collapsible/collapsible.stories.tsx +62 -0
  102. package/src/components/collapsible/collapsible.tsx +22 -0
  103. package/src/components/collapsible/index.ts +1 -0
  104. package/src/components/collapsible-panel/index.ts +8 -0
  105. package/src/components/collapsible-panel/use-collapsible-panel.test.tsx +93 -0
  106. package/src/components/collapsible-panel/use-collapsible-panel.ts +159 -0
  107. package/src/components/color-picker/color-picker.stories.tsx +93 -0
  108. package/src/components/color-picker/color-picker.test.tsx +79 -0
  109. package/src/components/color-picker/color-picker.tsx +341 -0
  110. package/src/components/color-picker/index.ts +1 -0
  111. package/src/components/combobox/combobox.stories.tsx +154 -0
  112. package/src/components/combobox/combobox.test.tsx +246 -0
  113. package/src/components/combobox/combobox.tsx +210 -0
  114. package/src/components/combobox/index.ts +1 -0
  115. package/src/components/command/command.stories.tsx +371 -0
  116. package/src/components/command/command.test.tsx +280 -0
  117. package/src/components/command/command.tsx +301 -0
  118. package/src/components/command/index.ts +13 -0
  119. package/src/components/confirm-dialog/confirm-dialog.stories.tsx +183 -0
  120. package/src/components/confirm-dialog/confirm-dialog.test.tsx +147 -0
  121. package/src/components/confirm-dialog/confirm-dialog.tsx +146 -0
  122. package/src/components/confirm-dialog/index.ts +2 -0
  123. package/src/components/context-menu/context-menu.stories.tsx +70 -0
  124. package/src/components/context-menu/context-menu.tsx +172 -0
  125. package/src/components/context-menu/index.ts +16 -0
  126. package/src/components/copyable-value/copyable-value.stories.tsx +80 -0
  127. package/src/components/copyable-value/copyable-value.test.tsx +128 -0
  128. package/src/components/copyable-value/copyable-value.tsx +101 -0
  129. package/src/components/copyable-value/index.ts +1 -0
  130. package/src/components/date-picker/date-picker.stories.tsx +42 -0
  131. package/src/components/date-picker/date-picker.tsx +49 -0
  132. package/src/components/date-picker/index.ts +1 -0
  133. package/src/components/date-range-picker/date-range-picker.stories.tsx +151 -0
  134. package/src/components/date-range-picker/date-range-picker.test.tsx +62 -0
  135. package/src/components/date-range-picker/date-range-picker.tsx +126 -0
  136. package/src/components/date-range-picker/index.ts +6 -0
  137. package/src/components/descriptions/descriptions.stories.tsx +43 -0
  138. package/src/components/descriptions/descriptions.test.tsx +41 -0
  139. package/src/components/descriptions/descriptions.tsx +95 -0
  140. package/src/components/descriptions/index.ts +6 -0
  141. package/src/components/dialog/dialog.stories.tsx +728 -0
  142. package/src/components/dialog/dialog.test.tsx +285 -0
  143. package/src/components/dialog/dialog.tsx +340 -0
  144. package/src/components/dialog/index.ts +18 -0
  145. package/src/components/dialog/use-dialog-dismiss-guard.test.tsx +80 -0
  146. package/src/components/dialog/use-dialog-dismiss-guard.ts +109 -0
  147. package/src/components/drawer/drawer.stories.tsx +76 -0
  148. package/src/components/drawer/drawer.tsx +85 -0
  149. package/src/components/drawer/index.ts +11 -0
  150. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +176 -0
  151. package/src/components/dropdown-menu/dropdown-menu.test.tsx +109 -0
  152. package/src/components/dropdown-menu/dropdown-menu.tsx +179 -0
  153. package/src/components/dropdown-menu/index.ts +17 -0
  154. package/src/components/empty-state/empty-state.stories.tsx +54 -0
  155. package/src/components/empty-state/empty-state.test.tsx +11 -0
  156. package/src/components/empty-state/empty-state.tsx +30 -0
  157. package/src/components/empty-state/index.ts +1 -0
  158. package/src/components/error-state/error-state.stories.tsx +51 -0
  159. package/src/components/error-state/error-state.tsx +27 -0
  160. package/src/components/error-state/index.ts +1 -0
  161. package/src/components/expand-dialog/expand-dialog.stories.tsx +246 -0
  162. package/src/components/expand-dialog/expand-dialog.test.tsx +185 -0
  163. package/src/components/expand-dialog/expand-dialog.tsx +387 -0
  164. package/src/components/expand-dialog/index.ts +11 -0
  165. package/src/components/field-row/field-row.stories.tsx +114 -0
  166. package/src/components/field-row/field-row.test.tsx +188 -0
  167. package/src/components/field-row/field-row.tsx +115 -0
  168. package/src/components/field-row/index.ts +1 -0
  169. package/src/components/file-upload/file-upload.stories.tsx +183 -0
  170. package/src/components/file-upload/file-upload.test.tsx +169 -0
  171. package/src/components/file-upload/file-upload.tsx +459 -0
  172. package/src/components/file-upload/index.ts +13 -0
  173. package/src/components/form/form.stories.tsx +71 -0
  174. package/src/components/form/form.tsx +141 -0
  175. package/src/components/form/index.ts +10 -0
  176. package/src/components/hover-card/hover-card.stories.tsx +57 -0
  177. package/src/components/hover-card/hover-card.tsx +25 -0
  178. package/src/components/hover-card/index.ts +1 -0
  179. package/src/components/icon-button/icon-button.stories.tsx +108 -0
  180. package/src/components/icon-button/icon-button.test.tsx +93 -0
  181. package/src/components/icon-button/icon-button.tsx +139 -0
  182. package/src/components/icon-button/index.ts +1 -0
  183. package/src/components/input/index.ts +1 -0
  184. package/src/components/input/input.stories.tsx +44 -0
  185. package/src/components/input/input.test.tsx +19 -0
  186. package/src/components/input/input.tsx +26 -0
  187. package/src/components/input-group/index.ts +11 -0
  188. package/src/components/input-group/input-group.stories.tsx +87 -0
  189. package/src/components/input-group/input-group.test.tsx +48 -0
  190. package/src/components/input-group/input-group.tsx +170 -0
  191. package/src/components/input-otp/index.ts +1 -0
  192. package/src/components/input-otp/input-otp.stories.tsx +66 -0
  193. package/src/components/input-otp/input-otp.tsx +68 -0
  194. package/src/components/kbd/index.ts +1 -0
  195. package/src/components/kbd/kbd.stories.tsx +45 -0
  196. package/src/components/kbd/kbd.tsx +20 -0
  197. package/src/components/key-value-editor/index.ts +1 -0
  198. package/src/components/key-value-editor/key-value-editor.stories.tsx +130 -0
  199. package/src/components/key-value-editor/key-value-editor.test.tsx +231 -0
  200. package/src/components/key-value-editor/key-value-editor.tsx +263 -0
  201. package/src/components/label/index.ts +1 -0
  202. package/src/components/label/label.stories.tsx +35 -0
  203. package/src/components/label/label.tsx +19 -0
  204. package/src/components/link-preview/index.ts +2 -0
  205. package/src/components/link-preview/link-preview.stories.tsx +178 -0
  206. package/src/components/link-preview/link-preview.test.tsx +123 -0
  207. package/src/components/link-preview/link-preview.tsx +194 -0
  208. package/src/components/list-editor/index.ts +1 -0
  209. package/src/components/list-editor/list-editor.stories.tsx +119 -0
  210. package/src/components/list-editor/list-editor.test.tsx +96 -0
  211. package/src/components/list-editor/list-editor.tsx +192 -0
  212. package/src/components/loading-state/index.ts +1 -0
  213. package/src/components/loading-state/loading-state.stories.tsx +38 -0
  214. package/src/components/loading-state/loading-state.tsx +26 -0
  215. package/src/components/locale-provider/index.ts +8 -0
  216. package/src/components/locale-provider/locale-provider.stories.tsx +83 -0
  217. package/src/components/locale-provider/locale-provider.test.tsx +238 -0
  218. package/src/components/locale-provider/locale-provider.tsx +187 -0
  219. package/src/components/locale-provider/messages.ts +300 -0
  220. package/src/components/match-highlight/index.ts +7 -0
  221. package/src/components/match-highlight/match-highlight.stories.tsx +146 -0
  222. package/src/components/match-highlight/match-highlight.test.tsx +202 -0
  223. package/src/components/match-highlight/match-highlight.tsx +145 -0
  224. package/src/components/mention-input/index.ts +28 -0
  225. package/src/components/mention-input/mention-input-mirror.stories.tsx +171 -0
  226. package/src/components/mention-input/mention-input.stories.tsx +463 -0
  227. package/src/components/mention-input/mention-input.test.tsx +617 -0
  228. package/src/components/mention-input/mention-input.tsx +1102 -0
  229. package/src/components/mention-input/mention-value.test.ts +253 -0
  230. package/src/components/mention-input/mention-value.ts +193 -0
  231. package/src/components/menubar/index.ts +17 -0
  232. package/src/components/menubar/menubar.stories.tsx +67 -0
  233. package/src/components/menubar/menubar.tsx +198 -0
  234. package/src/components/metric-card/index.ts +1 -0
  235. package/src/components/metric-card/metric-card.stories.tsx +224 -0
  236. package/src/components/metric-card/metric-card.test.tsx +198 -0
  237. package/src/components/metric-card/metric-card.tsx +229 -0
  238. package/src/components/model-picker/index.ts +11 -0
  239. package/src/components/model-picker/model-picker-state.ts +85 -0
  240. package/src/components/model-picker/model-picker.stories.tsx +128 -0
  241. package/src/components/model-picker/model-picker.test.tsx +241 -0
  242. package/src/components/model-picker/model-picker.tsx +270 -0
  243. package/src/components/nav-main/index.ts +1 -0
  244. package/src/components/nav-main/nav-main.tsx +147 -0
  245. package/src/components/nav-notifications/index.ts +5 -0
  246. package/src/components/nav-notifications/nav-notifications.test.tsx +15 -0
  247. package/src/components/nav-notifications/nav-notifications.tsx +70 -0
  248. package/src/components/nav-user/index.ts +1 -0
  249. package/src/components/nav-user/nav-user.test.tsx +29 -0
  250. package/src/components/nav-user/nav-user.tsx +114 -0
  251. package/src/components/navigation-menu/index.ts +10 -0
  252. package/src/components/navigation-menu/navigation-menu.stories.tsx +92 -0
  253. package/src/components/navigation-menu/navigation-menu.tsx +99 -0
  254. package/src/components/number-input/index.ts +1 -0
  255. package/src/components/number-input/number-input.stories.tsx +120 -0
  256. package/src/components/number-input/number-input.test.tsx +96 -0
  257. package/src/components/number-input/number-input.tsx +219 -0
  258. package/src/components/page-shell/index.ts +1 -0
  259. package/src/components/page-shell/page-shell.stories.tsx +105 -0
  260. package/src/components/page-shell/page-shell.test.tsx +57 -0
  261. package/src/components/page-shell/page-shell.tsx +62 -0
  262. package/src/components/pagination/index.ts +9 -0
  263. package/src/components/pagination/pagination.stories.tsx +57 -0
  264. package/src/components/pagination/pagination.tsx +77 -0
  265. package/src/components/popover/index.ts +1 -0
  266. package/src/components/popover/popover.stories.tsx +28 -0
  267. package/src/components/popover/popover.test.tsx +16 -0
  268. package/src/components/popover/popover.tsx +29 -0
  269. package/src/components/progress/index.ts +1 -0
  270. package/src/components/progress/progress.stories.tsx +80 -0
  271. package/src/components/progress/progress.test.tsx +35 -0
  272. package/src/components/progress/progress.tsx +50 -0
  273. package/src/components/radio-group/index.ts +1 -0
  274. package/src/components/radio-group/radio-group.stories.tsx +75 -0
  275. package/src/components/radio-group/radio-group.test.tsx +16 -0
  276. package/src/components/radio-group/radio-group.tsx +33 -0
  277. package/src/components/rating/index.ts +1 -0
  278. package/src/components/rating/rating.stories.tsx +111 -0
  279. package/src/components/rating/rating.test.tsx +41 -0
  280. package/src/components/rating/rating.tsx +226 -0
  281. package/src/components/resizable/index.ts +1 -0
  282. package/src/components/resizable/resizable.stories.tsx +25 -0
  283. package/src/components/resizable/resizable.tsx +40 -0
  284. package/src/components/reveal/index.ts +7 -0
  285. package/src/components/reveal/reveal.stories.tsx +90 -0
  286. package/src/components/reveal/reveal.test.tsx +43 -0
  287. package/src/components/reveal/reveal.tsx +95 -0
  288. package/src/components/revision-timeline/index.ts +10 -0
  289. package/src/components/revision-timeline/revision-timeline.stories.tsx +785 -0
  290. package/src/components/revision-timeline/revision-timeline.test.tsx +509 -0
  291. package/src/components/revision-timeline/revision-timeline.tsx +1107 -0
  292. package/src/components/scroll-area/index.ts +1 -0
  293. package/src/components/scroll-area/scroll-area.stories.tsx +45 -0
  294. package/src/components/scroll-area/scroll-area.tsx +43 -0
  295. package/src/components/section-header/index.ts +1 -0
  296. package/src/components/section-header/section-header.stories.tsx +20 -0
  297. package/src/components/section-header/section-header.tsx +36 -0
  298. package/src/components/segmented-field/index.ts +5 -0
  299. package/src/components/segmented-field/segmented-field.stories.tsx +121 -0
  300. package/src/components/segmented-field/segmented-field.test.tsx +163 -0
  301. package/src/components/segmented-field/segmented-field.tsx +235 -0
  302. package/src/components/select/index.ts +10 -0
  303. package/src/components/select/select.stories.tsx +106 -0
  304. package/src/components/select/select.test.tsx +98 -0
  305. package/src/components/select/select.tsx +230 -0
  306. package/src/components/separator/index.ts +2 -0
  307. package/src/components/separator/separator.stories.tsx +87 -0
  308. package/src/components/separator/separator.tsx +51 -0
  309. package/src/components/sheet/index.ts +12 -0
  310. package/src/components/sheet/sheet.stories.tsx +65 -0
  311. package/src/components/sheet/sheet.tsx +109 -0
  312. package/src/components/sidebar/index.ts +27 -0
  313. package/src/components/sidebar/sidebar.stories.tsx +69 -0
  314. package/src/components/sidebar/sidebar.test.tsx +43 -0
  315. package/src/components/sidebar/sidebar.tsx +688 -0
  316. package/src/components/skeleton/index.ts +1 -0
  317. package/src/components/skeleton/skeleton.stories.tsx +28 -0
  318. package/src/components/skeleton/skeleton.tsx +15 -0
  319. package/src/components/slider/index.ts +1 -0
  320. package/src/components/slider/slider.stories.tsx +211 -0
  321. package/src/components/slider/slider.test.tsx +168 -0
  322. package/src/components/slider/slider.tsx +136 -0
  323. package/src/components/slider-number/index.ts +1 -0
  324. package/src/components/slider-number/slider-number.stories.tsx +123 -0
  325. package/src/components/slider-number/slider-number.test.tsx +165 -0
  326. package/src/components/slider-number/slider-number.tsx +191 -0
  327. package/src/components/sonner/index.ts +1 -0
  328. package/src/components/sonner/sonner.stories.tsx +124 -0
  329. package/src/components/sonner/sonner.test.tsx +76 -0
  330. package/src/components/sonner/sonner.tsx +101 -0
  331. package/src/components/spinner/index.ts +1 -0
  332. package/src/components/spinner/spinner.stories.tsx +27 -0
  333. package/src/components/spinner/spinner.tsx +19 -0
  334. package/src/components/split-panel/index.ts +1 -0
  335. package/src/components/split-panel/split-panel.stories.tsx +60 -0
  336. package/src/components/split-panel/split-panel.test.tsx +38 -0
  337. package/src/components/split-panel/split-panel.tsx +95 -0
  338. package/src/components/state-panel/index.ts +1 -0
  339. package/src/components/state-panel/state-panel.stories.tsx +80 -0
  340. package/src/components/state-panel/state-panel.test.tsx +64 -0
  341. package/src/components/state-panel/state-panel.tsx +167 -0
  342. package/src/components/status-badge/index.ts +18 -0
  343. package/src/components/status-badge/status-badge.stories.tsx +132 -0
  344. package/src/components/status-badge/status-badge.test.tsx +189 -0
  345. package/src/components/status-badge/status-badge.tsx +330 -0
  346. package/src/components/switch/index.ts +1 -0
  347. package/src/components/switch/switch.stories.tsx +58 -0
  348. package/src/components/switch/switch.test.tsx +12 -0
  349. package/src/components/switch/switch.tsx +29 -0
  350. package/src/components/table/index.ts +10 -0
  351. package/src/components/table/table.stories.tsx +53 -0
  352. package/src/components/table/table.test.tsx +17 -0
  353. package/src/components/table/table.tsx +106 -0
  354. package/src/components/tabs/index.ts +1 -0
  355. package/src/components/tabs/tabs.stories.tsx +267 -0
  356. package/src/components/tabs/tabs.test.tsx +94 -0
  357. package/src/components/tabs/tabs.tsx +170 -0
  358. package/src/components/tag-input/index.ts +1 -0
  359. package/src/components/tag-input/tag-input.stories.tsx +120 -0
  360. package/src/components/tag-input/tag-input.test.tsx +97 -0
  361. package/src/components/tag-input/tag-input.tsx +253 -0
  362. package/src/components/team-switcher/index.ts +1 -0
  363. package/src/components/team-switcher/team-switcher.test.tsx +39 -0
  364. package/src/components/team-switcher/team-switcher.tsx +100 -0
  365. package/src/components/textarea/index.ts +1 -0
  366. package/src/components/textarea/textarea.stories.tsx +42 -0
  367. package/src/components/textarea/textarea.tsx +24 -0
  368. package/src/components/theme-switcher/index.ts +2 -0
  369. package/src/components/theme-switcher/theme-switcher.stories.tsx +146 -0
  370. package/src/components/theme-switcher/theme-switcher.test.tsx +253 -0
  371. package/src/components/theme-switcher/theme-switcher.tsx +280 -0
  372. package/src/components/theme-switcher/use-theme-transition.ts +62 -0
  373. package/src/components/timeline/index.ts +13 -0
  374. package/src/components/timeline/timeline.stories.tsx +99 -0
  375. package/src/components/timeline/timeline.test.tsx +196 -0
  376. package/src/components/timeline/timeline.tsx +188 -0
  377. package/src/components/toggle/index.ts +1 -0
  378. package/src/components/toggle/toggle.stories.tsx +116 -0
  379. package/src/components/toggle/toggle.test.tsx +119 -0
  380. package/src/components/toggle/toggle.tsx +64 -0
  381. package/src/components/toggle-group/index.ts +1 -0
  382. package/src/components/toggle-group/toggle-group.stories.tsx +110 -0
  383. package/src/components/toggle-group/toggle-group.tsx +59 -0
  384. package/src/components/toolbar/index.ts +14 -0
  385. package/src/components/toolbar/toolbar.stories.tsx +126 -0
  386. package/src/components/toolbar/toolbar.test.tsx +73 -0
  387. package/src/components/toolbar/toolbar.tsx +177 -0
  388. package/src/components/tooltip/index.ts +1 -0
  389. package/src/components/tooltip/tooltip.stories.tsx +67 -0
  390. package/src/components/tooltip/tooltip.tsx +27 -0
  391. package/src/components/top-nav/index.ts +1 -0
  392. package/src/components/top-nav/top-nav.stories.tsx +18 -0
  393. package/src/components/top-nav/top-nav.tsx +34 -0
  394. package/src/components/transfer/index.ts +2 -0
  395. package/src/components/transfer/transfer.stories.tsx +162 -0
  396. package/src/components/transfer/transfer.test.tsx +177 -0
  397. package/src/components/transfer/transfer.tsx +481 -0
  398. package/src/components/tree/index.ts +6 -0
  399. package/src/components/tree/tree.stories.tsx +618 -0
  400. package/src/components/tree/tree.test.tsx +699 -0
  401. package/src/components/tree/tree.tsx +1149 -0
  402. package/src/components/tree/use-tree-keyboard.ts +314 -0
  403. package/src/components/tree-select/index.ts +1 -0
  404. package/src/components/tree-select/tree-select.stories.tsx +127 -0
  405. package/src/components/tree-select/tree-select.test.tsx +40 -0
  406. package/src/components/tree-select/tree-select.tsx +137 -0
  407. package/src/components/typography/index.ts +31 -0
  408. package/src/components/typography/prose.test.tsx +48 -0
  409. package/src/components/typography/prose.tsx +194 -0
  410. package/src/components/typography/typography.stories.tsx +244 -0
  411. package/src/components/typography/typography.test.tsx +140 -0
  412. package/src/components/typography/typography.tsx +126 -0
  413. package/src/components/view-toolbar/index.ts +10 -0
  414. package/src/components/view-toolbar/view-toolbar.stories.tsx +342 -0
  415. package/src/components/view-toolbar/view-toolbar.test.tsx +262 -0
  416. package/src/components/view-toolbar/view-toolbar.tsx +320 -0
  417. package/src/components/virtual-select/index.ts +2 -0
  418. package/src/components/virtual-select/use-virtual-listbox.ts +132 -0
  419. package/src/components/virtual-select/virtual-select.stories.tsx +203 -0
  420. package/src/components/virtual-select/virtual-select.test.tsx +106 -0
  421. package/src/components/virtual-select/virtual-select.tsx +361 -0
  422. package/src/components/wizard/index.ts +12 -0
  423. package/src/components/wizard/wizard.stories.tsx +148 -0
  424. package/src/components/wizard/wizard.test.tsx +67 -0
  425. package/src/components/wizard/wizard.tsx +354 -0
  426. package/src/decoration.stories.tsx +236 -0
  427. package/src/index.ts +174 -0
  428. package/src/lib/cn.test.ts +39 -0
  429. package/src/lib/cn.ts +31 -0
  430. package/src/lib/compact-number.ts +61 -0
  431. package/src/lib/document-address.test.ts +75 -0
  432. package/src/lib/document-address.ts +265 -0
  433. package/src/lib/download.ts +93 -0
  434. package/src/lib/file-icon.test.ts +45 -0
  435. package/src/lib/file-icon.ts +58 -0
  436. package/src/lib/file-kind.test.ts +98 -0
  437. package/src/lib/file-kind.ts +303 -0
  438. package/src/lib/file-source.test.ts +217 -0
  439. package/src/lib/file-source.ts +269 -0
  440. package/src/lib/merge-refs.test.ts +37 -0
  441. package/src/lib/merge-refs.ts +17 -0
  442. package/src/lib/streamdown-translations.ts +99 -0
  443. package/src/lib/use-copy-to-clipboard.ts +75 -0
  444. package/src/lib/use-mobile.ts +16 -0
  445. package/src/motion.stories.tsx +266 -0
  446. package/src/taste-profile.test.tsx +252 -0
  447. package/src/templates-object-detail-hub.stories.tsx +466 -0
  448. package/src/templates-screen-states.stories.tsx +306 -0
  449. package/src/templates-settings.stories.tsx +245 -0
@@ -0,0 +1,371 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
3
+ import { expect, waitFor, within } from "storybook/test";
4
+ import { Calculator, Calendar, CreditCard, Settings, Smile, User } from "lucide-react";
5
+ import { Button } from "../button";
6
+ import { DialogTitle } from "../dialog";
7
+ import {
8
+ Command,
9
+ CommandDialog,
10
+ CommandEmpty,
11
+ CommandGroup,
12
+ CommandInput,
13
+ CommandItem,
14
+ CommandList,
15
+ CommandSeparator,
16
+ CommandShortcut,
17
+ } from "./command";
18
+ const meta = {
19
+ title: "Overlays/Command",
20
+ component: Command,
21
+ tags: ["autodocs"],
22
+ argTypes: {
23
+ label: {
24
+ description: "Accessible label for the command palette (cmdk `label` prop).",
25
+ control: "text",
26
+ table: { category: "Accessibility" },
27
+ },
28
+ filter: {
29
+ description: "Custom ranking function for filtering items (overrides fuzzy default).",
30
+ control: false,
31
+ table: { category: "Behaviour" },
32
+ },
33
+ shouldFilter: {
34
+ description: "Whether cmdk filters items by the current search value. Default true.",
35
+ control: "boolean",
36
+ table: { category: "Behaviour" },
37
+ },
38
+ loop: {
39
+ description: "Whether keyboard navigation wraps from last item back to first.",
40
+ control: "boolean",
41
+ table: { category: "Behaviour" },
42
+ },
43
+ className: {
44
+ description: "Additional CSS classes applied to the root element.",
45
+ control: "text",
46
+ table: { category: "Styling" },
47
+ },
48
+ },
49
+ } satisfies Meta<typeof Command>;
50
+ export default meta;
51
+ type Story = StoryObj<typeof meta>;
52
+ export const Default: Story = {
53
+ render: () => (
54
+ <Command className="w-72 rounded-lg shadow-ring-md">
55
+ <CommandInput placeholder="Type a command…" />
56
+ <CommandList>
57
+ <CommandEmpty>No results found.</CommandEmpty>
58
+ <CommandGroup heading="Suggestions">
59
+ <CommandItem>New project</CommandItem>
60
+ <CommandItem>Open settings</CommandItem>
61
+ <CommandItem>Invite teammate</CommandItem>
62
+ </CommandGroup>
63
+ </CommandList>
64
+ </Command>
65
+ ),
66
+ // Types a search query and confirms filtering works; then clears to see all items.
67
+ play: async ({ canvasElement, userEvent }) => {
68
+ const canvas = within(canvasElement);
69
+ const input = canvas.getByPlaceholderText("Type a command…");
70
+ await waitFor(() => expect(input).toBeVisible());
71
+ // Filter to "settings"
72
+ await userEvent.type(input, "settings");
73
+ await waitFor(() => expect(canvas.getByText("Open settings")).toBeVisible());
74
+ // "New project" should be filtered out
75
+ await expect(canvas.queryByText("New project")).toBeNull();
76
+ // Clear filter — all items return
77
+ await userEvent.clear(input);
78
+ await waitFor(() => expect(canvas.getByText("New project")).toBeVisible());
79
+ },
80
+ };
81
+
82
+ /* -------------------------------------------------------------------------- */
83
+ /* Command Dialog (⌘K palette) */
84
+ /* -------------------------------------------------------------------------- */
85
+
86
+ /** The ⌘K command palette: a `CommandDialog` toggled by a shortcut or a button. */
87
+ function CommandPaletteDemo() {
88
+ const [open, setOpen] = useState(false);
89
+
90
+ useEffect(() => {
91
+ const onKeyDown = (e: KeyboardEvent) => {
92
+ if (e.key.toLowerCase() === "k" && (e.metaKey || e.ctrlKey)) {
93
+ e.preventDefault();
94
+ setOpen((prev) => !prev);
95
+ }
96
+ };
97
+ document.addEventListener("keydown", onKeyDown);
98
+ return () => document.removeEventListener("keydown", onKeyDown);
99
+ }, []);
100
+
101
+ const run = () => setOpen(false);
102
+
103
+ return (
104
+ <div className="flex flex-col items-center gap-3 p-6">
105
+ <p className="text-body text-muted-foreground">
106
+ Press{" "}
107
+ <kbd className="rounded border bg-muted px-1.5 py-0.5 font-mono text-meta" translate="no">
108
+ ⌘ K
109
+ </kbd>{" "}
110
+ or use the button.
111
+ </p>
112
+ <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
113
+ Open command palette
114
+ </Button>
115
+
116
+ <CommandDialog open={open} onOpenChange={setOpen}>
117
+ {/* CommandDialog doesn't ship a built-in title, so supply an sr-only one
118
+ for the dialog's accessible name. */}
119
+ <DialogTitle className="sr-only">Command palette</DialogTitle>
120
+ <CommandInput placeholder="Type a command or search…" />
121
+ <CommandList>
122
+ <CommandEmpty>No results found.</CommandEmpty>
123
+ <CommandGroup heading="Suggestions">
124
+ <CommandItem onSelect={run}>
125
+ <Calendar />
126
+ Calendar
127
+ </CommandItem>
128
+ <CommandItem onSelect={run}>
129
+ <Smile />
130
+ Search emoji
131
+ </CommandItem>
132
+ <CommandItem onSelect={run}>
133
+ <Calculator />
134
+ Calculator
135
+ </CommandItem>
136
+ </CommandGroup>
137
+ <CommandSeparator />
138
+ <CommandGroup heading="Settings">
139
+ <CommandItem onSelect={run}>
140
+ <User />
141
+ Profile
142
+ <CommandShortcut>⌘P</CommandShortcut>
143
+ </CommandItem>
144
+ <CommandItem onSelect={run}>
145
+ <CreditCard />
146
+ Billing
147
+ <CommandShortcut>⌘B</CommandShortcut>
148
+ </CommandItem>
149
+ <CommandItem onSelect={run}>
150
+ <Settings />
151
+ Settings
152
+ <CommandShortcut>⌘S</CommandShortcut>
153
+ </CommandItem>
154
+ </CommandGroup>
155
+ </CommandList>
156
+ </CommandDialog>
157
+ </div>
158
+ );
159
+ }
160
+
161
+ /** A ⌘K command palette in a dialog, opened by shortcut or button. */
162
+ export const Palette: Story = {
163
+ name: "Command Dialog (⌘K)",
164
+ render: () => <CommandPaletteDemo />,
165
+ };
166
+
167
+ /* -------------------------------------------------------------------------- */
168
+ /* Groups, icons, shortcuts & separators */
169
+ /* -------------------------------------------------------------------------- */
170
+
171
+ /** An inline menu with grouped items, leading icons, separators and shortcuts. */
172
+ export const Groups: Story = {
173
+ name: "Groups, icons & shortcuts",
174
+ render: () => (
175
+ <Command className="w-80 rounded-lg shadow-ring-md">
176
+ <CommandInput placeholder="Type a command or search…" />
177
+ <CommandList>
178
+ <CommandEmpty>No results found.</CommandEmpty>
179
+ <CommandGroup heading="Suggestions">
180
+ <CommandItem>
181
+ <Calendar />
182
+ Calendar
183
+ </CommandItem>
184
+ <CommandItem>
185
+ <Smile />
186
+ Search emoji
187
+ </CommandItem>
188
+ <CommandItem>
189
+ <Calculator />
190
+ Calculator
191
+ </CommandItem>
192
+ </CommandGroup>
193
+ <CommandSeparator />
194
+ <CommandGroup heading="Settings">
195
+ <CommandItem>
196
+ <User />
197
+ Profile
198
+ <CommandShortcut>⌘P</CommandShortcut>
199
+ </CommandItem>
200
+ <CommandItem>
201
+ <CreditCard />
202
+ Billing
203
+ <CommandShortcut>⌘B</CommandShortcut>
204
+ </CommandItem>
205
+ <CommandItem>
206
+ <Settings />
207
+ Settings
208
+ <CommandShortcut>⌘S</CommandShortcut>
209
+ </CommandItem>
210
+ </CommandGroup>
211
+ </CommandList>
212
+ </Command>
213
+ ),
214
+ };
215
+
216
+ /* -------------------------------------------------------------------------- */
217
+ /* Scrollable */
218
+ /* -------------------------------------------------------------------------- */
219
+
220
+ const FRAMEWORKS = [
221
+ "Next.js",
222
+ "React",
223
+ "Remix",
224
+ "Astro",
225
+ "SvelteKit",
226
+ "Nuxt",
227
+ "SolidStart",
228
+ "Qwik City",
229
+ "Gatsby",
230
+ "Vite",
231
+ "Angular",
232
+ "Vue",
233
+ "Ember",
234
+ "Preact",
235
+ "Lit",
236
+ "Alpine.js",
237
+ "HTMX",
238
+ "Eleventy",
239
+ "Hugo",
240
+ "Docusaurus",
241
+ ];
242
+
243
+ /** A long list inside the `CommandList`'s built-in scroll region (max-h 300px). */
244
+ export const Scrollable: Story = {
245
+ name: "Scrollable",
246
+ render: () => (
247
+ <Command className="w-80 rounded-lg shadow-ring-md">
248
+ <CommandInput placeholder="Search framework…" />
249
+ <CommandList>
250
+ <CommandEmpty>No results found.</CommandEmpty>
251
+ <CommandGroup heading="Frameworks">
252
+ {FRAMEWORKS.map((framework) => (
253
+ <CommandItem key={framework}>{framework}</CommandItem>
254
+ ))}
255
+ </CommandGroup>
256
+ </CommandList>
257
+ </Command>
258
+ ),
259
+ };
260
+
261
+ /* -------------------------------------------------------------------------- */
262
+ /* External combobox input (#365) */
263
+ /* -------------------------------------------------------------------------- */
264
+
265
+ /**
266
+ * A plain `<input role="combobox">` rendered OUTSIDE the `Command` tree (the
267
+ * composer-textarea / `@`-mention shape #365 was filed for) driving
268
+ * `aria-activedescendant` off `onActiveItemIdChange` — never by positionally
269
+ * querying `[role="option"]`. Arrow keys typed into the input are forwarded
270
+ * onto the `Command` root so cmdk's own navigation still owns selection.
271
+ */
272
+ function ExternalInputComboboxDemo() {
273
+ const [activeId, setActiveId] = useState<string | undefined>();
274
+ const listRef = useRef<HTMLDivElement>(null);
275
+
276
+ const forwardKey = (event: ReactKeyboardEvent<HTMLInputElement>) => {
277
+ if (["ArrowDown", "ArrowUp", "Enter", "Home", "End"].includes(event.key)) {
278
+ event.preventDefault();
279
+ listRef.current?.dispatchEvent(
280
+ new KeyboardEvent("keydown", { key: event.key, bubbles: true, cancelable: true }),
281
+ );
282
+ }
283
+ };
284
+
285
+ return (
286
+ <div className="w-80 space-y-2">
287
+ <input
288
+ aria-activedescendant={activeId}
289
+ aria-controls="mention-list"
290
+ aria-expanded="true"
291
+ role="combobox"
292
+ onKeyDown={forwardKey}
293
+ placeholder="Type @ to mention…"
294
+ className="flex h-10 w-full rounded-md border border-input bg-background px-3 text-body outline-none focus-visible:ring-2 focus-visible:ring-ring"
295
+ />
296
+ <Command
297
+ id="mention-list"
298
+ ref={listRef}
299
+ onActiveItemIdChange={setActiveId}
300
+ className="rounded-lg border shadow-ring-md"
301
+ >
302
+ <CommandList>
303
+ <CommandGroup heading="Teammates">
304
+ <CommandItem value="ada">Ada Lovelace</CommandItem>
305
+ <CommandItem value="grace">Grace Hopper</CommandItem>
306
+ <CommandItem value="alan">Alan Turing</CommandItem>
307
+ </CommandGroup>
308
+ </CommandList>
309
+ </Command>
310
+ </div>
311
+ );
312
+ }
313
+
314
+ export const ExternalInputCombobox: Story = {
315
+ name: "External input combobox (aria-activedescendant, #365)",
316
+ parameters: {
317
+ docs: {
318
+ description: {
319
+ story:
320
+ "`Command`'s `onActiveItemIdChange` reports the DOM id cmdk assigned to the " +
321
+ "highlighted item, so an input rendered outside the `Command` tree — a composer " +
322
+ "textarea driving an `@`-mention popup — can set `aria-activedescendant` " +
323
+ 'correctly without positionally querying `[role="option"]`.',
324
+ },
325
+ },
326
+ },
327
+ render: () => <ExternalInputComboboxDemo />,
328
+ play: async ({ canvasElement }) => {
329
+ const canvas = within(canvasElement);
330
+ const input = canvas.getByRole("combobox");
331
+
332
+ await waitFor(() => expect(input).toBeVisible());
333
+
334
+ // Before ANY interaction: cmdk highlights the first item on mount, so the
335
+ // external input must already point at it. (cmdk's own `state.selectedItemId`
336
+ // is `undefined` here — `Command` resolves the id from the committed DOM
337
+ // instead, which is what makes this assertion pass.)
338
+ await waitFor(() => {
339
+ const onMount = canvasElement.querySelector('[aria-selected="true"]') as HTMLElement;
340
+ expect(onMount).not.toBeNull();
341
+ expect(input.getAttribute("aria-activedescendant")).toBe(onMount.id);
342
+ });
343
+
344
+ // Forward one ArrowDown (the real keyboard-navigation path) to move
345
+ // cmdk's selection, then confirm the external input's
346
+ // aria-activedescendant follows the newly-highlighted item's DOM id —
347
+ // never a positional index.
348
+ input.focus();
349
+ input.dispatchEvent(
350
+ new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }),
351
+ );
352
+
353
+ let firstId: string | null = null;
354
+ await waitFor(() => {
355
+ const selected = canvasElement.querySelector('[aria-selected="true"]') as HTMLElement;
356
+ expect(selected).not.toBeNull();
357
+ expect(input.getAttribute("aria-activedescendant")).toBe(selected.id);
358
+ firstId = selected.id;
359
+ });
360
+
361
+ // A second ArrowDown must move it again, to a DIFFERENT id.
362
+ input.dispatchEvent(
363
+ new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }),
364
+ );
365
+ await waitFor(() => {
366
+ const nowSelected = canvasElement.querySelector('[aria-selected="true"]') as HTMLElement;
367
+ expect(nowSelected.id).not.toBe(firstId);
368
+ expect(input.getAttribute("aria-activedescendant")).toBe(nowSelected.id);
369
+ });
370
+ },
371
+ };
@@ -0,0 +1,280 @@
1
+ import { useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { render, screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import {
6
+ Command,
7
+ CommandGroup,
8
+ CommandInput,
9
+ CommandItem,
10
+ CommandList,
11
+ useCommandActiveItemId,
12
+ } from "./command";
13
+
14
+ // jsdom does not implement `Element.prototype.scrollIntoView`, and `cmdk`
15
+ // calls it unconditionally (not feature-detected — it's a third-party
16
+ // dependency, not brand-ui code) whenever the highlighted item changes.
17
+ // `packages/ui/vitest.setup.ts` deliberately does NOT stub this globally (it
18
+ // would mask a real brand-ui component skipping its own feature-detection),
19
+ // so — mirroring `tree.test.tsx`'s local stub for the same jsdom gap — every
20
+ // test in this file gets a local no-op stub instead.
21
+ let originalScrollIntoView: typeof Element.prototype.scrollIntoView;
22
+
23
+ beforeEach(() => {
24
+ originalScrollIntoView = Element.prototype.scrollIntoView;
25
+ Element.prototype.scrollIntoView = vi.fn();
26
+ });
27
+
28
+ afterEach(() => {
29
+ Element.prototype.scrollIntoView = originalScrollIntoView;
30
+ });
31
+
32
+ /** #365 — cmdk assigns each item's `id`/`role`/`aria-selected` internally, so
33
+ * `Command`'s `onActiveItemIdChange` (and the `useCommandActiveItemId` hook)
34
+ * are the supported way to read the highlighted item's DOM id. */
35
+ describe("Command — onActiveItemIdChange (#365)", () => {
36
+ function Harness({ onActiveItemIdChange }: { onActiveItemIdChange: (id?: string) => void }) {
37
+ return (
38
+ <Command onActiveItemIdChange={onActiveItemIdChange}>
39
+ <CommandInput placeholder="Search…" />
40
+ <CommandList>
41
+ <CommandGroup heading="Fruit">
42
+ <CommandItem value="apple">Apple</CommandItem>
43
+ <CommandItem value="banana">Banana</CommandItem>
44
+ <CommandItem value="cherry">Cherry</CommandItem>
45
+ </CommandGroup>
46
+ </CommandList>
47
+ </Command>
48
+ );
49
+ }
50
+
51
+ it("reports the DOM id of the currently-selected item — never by positional index", async () => {
52
+ const onActiveItemIdChange = vi.fn();
53
+ const { container } = render(<Harness onActiveItemIdChange={onActiveItemIdChange} />);
54
+
55
+ const input = screen.getByPlaceholderText("Search…");
56
+ await userEvent.type(input, "{ArrowDown}{ArrowDown}");
57
+
58
+ const selected = container.querySelector('[aria-selected="true"]') as HTMLElement;
59
+ expect(selected).not.toBeNull();
60
+
61
+ // The LAST reported id must match the committed DOM id of the selected
62
+ // item — read from cmdk's own `aria-selected` attribute, never a
63
+ // positional/index match against the item list.
64
+ const lastCall = onActiveItemIdChange.mock.calls.at(-1);
65
+ expect(lastCall?.[0]).toBe(selected.id);
66
+ });
67
+
68
+ it("reports undefined once filtering leaves no matches, and a defined id again once restored", async () => {
69
+ const onActiveItemIdChange = vi.fn();
70
+ const { container } = render(<Harness onActiveItemIdChange={onActiveItemIdChange} />);
71
+
72
+ const input = screen.getByPlaceholderText("Search…");
73
+ await userEvent.type(input, "zzz-no-match");
74
+ await waitFor(() => expect(onActiveItemIdChange.mock.calls.at(-1)?.[0]).toBeUndefined());
75
+
76
+ // Clearing the filter re-highlights an item, and the id must come back on
77
+ // its own — with NO keyboard navigation to nudge it (cmdk's own
78
+ // `state.selectedItemId` stays `undefined` here; see the next two tests).
79
+ await userEvent.clear(input);
80
+ await waitFor(() => {
81
+ const selected = container.querySelector('[aria-selected="true"]') as HTMLElement;
82
+ expect(selected).not.toBeNull();
83
+ expect(onActiveItemIdChange.mock.calls.at(-1)?.[0]).toBe(selected.id);
84
+ });
85
+ });
86
+
87
+ // The two tests below lock the gap in cmdk 1.1.1 that `Command` works around.
88
+ // cmdk only recomputes `state.selectedItemId` in a callback queued from
89
+ // `setState("value", …)`, and its own `selectFirstItem()` runs inside the very
90
+ // layout-effect flush that drains that queue — so the callback reads the DOM
91
+ // before React has committed the new `aria-selected`, and the field stays
92
+ // `undefined`. Reproduced with zero brand-ui code: cmdk's own `Command.Input`
93
+ // renders NO `aria-activedescendant` in either state. Without the workaround a
94
+ // consumer wiring an `@`-mention popup gets `undefined` for exactly the two
95
+ // states the popup spends its life in, and is pushed back to the positional
96
+ // DOM query this issue exists to remove.
97
+ it("reports the item cmdk auto-highlights on MOUNT, before any interaction", async () => {
98
+ const onActiveItemIdChange = vi.fn();
99
+ const { container } = render(<Harness onActiveItemIdChange={onActiveItemIdChange} />);
100
+
101
+ await waitFor(() => {
102
+ const selected = container.querySelector('[aria-selected="true"]') as HTMLElement;
103
+ expect(selected).not.toBeNull();
104
+ expect(onActiveItemIdChange.mock.calls.at(-1)?.[0]).toBe(selected.id);
105
+ });
106
+ });
107
+
108
+ it("reports the item re-highlighted by FILTERING, with no keyboard navigation", async () => {
109
+ const onActiveItemIdChange = vi.fn();
110
+ const { container } = render(<Harness onActiveItemIdChange={onActiveItemIdChange} />);
111
+
112
+ // "an" matches Banana only — cmdk moves the highlight off Apple without an
113
+ // arrow key ever being pressed.
114
+ await userEvent.type(screen.getByPlaceholderText("Search…"), "an");
115
+
116
+ await waitFor(() => {
117
+ const selected = container.querySelector('[aria-selected="true"]') as HTMLElement;
118
+ expect(selected).not.toBeNull();
119
+ expect(selected.textContent).toBe("Banana");
120
+ expect(onActiveItemIdChange.mock.calls.at(-1)?.[0]).toBe(selected.id);
121
+ });
122
+ });
123
+
124
+ it("does not change cmdk's own selection/filtering behaviour (control)", async () => {
125
+ const onSelect = vi.fn();
126
+ render(
127
+ <Command>
128
+ <CommandInput placeholder="Search…" />
129
+ <CommandList>
130
+ <CommandGroup heading="Fruit">
131
+ <CommandItem value="apple" onSelect={onSelect}>
132
+ Apple
133
+ </CommandItem>
134
+ <CommandItem value="banana" onSelect={onSelect}>
135
+ Banana
136
+ </CommandItem>
137
+ </CommandGroup>
138
+ </CommandList>
139
+ </Command>,
140
+ );
141
+
142
+ const input = screen.getByPlaceholderText("Search…");
143
+ await userEvent.type(input, "banana");
144
+ expect(screen.queryByText("Apple")).not.toBeInTheDocument();
145
+ expect(screen.getByText("Banana")).toBeInTheDocument();
146
+
147
+ await userEvent.type(input, "{Enter}");
148
+ expect(onSelect).toHaveBeenCalledWith("banana");
149
+ });
150
+
151
+ function InnerReader({ onId }: { onId: (id?: string) => void }) {
152
+ const id = useCommandActiveItemId();
153
+ onId(id);
154
+ return null;
155
+ }
156
+
157
+ it("useCommandActiveItemId reports the same id as the onActiveItemIdChange callback", async () => {
158
+ const fromCallback = vi.fn();
159
+ const fromHook = vi.fn();
160
+
161
+ function HookHarness() {
162
+ return (
163
+ <Command onActiveItemIdChange={fromCallback}>
164
+ <CommandInput placeholder="Search…" />
165
+ <InnerReader onId={fromHook} />
166
+ <CommandList>
167
+ <CommandGroup heading="Fruit">
168
+ <CommandItem value="apple">Apple</CommandItem>
169
+ <CommandItem value="banana">Banana</CommandItem>
170
+ </CommandGroup>
171
+ </CommandList>
172
+ </Command>
173
+ );
174
+ }
175
+
176
+ render(<HookHarness />);
177
+ const input = screen.getByPlaceholderText("Search…");
178
+ await userEvent.type(input, "{ArrowDown}");
179
+
180
+ expect(fromHook.mock.calls.at(-1)?.[0]).toBe(fromCallback.mock.calls.at(-1)?.[0]);
181
+ });
182
+ });
183
+
184
+ describe("CommandItem — dev-only warning for cmdk-overridden props", () => {
185
+ it("warns when a consumer passes id, role, or aria-selected", () => {
186
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
187
+ try {
188
+ render(
189
+ <Command>
190
+ <CommandList>
191
+ <CommandItem id="my-custom-id">Apple</CommandItem>
192
+ </CommandList>
193
+ </Command>,
194
+ );
195
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('"id"'));
196
+ } finally {
197
+ warn.mockRestore();
198
+ }
199
+ });
200
+ });
201
+
202
+ /**
203
+ * A plain external input ABOVE the `Command` tree — the composer-textarea shape
204
+ * this issue was filed for. Arrow keys typed into the input are forwarded onto
205
+ * the `Command` root (the same technique `command.stories.tsx`'s
206
+ * `ExternalInputCombobox` story uses), and `aria-activedescendant` is driven off
207
+ * `onActiveItemIdChange` — never a positional DOM query.
208
+ */
209
+ describe("Command — external combobox input wiring (integration)", () => {
210
+ function ExternalCombobox() {
211
+ const [activeId, setActiveId] = useState<string | undefined>();
212
+ const listRef = useRef<HTMLDivElement>(null);
213
+
214
+ const forwardKey = (event: ReactKeyboardEvent<HTMLInputElement>) => {
215
+ if (["ArrowDown", "ArrowUp", "Enter", "Home", "End"].includes(event.key)) {
216
+ event.preventDefault();
217
+ listRef.current?.dispatchEvent(
218
+ new KeyboardEvent("keydown", { key: event.key, bubbles: true, cancelable: true }),
219
+ );
220
+ }
221
+ };
222
+
223
+ return (
224
+ <div>
225
+ <input
226
+ aria-activedescendant={activeId}
227
+ aria-controls="fruit-list"
228
+ aria-expanded="true"
229
+ role="combobox"
230
+ onKeyDown={forwardKey}
231
+ placeholder="Type to search fruit…"
232
+ />
233
+ <Command id="fruit-list" ref={listRef} onActiveItemIdChange={setActiveId}>
234
+ <CommandList>
235
+ <CommandGroup heading="Fruit">
236
+ <CommandItem value="apple">Apple</CommandItem>
237
+ <CommandItem value="banana">Banana</CommandItem>
238
+ <CommandItem value="cherry">Cherry</CommandItem>
239
+ </CommandGroup>
240
+ </CommandList>
241
+ </Command>
242
+ </div>
243
+ );
244
+ }
245
+
246
+ it("keeps the external input's aria-activedescendant in sync with the selected item's id", async () => {
247
+ const { container } = render(<ExternalCombobox />);
248
+ const input = screen.getByPlaceholderText("Type to search fruit…");
249
+
250
+ // FIRST, before any interaction at all: a screen-reader user who focuses
251
+ // the input while the popup is already open must be told which option is
252
+ // highlighted. cmdk highlights the first item on mount, so
253
+ // aria-activedescendant has to point at it right away.
254
+ await waitFor(() => {
255
+ const onMount = container.querySelector('[aria-selected="true"]') as HTMLElement;
256
+ expect(onMount).not.toBeNull();
257
+ expect(input).toHaveAttribute("aria-activedescendant", onMount.id);
258
+ });
259
+
260
+ // Forward one ArrowDown to move cmdk's selection (exercising the real
261
+ // keyboard-navigation path the ExternalInputCombobox story drives), then
262
+ // assert the external input's aria-activedescendant tracks the newly
263
+ // highlighted item's DOM id — never a positional index.
264
+ await userEvent.type(input, "{ArrowDown}");
265
+ await waitFor(() => {
266
+ const nowSelected = container.querySelector('[aria-selected="true"]') as HTMLElement;
267
+ expect(nowSelected).not.toBeNull();
268
+ expect(input).toHaveAttribute("aria-activedescendant", nowSelected.id);
269
+ });
270
+
271
+ // A second ArrowDown must move it again, to a DIFFERENT id.
272
+ const firstId = input.getAttribute("aria-activedescendant");
273
+ await userEvent.type(input, "{ArrowDown}");
274
+ await waitFor(() => {
275
+ const nowSelected = container.querySelector('[aria-selected="true"]') as HTMLElement;
276
+ expect(nowSelected.id).not.toBe(firstId);
277
+ expect(input).toHaveAttribute("aria-activedescendant", nowSelected.id);
278
+ });
279
+ });
280
+ });