@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,699 @@
1
+ /**
2
+ * Tree component unit tests.
3
+ *
4
+ * Environment notes (jsdom):
5
+ * - jsdom has zero layout: offsetHeight / getBoundingClientRect always return 0.
6
+ * `useVirtualizer` therefore computes zero total-size and renders 0 virtual rows
7
+ * unless we stub layout APIs. We apply a minimal stub (offsetHeight + getBCR) on
8
+ * the scroll container so the virtualizer sees a non-zero viewport and renders
9
+ * at least the overscan window.
10
+ * - The "only a small window is mounted" assertion is best-effort under jsdom: we
11
+ * verify that fewer than all 5 000 treeitems are in the DOM.
12
+ * - Interaction (ArrowDown scrolls virtualizer) and a11y (axe) are verified via
13
+ * the Storybook `run-story-tests` gate on the real browser surface.
14
+ */
15
+ import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
16
+ import { render, screen, waitFor, act } from "@testing-library/react";
17
+ import userEvent from "@testing-library/user-event";
18
+ import { Tree, type TreeNode } from "./tree";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // jsdom layout stubs for @tanstack/react-virtual
22
+ // ---------------------------------------------------------------------------
23
+
24
+ const VIEWPORT_HEIGHT = 400;
25
+ const ROW_HEIGHT = 32;
26
+
27
+ /**
28
+ * Stub `offsetHeight` and `getBoundingClientRect` on any element that has the
29
+ * data-virtual-scroll attribute, so the virtualizer computes a non-zero
30
+ * viewport and renders rows. We patch at the Element prototype level so every
31
+ * scroll container benefits.
32
+ */
33
+ let originalGetBCR: () => DOMRect;
34
+
35
+ beforeAll(() => {
36
+ originalGetBCR = Element.prototype.getBoundingClientRect;
37
+
38
+ // Give every element a height. The virtualizer uses this for its scroll container.
39
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
40
+ configurable: true,
41
+ get() {
42
+ return VIEWPORT_HEIGHT;
43
+ },
44
+ });
45
+
46
+ Element.prototype.getBoundingClientRect = function () {
47
+ return {
48
+ width: 300,
49
+ height: VIEWPORT_HEIGHT,
50
+ top: 0,
51
+ left: 0,
52
+ bottom: VIEWPORT_HEIGHT,
53
+ right: 300,
54
+ x: 0,
55
+ y: 0,
56
+ toJSON() {
57
+ return this;
58
+ },
59
+ };
60
+ };
61
+ });
62
+
63
+ afterAll(() => {
64
+ Element.prototype.getBoundingClientRect = originalGetBCR;
65
+ // Remove the accessor we installed by redefining with a plain value descriptor.
66
+ // We cannot mix accessor (get/set) and data (value/writable) in one call —
67
+ // so delete the property first, then reinstall as a data property (jsdom default).
68
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
69
+ configurable: true,
70
+ get() {
71
+ return 0;
72
+ },
73
+ });
74
+ });
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Fixtures
78
+ // ---------------------------------------------------------------------------
79
+
80
+ const nodes: TreeNode[] = [
81
+ {
82
+ id: "fruits",
83
+ label: "Fruits",
84
+ children: [
85
+ { id: "apple", label: "Apple" },
86
+ { id: "banana", label: "Banana" },
87
+ ],
88
+ },
89
+ {
90
+ id: "veggies",
91
+ label: "Vegetables",
92
+ children: [{ id: "carrot", label: "Carrot" }],
93
+ },
94
+ ];
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // scrollToId / scrollSelectionIntoView — reveal a programmatically-selected node
98
+ // ---------------------------------------------------------------------------
99
+
100
+ describe("Tree reveal-into-view (scrollToId / scrollSelectionIntoView)", () => {
101
+ it("scrollToId expands a collapsed ancestor so the node is revealed (non-virtual)", async () => {
102
+ const scrollSpy = vi.fn();
103
+ const orig = Element.prototype.scrollIntoView;
104
+ Element.prototype.scrollIntoView = scrollSpy;
105
+ try {
106
+ const { rerender } = render(<Tree nodes={nodes} />);
107
+ // "Carrot" lives under the collapsed "Vegetables" — not in the DOM yet.
108
+ expect(screen.queryByText("Carrot")).toBeNull();
109
+ rerender(<Tree nodes={nodes} scrollToId="carrot" />);
110
+ await waitFor(() => expect(screen.getByText("Carrot")).toBeInTheDocument());
111
+ await waitFor(() => expect(scrollSpy).toHaveBeenCalled());
112
+ } finally {
113
+ Element.prototype.scrollIntoView = orig;
114
+ }
115
+ });
116
+
117
+ it("scrollToId scrolls an already-visible node into view (non-virtual)", async () => {
118
+ const scrollSpy = vi.fn();
119
+ const orig = Element.prototype.scrollIntoView;
120
+ Element.prototype.scrollIntoView = scrollSpy;
121
+ try {
122
+ const { rerender } = render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
123
+ rerender(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} scrollToId="apple" />);
124
+ await waitFor(() => expect(scrollSpy).toHaveBeenCalled());
125
+ } finally {
126
+ Element.prototype.scrollIntoView = orig;
127
+ }
128
+ });
129
+
130
+ it("scrollSelectionIntoView reveals a newly-selected collapsed node", async () => {
131
+ const scrollSpy = vi.fn();
132
+ const orig = Element.prototype.scrollIntoView;
133
+ Element.prototype.scrollIntoView = scrollSpy;
134
+ try {
135
+ const { rerender } = render(<Tree nodes={nodes} scrollSelectionIntoView selectedIds={[]} />);
136
+ expect(screen.queryByText("Carrot")).toBeNull();
137
+ rerender(<Tree nodes={nodes} scrollSelectionIntoView selectedIds={["carrot"]} />);
138
+ await waitFor(() => expect(screen.getByText("Carrot")).toBeInTheDocument());
139
+ } finally {
140
+ Element.prototype.scrollIntoView = orig;
141
+ }
142
+ });
143
+
144
+ it("scrollToId requests ancestor expansion in virtualize mode (controlled)", async () => {
145
+ const onExpandedChange = vi.fn();
146
+ const { rerender } = render(
147
+ <Tree
148
+ nodes={nodes}
149
+ virtualize
150
+ maxHeight="400px"
151
+ estimateRowHeight={ROW_HEIGHT}
152
+ expandedIds={[]}
153
+ onExpandedChange={onExpandedChange}
154
+ />,
155
+ );
156
+ rerender(
157
+ <Tree
158
+ nodes={nodes}
159
+ virtualize
160
+ maxHeight="400px"
161
+ estimateRowHeight={ROW_HEIGHT}
162
+ expandedIds={[]}
163
+ onExpandedChange={onExpandedChange}
164
+ scrollToId="carrot"
165
+ />,
166
+ );
167
+ // Reveal calls setExpandedIds → onExpandedChange (controlled) with the ancestor.
168
+ await waitFor(() => expect(onExpandedChange).toHaveBeenCalled());
169
+ const lastCall = onExpandedChange.mock.calls.at(-1)?.[0] as string[];
170
+ expect([...lastCall]).toContain("veggies");
171
+ });
172
+
173
+ it("scrollToId to an unknown id is a graceful no-op", () => {
174
+ const onExpandedChange = vi.fn();
175
+ const { rerender } = render(<Tree nodes={nodes} onExpandedChange={onExpandedChange} />);
176
+ expect(() =>
177
+ rerender(
178
+ <Tree nodes={nodes} onExpandedChange={onExpandedChange} scrollToId="does-not-exist" />,
179
+ ),
180
+ ).not.toThrow();
181
+ expect(onExpandedChange).not.toHaveBeenCalled();
182
+ });
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Existing non-virtualized tests (must stay green)
187
+ // ---------------------------------------------------------------------------
188
+
189
+ describe("Tree (non-virtualized)", () => {
190
+ it("renders a tree with treeitems", () => {
191
+ render(<Tree nodes={nodes} />);
192
+ expect(screen.getByRole("tree")).toBeInTheDocument();
193
+ expect(screen.getByRole("treeitem", { name: /Fruits/ })).toBeInTheDocument();
194
+ expect(screen.getByRole("treeitem", { name: /Vegetables/ })).toBeInTheDocument();
195
+ });
196
+
197
+ it("collapses children by default (descendants are not rendered)", () => {
198
+ render(<Tree nodes={nodes} />);
199
+ expect(screen.queryByRole("treeitem", { name: /Apple/ })).not.toBeInTheDocument();
200
+ });
201
+
202
+ it("uses roving tabindex — exactly one treeitem is tabbable", () => {
203
+ render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
204
+ const items = screen.getAllByRole("treeitem");
205
+ const tabbable = items.filter((el) => el.getAttribute("tabindex") === "0");
206
+ expect(tabbable).toHaveLength(1);
207
+ });
208
+
209
+ it("marks expanded parents with aria-expanded and renders their children", () => {
210
+ render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
211
+ expect(screen.getByRole("treeitem", { name: /Fruits/ })).toHaveAttribute(
212
+ "aria-expanded",
213
+ "true",
214
+ );
215
+ expect(screen.getByRole("treeitem", { name: /Apple/ })).toBeInTheDocument();
216
+ });
217
+
218
+ it("exposes hierarchy semantics (aria-level / setsize / posinset)", () => {
219
+ render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
220
+ const fruits = screen.getByRole("treeitem", { name: /Fruits/ });
221
+ expect(fruits).toHaveAttribute("aria-level", "1");
222
+ expect(fruits).toHaveAttribute("aria-setsize", "2");
223
+ expect(fruits).toHaveAttribute("aria-posinset", "1");
224
+ const apple = screen.getByRole("treeitem", { name: /Apple/ });
225
+ expect(apple).toHaveAttribute("aria-level", "2");
226
+ });
227
+
228
+ it("toggles expansion when a parent row is clicked", async () => {
229
+ const user = userEvent.setup();
230
+ render(<Tree nodes={nodes} selectionMode="none" />);
231
+ const fruits = screen.getByRole("treeitem", { name: /Fruits/ });
232
+ expect(fruits).toHaveAttribute("aria-expanded", "false");
233
+ await user.click(fruits);
234
+ expect(fruits).toHaveAttribute("aria-expanded", "true");
235
+ expect(screen.getByRole("treeitem", { name: /Apple/ })).toBeInTheDocument();
236
+ });
237
+
238
+ it("fires onSelectionChange and marks aria-selected on select", async () => {
239
+ const user = userEvent.setup();
240
+ const onSelectionChange = vi.fn();
241
+ render(
242
+ <Tree nodes={nodes} defaultExpandedIds={["fruits"]} onSelectionChange={onSelectionChange} />,
243
+ );
244
+ await user.click(screen.getByRole("treeitem", { name: /Apple/ }));
245
+ expect(onSelectionChange).toHaveBeenCalledWith(["apple"]);
246
+ expect(screen.getByRole("treeitem", { name: /Apple/ })).toHaveAttribute(
247
+ "aria-selected",
248
+ "true",
249
+ );
250
+ });
251
+
252
+ it("moves the active item with ArrowDown (roving focus)", async () => {
253
+ const user = userEvent.setup();
254
+ render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
255
+ const fruits = screen.getByRole("treeitem", { name: /Fruits/ });
256
+ fruits.focus();
257
+ expect(fruits).toHaveAttribute("tabindex", "0");
258
+ await user.keyboard("{ArrowDown}");
259
+ const apple = screen.getByRole("treeitem", { name: /Apple/ });
260
+ expect(apple).toHaveAttribute("tabindex", "0");
261
+ expect(fruits).toHaveAttribute("tabindex", "-1");
262
+ });
263
+
264
+ it("collapses an expanded parent with ArrowLeft", async () => {
265
+ const user = userEvent.setup();
266
+ render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
267
+ const fruits = screen.getByRole("treeitem", { name: /Fruits/ });
268
+ fruits.focus();
269
+ await user.keyboard("{ArrowLeft}");
270
+ expect(fruits).toHaveAttribute("aria-expanded", "false");
271
+ expect(screen.queryByRole("treeitem", { name: /Apple/ })).not.toBeInTheDocument();
272
+ });
273
+
274
+ it("does not select disabled nodes", async () => {
275
+ const user = userEvent.setup();
276
+ const onSelectionChange = vi.fn();
277
+ const withDisabled: TreeNode[] = [
278
+ { id: "a", label: "Alpha", disabled: true },
279
+ { id: "b", label: "Beta" },
280
+ ];
281
+ render(<Tree nodes={withDisabled} onSelectionChange={onSelectionChange} />);
282
+ await user.click(screen.getByRole("treeitem", { name: /Alpha/ }));
283
+ expect(onSelectionChange).not.toHaveBeenCalled();
284
+ });
285
+ });
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // Lazy loading tests
289
+ // ---------------------------------------------------------------------------
290
+
291
+ describe("Tree (lazy loading)", () => {
292
+ function makeLazyNodes(): TreeNode[] {
293
+ return [
294
+ { id: "root-a", label: "Category A", hasChildren: true },
295
+ { id: "root-b", label: "Category B" },
296
+ ];
297
+ }
298
+
299
+ it("shows the expand affordance for hasChildren nodes even without loaded children", () => {
300
+ render(<Tree nodes={makeLazyNodes()} />);
301
+ const catA = screen.getByRole("treeitem", { name: /Category A/ });
302
+ // aria-expanded should be present (false) because it's expandable
303
+ expect(catA).toHaveAttribute("aria-expanded", "false");
304
+ });
305
+
306
+ it("calls loadChildren on first expand and shows loading state (aria-busy)", async () => {
307
+ const user = userEvent.setup();
308
+ let resolveLoad!: (nodes: TreeNode[]) => void;
309
+ const loadChildren = vi.fn(
310
+ () =>
311
+ new Promise<TreeNode[]>((res) => {
312
+ resolveLoad = res;
313
+ }),
314
+ );
315
+
316
+ render(<Tree nodes={makeLazyNodes()} loadChildren={loadChildren} />);
317
+ const catA = screen.getByRole("treeitem", { name: /Category A/ });
318
+
319
+ await user.click(catA);
320
+
321
+ // loadChildren must have been called
322
+ expect(loadChildren).toHaveBeenCalledTimes(1);
323
+ expect(loadChildren).toHaveBeenCalledWith(expect.objectContaining({ id: "root-a" }));
324
+
325
+ // While loading, aria-busy is set on the parent treeitem
326
+ await waitFor(() => {
327
+ expect(screen.getByRole("treeitem", { name: /Category A/ })).toHaveAttribute(
328
+ "aria-busy",
329
+ "true",
330
+ );
331
+ });
332
+
333
+ // Resolve the promise with children
334
+ await act(async () => {
335
+ resolveLoad([
336
+ { id: "child-1", label: "Child 1" },
337
+ { id: "child-2", label: "Child 2" },
338
+ ]);
339
+ });
340
+
341
+ // Children should now be rendered
342
+ await waitFor(() => {
343
+ expect(screen.getByRole("treeitem", { name: /Child 1/ })).toBeInTheDocument();
344
+ expect(screen.getByRole("treeitem", { name: /Child 2/ })).toBeInTheDocument();
345
+ });
346
+
347
+ // aria-busy should be cleared
348
+ expect(screen.getByRole("treeitem", { name: /Category A/ })).not.toHaveAttribute("aria-busy");
349
+ });
350
+
351
+ it("does not call loadChildren again on re-expand (cached)", async () => {
352
+ const user = userEvent.setup();
353
+ const loadChildren = vi.fn().mockResolvedValue([{ id: "child-1", label: "Child 1" }]);
354
+
355
+ render(<Tree nodes={makeLazyNodes()} loadChildren={loadChildren} />);
356
+ const catA = screen.getByRole("treeitem", { name: /Category A/ });
357
+
358
+ // First expand — triggers fetch
359
+ await user.click(catA);
360
+ await waitFor(() => {
361
+ expect(screen.getByRole("treeitem", { name: /Child 1/ })).toBeInTheDocument();
362
+ });
363
+ expect(loadChildren).toHaveBeenCalledTimes(1);
364
+
365
+ // Collapse
366
+ await user.click(screen.getByRole("treeitem", { name: /Category A/ }));
367
+ await waitFor(() => {
368
+ expect(screen.queryByRole("treeitem", { name: /Child 1/ })).not.toBeInTheDocument();
369
+ });
370
+
371
+ // Re-expand — should NOT call loadChildren again
372
+ await user.click(screen.getByRole("treeitem", { name: /Category A/ }));
373
+ await waitFor(() => {
374
+ expect(screen.getByRole("treeitem", { name: /Child 1/ })).toBeInTheDocument();
375
+ });
376
+ expect(loadChildren).toHaveBeenCalledTimes(1);
377
+ });
378
+
379
+ it("shows error state and retry button when loadChildren rejects", async () => {
380
+ const user = userEvent.setup();
381
+ const loadChildren = vi.fn().mockRejectedValue(new Error("Network error"));
382
+
383
+ render(<Tree nodes={makeLazyNodes()} loadChildren={loadChildren} />);
384
+ const catA = screen.getByRole("treeitem", { name: /Category A/ });
385
+
386
+ await user.click(catA);
387
+
388
+ // Error state: "Failed to load" text visible, no crash
389
+ await waitFor(() => {
390
+ expect(screen.getByText(/Failed to load/i)).toBeInTheDocument();
391
+ });
392
+ expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
393
+
394
+ // aria-busy is NOT set in error state (it's cleared)
395
+ expect(screen.getByRole("treeitem", { name: /Category A/ })).not.toHaveAttribute("aria-busy");
396
+ });
397
+
398
+ it("retries successfully after error", async () => {
399
+ const user = userEvent.setup();
400
+ const loadChildren = vi
401
+ .fn()
402
+ .mockRejectedValueOnce(new Error("fail"))
403
+ .mockResolvedValue([{ id: "retry-child", label: "Retry Child" }]);
404
+
405
+ render(<Tree nodes={makeLazyNodes()} loadChildren={loadChildren} />);
406
+ await user.click(screen.getByRole("treeitem", { name: /Category A/ }));
407
+
408
+ await waitFor(() => expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument());
409
+
410
+ await user.click(screen.getByRole("button", { name: /retry/i }));
411
+
412
+ await waitFor(() => {
413
+ expect(screen.getByRole("treeitem", { name: /Retry Child/ })).toBeInTheDocument();
414
+ });
415
+ });
416
+ });
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // surface prop tests
420
+ // ---------------------------------------------------------------------------
421
+
422
+ describe("Tree (surface prop)", () => {
423
+ const simpleNodes: TreeNode[] = [
424
+ { id: "a", label: "Alpha" },
425
+ { id: "b", label: "Beta" },
426
+ ];
427
+
428
+ it("default surface uses bg-accent on the selected row", async () => {
429
+ const user = userEvent.setup();
430
+ render(<Tree nodes={simpleNodes} />);
431
+ await user.click(screen.getByRole("treeitem", { name: /Alpha/ }));
432
+ const selected = screen.getByRole("treeitem", { name: /Alpha/ });
433
+ expect(selected).toHaveAttribute("aria-selected", "true");
434
+ // classList gives exact token membership — avoids false matches from hover:bg-accent
435
+ expect(selected.classList.contains("bg-accent")).toBe(true);
436
+ expect(selected.classList.contains("bg-sidebar-accent")).toBe(false);
437
+ });
438
+
439
+ it("surface='default' (explicit) uses bg-accent on the selected row", async () => {
440
+ const user = userEvent.setup();
441
+ render(<Tree nodes={simpleNodes} surface="default" />);
442
+ await user.click(screen.getByRole("treeitem", { name: /Alpha/ }));
443
+ const selected = screen.getByRole("treeitem", { name: /Alpha/ });
444
+ expect(selected).toHaveAttribute("aria-selected", "true");
445
+ expect(selected.classList.contains("bg-accent")).toBe(true);
446
+ expect(selected.classList.contains("bg-sidebar-accent")).toBe(false);
447
+ });
448
+
449
+ it("surface='sidebar' uses bg-sidebar-accent + text-sidebar-accent-foreground on the selected row", async () => {
450
+ const user = userEvent.setup();
451
+ render(<Tree nodes={simpleNodes} surface="sidebar" />);
452
+ await user.click(screen.getByRole("treeitem", { name: /Alpha/ }));
453
+ const selected = screen.getByRole("treeitem", { name: /Alpha/ });
454
+ expect(selected).toHaveAttribute("aria-selected", "true");
455
+ // classList gives exact token membership — avoids false matches from hover:bg-accent
456
+ expect(selected.classList.contains("bg-sidebar-accent")).toBe(true);
457
+ expect(selected.classList.contains("text-sidebar-accent-foreground")).toBe(true);
458
+ expect(selected.classList.contains("bg-accent")).toBe(false);
459
+ });
460
+
461
+ it("surface='sidebar' threads through to nested child rows", async () => {
462
+ const user = userEvent.setup();
463
+ const nestedNodes: TreeNode[] = [
464
+ {
465
+ id: "parent",
466
+ label: "Parent",
467
+ children: [{ id: "child", label: "Child" }],
468
+ },
469
+ ];
470
+ render(<Tree nodes={nestedNodes} surface="sidebar" defaultExpandedIds={["parent"]} />);
471
+ await user.click(screen.getByRole("treeitem", { name: /Child/ }));
472
+ const selected = screen.getByRole("treeitem", { name: /Child/ });
473
+ expect(selected).toHaveAttribute("aria-selected", "true");
474
+ expect(selected.classList.contains("bg-sidebar-accent")).toBe(true);
475
+ expect(selected.classList.contains("bg-accent")).toBe(false);
476
+ });
477
+
478
+ it("surface='sidebar' works in virtualized mode", async () => {
479
+ const user = userEvent.setup();
480
+ const smallNodes: TreeNode[] = Array.from({ length: 20 }, (_, i) => ({
481
+ id: `n-${i}`,
482
+ label: `Node ${i + 1}`,
483
+ }));
484
+ render(
485
+ <Tree
486
+ nodes={smallNodes}
487
+ surface="sidebar"
488
+ virtualize
489
+ maxHeight="400px"
490
+ estimateRowHeight={32}
491
+ />,
492
+ );
493
+ await user.click(screen.getByRole("treeitem", { name: "Node 1" }));
494
+ const selected = screen.getByRole("treeitem", { name: "Node 1" });
495
+ expect(selected).toHaveAttribute("aria-selected", "true");
496
+ expect(selected.classList.contains("bg-sidebar-accent")).toBe(true);
497
+ expect(selected.classList.contains("bg-accent")).toBe(false);
498
+ });
499
+ });
500
+
501
+ // ---------------------------------------------------------------------------
502
+ // Virtualization tests
503
+ // ---------------------------------------------------------------------------
504
+
505
+ describe("Tree (virtualized)", () => {
506
+ function build(count: number): TreeNode[] {
507
+ return Array.from({ length: count }, (_, i) => ({ id: `n-${i}`, label: `Node ${i + 1}` }));
508
+ }
509
+
510
+ it("renders a scroll container and the role=tree root when virtualize=true", () => {
511
+ render(
512
+ <Tree nodes={build(5000)} virtualize maxHeight="400px" estimateRowHeight={ROW_HEIGHT} />,
513
+ );
514
+ expect(screen.getByRole("tree")).toBeInTheDocument();
515
+ });
516
+
517
+ it("renders far fewer than 5 000 treeitems in the DOM (windowing is active)", () => {
518
+ // Environment note: jsdom layout is stubbed above so the virtualizer can
519
+ // compute a non-zero window. The exact rendered count depends on the stub
520
+ // heights but should always be ≪ 5 000.
521
+ render(
522
+ <Tree nodes={build(5000)} virtualize maxHeight="400px" estimateRowHeight={ROW_HEIGHT} />,
523
+ );
524
+ const items = screen.queryAllByRole("treeitem");
525
+ // Expect far fewer than all 5000 nodes in the DOM.
526
+ // Under the jsdom stub (400px viewport / 32px rows = ~12–26 with overscan).
527
+ expect(items.length).toBeLessThan(100);
528
+ expect(items.length).toBeGreaterThan(0);
529
+ });
530
+
531
+ it("carries correct ARIA attributes on virtual rows (level / setsize / posinset)", () => {
532
+ const smallNodes = build(20);
533
+ render(<Tree nodes={smallNodes} virtualize maxHeight="400px" estimateRowHeight={ROW_HEIGHT} />);
534
+ // First rendered item should be node-0 at level 1, setsize 20, posinset 1
535
+ const first = screen.getByRole("treeitem", { name: "Node 1" });
536
+ expect(first).toHaveAttribute("aria-level", "1");
537
+ expect(first).toHaveAttribute("aria-setsize", "20");
538
+ expect(first).toHaveAttribute("aria-posinset", "1");
539
+ });
540
+
541
+ it("ArrowDown moves active tabindex in the virtual flat list", async () => {
542
+ const user = userEvent.setup();
543
+ const smallNodes = build(30);
544
+ render(<Tree nodes={smallNodes} virtualize maxHeight="400px" estimateRowHeight={ROW_HEIGHT} />);
545
+
546
+ const first = screen.getByRole("treeitem", { name: "Node 1" });
547
+ first.focus();
548
+ expect(first).toHaveAttribute("tabindex", "0");
549
+
550
+ await user.keyboard("{ArrowDown}");
551
+
552
+ const second = screen.getByRole("treeitem", { name: "Node 2" });
553
+ expect(second).toHaveAttribute("tabindex", "0");
554
+ expect(first).toHaveAttribute("tabindex", "-1");
555
+ });
556
+
557
+ it("selects a node on click in virtual mode", async () => {
558
+ const user = userEvent.setup();
559
+ const onSelectionChange = vi.fn();
560
+ const smallNodes = build(20);
561
+ render(
562
+ <Tree
563
+ nodes={smallNodes}
564
+ virtualize
565
+ maxHeight="400px"
566
+ estimateRowHeight={ROW_HEIGHT}
567
+ onSelectionChange={onSelectionChange}
568
+ />,
569
+ );
570
+ await user.click(screen.getByRole("treeitem", { name: "Node 3" }));
571
+ expect(onSelectionChange).toHaveBeenCalledWith(["n-2"]);
572
+ });
573
+ });
574
+
575
+ // ---------------------------------------------------------------------------
576
+ // accessory / trailing slot (#369)
577
+ // ---------------------------------------------------------------------------
578
+
579
+ describe("Tree (accessory / trailing slot, #369)", () => {
580
+ const nodesWithAccessory: TreeNode[] = [
581
+ { id: "a", label: "package.json", accessory: <span>1.2 KB</span> },
582
+ { id: "b", label: "index.ts" },
583
+ ];
584
+
585
+ it("renders the accessory content in the row", () => {
586
+ render(<Tree nodes={nodesWithAccessory} />);
587
+ expect(screen.getByText("1.2 KB")).toBeInTheDocument();
588
+ });
589
+
590
+ it("the row's accessible name is exactly the node label — the accessory never leaks in", () => {
591
+ render(<Tree nodes={nodesWithAccessory} />);
592
+ expect(screen.getByRole("treeitem", { name: "package.json" })).toBeInTheDocument();
593
+ expect(screen.queryByRole("treeitem", { name: "package.json1.2 KB" })).not.toBeInTheDocument();
594
+ });
595
+
596
+ it("clicking the accessory does not select or expand the row (non-virtual path)", async () => {
597
+ const user = userEvent.setup();
598
+ const onSelectionChange = vi.fn();
599
+ render(<Tree nodes={nodesWithAccessory} onSelectionChange={onSelectionChange} />);
600
+ await user.click(screen.getByText("1.2 KB"));
601
+ expect(onSelectionChange).not.toHaveBeenCalled();
602
+ expect(screen.getByRole("treeitem", { name: "package.json" })).not.toHaveAttribute(
603
+ "aria-selected",
604
+ "true",
605
+ );
606
+ });
607
+
608
+ it("clicking the row itself (not the accessory) still selects it (non-virtual path)", async () => {
609
+ const user = userEvent.setup();
610
+ const onSelectionChange = vi.fn();
611
+ render(<Tree nodes={nodesWithAccessory} onSelectionChange={onSelectionChange} />);
612
+ await user.click(screen.getByRole("treeitem", { name: "package.json" }));
613
+ expect(onSelectionChange).toHaveBeenCalledWith(["a"]);
614
+ });
615
+
616
+ it("keyboard select (Enter) is unaffected by the accessory's presence", async () => {
617
+ const user = userEvent.setup();
618
+ const onSelectionChange = vi.fn();
619
+ render(<Tree nodes={nodesWithAccessory} onSelectionChange={onSelectionChange} />);
620
+ const row = screen.getByRole("treeitem", { name: "package.json" });
621
+ row.focus();
622
+ await user.keyboard("{Enter}");
623
+ expect(onSelectionChange).toHaveBeenCalledWith(["a"]);
624
+ });
625
+
626
+ it("renders the accessory and guards clicks in virtualized mode too (the duplicated render path)", async () => {
627
+ const user = userEvent.setup();
628
+ const onSelectionChange = vi.fn();
629
+ render(
630
+ <Tree
631
+ nodes={nodesWithAccessory}
632
+ virtualize
633
+ maxHeight="400px"
634
+ estimateRowHeight={ROW_HEIGHT}
635
+ onSelectionChange={onSelectionChange}
636
+ />,
637
+ );
638
+ expect(screen.getByText("1.2 KB")).toBeInTheDocument();
639
+ expect(screen.getByRole("treeitem", { name: "package.json" })).toBeInTheDocument();
640
+ await user.click(screen.getByText("1.2 KB"));
641
+ expect(onSelectionChange).not.toHaveBeenCalled();
642
+ });
643
+
644
+ // `TreeNode.label` is a ReactNode, so `aria-label` (string-only) cannot name
645
+ // every row — without `aria-labelledby` the name falls back to the row's
646
+ // CONTENTS and the accessory leaks in. Locks both render paths.
647
+ it.each([
648
+ ["non-virtual", false],
649
+ ["virtualized", true],
650
+ ])("a ReactNode label still excludes the accessory from the name (%s)", (_name, virtualize) => {
651
+ const nodes: TreeNode[] = [
652
+ { id: "a", label: <span>package.json</span>, accessory: <span>1.2 KB</span> },
653
+ ];
654
+ render(
655
+ <Tree
656
+ nodes={nodes}
657
+ virtualize={virtualize}
658
+ maxHeight={virtualize ? "400px" : undefined}
659
+ estimateRowHeight={virtualize ? ROW_HEIGHT : undefined}
660
+ />,
661
+ );
662
+ expect(screen.getByRole("treeitem", { name: "package.json" })).toBeInTheDocument();
663
+ expect(screen.queryByRole("treeitem", { name: /1\.2 KB/ })).not.toBeInTheDocument();
664
+ });
665
+
666
+ // The tree's keyboard handler lives on the ROOT and preventDefaults
667
+ // Enter/Space/arrows; the row's onFocus (focusin — it bubbles) re-focuses the
668
+ // row. Both must stop at the accessory or interactive content there is dead.
669
+ it("interactive accessory content is focusable and activates itself, not the row", async () => {
670
+ const user = userEvent.setup();
671
+ const onSelectionChange = vi.fn();
672
+ const onAccessoryClick = vi.fn();
673
+ const nodes: TreeNode[] = [
674
+ {
675
+ id: "a",
676
+ label: "package.json",
677
+ accessory: (
678
+ <button type="button" onClick={onAccessoryClick}>
679
+ Details
680
+ </button>
681
+ ),
682
+ },
683
+ { id: "b", label: "index.ts" },
684
+ ];
685
+ render(<Tree nodes={nodes} onSelectionChange={onSelectionChange} />);
686
+
687
+ const accessoryButton = screen.getByRole("button", { name: "Details" });
688
+ accessoryButton.focus();
689
+ expect(document.activeElement).toBe(accessoryButton);
690
+
691
+ await user.keyboard("{Enter}");
692
+ expect(onAccessoryClick).toHaveBeenCalledTimes(1);
693
+ expect(onSelectionChange).not.toHaveBeenCalled();
694
+
695
+ // Tree navigation must not steal focus out of the accessory.
696
+ await user.keyboard("{ArrowDown}");
697
+ expect(document.activeElement).toBe(accessoryButton);
698
+ });
699
+ });