@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,509 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import {
5
+ RevisionTimeline,
6
+ type Revision,
7
+ type RevisionBranch,
8
+ type RevisionEdge,
9
+ } from "./revision-timeline";
10
+
11
+ afterEach(cleanup);
12
+
13
+ // ─── Fixtures ────────────────────────────────────────────────────────────────
14
+
15
+ const DAY1 = new Date("2024-06-14T09:15:00");
16
+ const DAY2 = new Date("2024-06-13T14:30:00");
17
+
18
+ const REVISIONS: Revision[] = [
19
+ {
20
+ id: "r1",
21
+ message: "feat: add component",
22
+ author: "Alice",
23
+ shortSha: "abc1234",
24
+ timestamp: DAY1,
25
+ additions: 100,
26
+ deletions: 5,
27
+ lane: 0,
28
+ refs: ["main", "HEAD"],
29
+ },
30
+ {
31
+ id: "r2",
32
+ message: "fix: correct styles",
33
+ author: "Bob",
34
+ shortSha: "def5678",
35
+ timestamp: DAY1,
36
+ additions: 10,
37
+ deletions: 2,
38
+ lane: 0,
39
+ },
40
+ {
41
+ id: "r3",
42
+ message: "chore: initial commit",
43
+ author: "Alice",
44
+ shortSha: "ghi9012",
45
+ timestamp: DAY2,
46
+ additions: 50,
47
+ deletions: 0,
48
+ lane: 0,
49
+ },
50
+ ];
51
+
52
+ const MERGE_REVISIONS: Revision[] = [
53
+ {
54
+ id: "m1",
55
+ message: "Merge branch feat into main",
56
+ author: "Alice",
57
+ shortSha: "merge01",
58
+ timestamp: DAY1,
59
+ additions: 0,
60
+ deletions: 0,
61
+ lane: 0,
62
+ },
63
+ {
64
+ id: "f1",
65
+ message: "feat: feature work",
66
+ author: "Bob",
67
+ shortSha: "feat001",
68
+ timestamp: DAY1,
69
+ additions: 30,
70
+ deletions: 0,
71
+ lane: 1,
72
+ },
73
+ {
74
+ id: "b1",
75
+ message: "chore: base work",
76
+ author: "Alice",
77
+ shortSha: "base001",
78
+ timestamp: DAY2,
79
+ additions: 20,
80
+ deletions: 0,
81
+ lane: 0,
82
+ },
83
+ ];
84
+
85
+ const MERGE_EDGES: RevisionEdge[] = [
86
+ { from: "m1", to: "f1" },
87
+ { from: "m1", to: "b1" },
88
+ { from: "f1", to: "b1" },
89
+ ];
90
+
91
+ // ─── Day grouping ────────────────────────────────────────────────────────────
92
+
93
+ describe("Day grouping", () => {
94
+ it("renders a day-group separator for each distinct calendar day", () => {
95
+ render(<RevisionTimeline revisions={REVISIONS} groupBy="day" />);
96
+ // Two distinct days → two separators
97
+ const separators = screen.getAllByRole("separator");
98
+ expect(separators).toHaveLength(2);
99
+ });
100
+
101
+ it("day header label contains recognisable date text", () => {
102
+ render(<RevisionTimeline revisions={REVISIONS} groupBy="day" />);
103
+ const separators = screen.getAllByRole("separator");
104
+ // Each separator has an aria-label with the formatted date
105
+ expect(separators[0]!.getAttribute("aria-label")).toBeTruthy();
106
+ expect(separators[1]!.getAttribute("aria-label")).toBeTruthy();
107
+ // The two labels must be different (different days)
108
+ expect(separators[0]!.getAttribute("aria-label")).not.toBe(
109
+ separators[1]!.getAttribute("aria-label"),
110
+ );
111
+ });
112
+
113
+ it("renders no separators when groupBy=none", () => {
114
+ render(<RevisionTimeline revisions={REVISIONS} groupBy="none" />);
115
+ expect(screen.queryAllByRole("separator")).toHaveLength(0);
116
+ });
117
+
118
+ it("accepts a custom formatDay function", () => {
119
+ render(
120
+ <RevisionTimeline revisions={REVISIONS} groupBy="day" formatDay={() => "CUSTOM-DAY-LABEL"} />,
121
+ );
122
+ expect(screen.getAllByText("CUSTOM-DAY-LABEL")).toHaveLength(2);
123
+ });
124
+ });
125
+
126
+ // ─── List semantics ──────────────────────────────────────────────────────────
127
+
128
+ describe("List semantics", () => {
129
+ it("renders a list with one listitem per revision", () => {
130
+ render(<RevisionTimeline revisions={REVISIONS} />);
131
+ expect(screen.getAllByRole("listitem")).toHaveLength(REVISIONS.length);
132
+ });
133
+
134
+ it("each listitem contains a button with a descriptive aria-label", () => {
135
+ render(<RevisionTimeline revisions={REVISIONS} />);
136
+ const buttons = screen.getAllByRole("button");
137
+ expect(buttons).toHaveLength(REVISIONS.length);
138
+ expect(buttons[0]!.getAttribute("aria-label")).toContain("feat: add component");
139
+ });
140
+ });
141
+
142
+ // ─── Churn indicators ────────────────────────────────────────────────────────
143
+
144
+ describe("Churn indicators", () => {
145
+ it("renders the additions count with tabular-nums and accessible label", () => {
146
+ render(<RevisionTimeline revisions={[REVISIONS[0]!]} groupBy="none" />);
147
+ const churnEl = document.querySelector('[aria-label*="additions"]');
148
+ expect(churnEl).not.toBeNull();
149
+ expect(churnEl!.textContent).toContain("100");
150
+ expect(churnEl!.textContent).toContain("5");
151
+ expect(churnEl!.className).toContain("tabular-nums");
152
+ });
153
+
154
+ it("hides the churn indicator when both additions and deletions are 0 or undefined", () => {
155
+ const rev: Revision = {
156
+ id: "x",
157
+ message: "no churn",
158
+ timestamp: DAY1,
159
+ lane: 0,
160
+ };
161
+ render(<RevisionTimeline revisions={[rev]} groupBy="none" />);
162
+ expect(document.querySelector('[aria-label*="additions"]')).toBeNull();
163
+ });
164
+
165
+ it("shows only additions when deletions is 0", () => {
166
+ const rev: Revision = {
167
+ id: "x",
168
+ message: "adds only",
169
+ timestamp: DAY1,
170
+ lane: 0,
171
+ additions: 42,
172
+ deletions: 0,
173
+ };
174
+ render(<RevisionTimeline revisions={[rev]} groupBy="none" />);
175
+ const churnEl = document.querySelector('[aria-label*="additions"]');
176
+ expect(churnEl).not.toBeNull();
177
+ expect(churnEl!.textContent).toContain("42");
178
+ });
179
+ });
180
+
181
+ // ─── Selection — uncontrolled ────────────────────────────────────────────────
182
+
183
+ describe("Selection (uncontrolled)", () => {
184
+ it("calls onSelect with the revision id when a row is clicked", async () => {
185
+ const onSelect = vi.fn();
186
+ const user = userEvent.setup();
187
+ render(<RevisionTimeline revisions={REVISIONS} groupBy="none" onSelect={onSelect} />);
188
+
189
+ const buttons = screen.getAllByRole("button");
190
+ await user.click(buttons[0]!);
191
+ expect(onSelect).toHaveBeenCalledTimes(1);
192
+ expect(onSelect).toHaveBeenCalledWith("r1");
193
+ });
194
+
195
+ it("marks clicked row as aria-pressed=true", async () => {
196
+ const user = userEvent.setup();
197
+ render(<RevisionTimeline revisions={REVISIONS} groupBy="none" />);
198
+
199
+ const buttons = screen.getAllByRole("button");
200
+ await user.click(buttons[1]!);
201
+ expect(buttons[1]).toHaveAttribute("aria-pressed", "true");
202
+ expect(buttons[0]).toHaveAttribute("aria-pressed", "false");
203
+ });
204
+
205
+ it("moves selection when a different row is clicked", async () => {
206
+ const user = userEvent.setup();
207
+ render(<RevisionTimeline revisions={REVISIONS} groupBy="none" />);
208
+
209
+ const buttons = screen.getAllByRole("button");
210
+ await user.click(buttons[0]!);
211
+ await user.click(buttons[2]!);
212
+ expect(buttons[0]).toHaveAttribute("aria-pressed", "false");
213
+ expect(buttons[2]).toHaveAttribute("aria-pressed", "true");
214
+ });
215
+
216
+ it("respects defaultSelectedId for initial state", () => {
217
+ render(<RevisionTimeline revisions={REVISIONS} groupBy="none" defaultSelectedId="r2" />);
218
+ const buttons = screen.getAllByRole("button");
219
+ expect(buttons[1]).toHaveAttribute("aria-pressed", "true");
220
+ expect(buttons[0]).toHaveAttribute("aria-pressed", "false");
221
+ });
222
+ });
223
+
224
+ // ─── Selection — controlled ───────────────────────────────────────────────────
225
+
226
+ describe("Selection (controlled)", () => {
227
+ it("reflects the controlled selectedId", () => {
228
+ render(<RevisionTimeline revisions={REVISIONS} groupBy="none" selectedId="r3" />);
229
+ const buttons = screen.getAllByRole("button");
230
+ expect(buttons[2]).toHaveAttribute("aria-pressed", "true");
231
+ expect(buttons[0]).toHaveAttribute("aria-pressed", "false");
232
+ });
233
+
234
+ it("calls onSelect but does not change selection internally when controlled", async () => {
235
+ const onSelect = vi.fn();
236
+ const user = userEvent.setup();
237
+ render(
238
+ <RevisionTimeline revisions={REVISIONS} groupBy="none" selectedId="r1" onSelect={onSelect} />,
239
+ );
240
+ const buttons = screen.getAllByRole("button");
241
+ await user.click(buttons[1]!);
242
+ expect(onSelect).toHaveBeenCalledWith("r2");
243
+ // Controlled — selection stays on r1 because selectedId prop hasn't changed
244
+ expect(buttons[0]).toHaveAttribute("aria-pressed", "true");
245
+ expect(buttons[1]).toHaveAttribute("aria-pressed", "false");
246
+ });
247
+ });
248
+
249
+ // ─── Merge rows ─────────────────────────────────────────────────────────────
250
+
251
+ describe("Merge rows", () => {
252
+ it("renders a merge commit row when a revision has ≥2 incoming edges", () => {
253
+ render(<RevisionTimeline revisions={MERGE_REVISIONS} edges={MERGE_EDGES} groupBy="none" />);
254
+ // The merge commit button should exist
255
+ const mergeBtn = screen.getByRole("button", {
256
+ name: /Merge branch feat into main/,
257
+ });
258
+ expect(mergeBtn).toBeInTheDocument();
259
+ });
260
+
261
+ it("renders an SVG gutter for branch lanes", () => {
262
+ render(<RevisionTimeline revisions={MERGE_REVISIONS} edges={MERGE_EDGES} groupBy="none" />);
263
+ const svg = document.querySelector("svg[aria-hidden='true']");
264
+ expect(svg).not.toBeNull();
265
+ });
266
+
267
+ it("renders all revisions including the merge commit", () => {
268
+ render(<RevisionTimeline revisions={MERGE_REVISIONS} edges={MERGE_EDGES} groupBy="none" />);
269
+ expect(screen.getAllByRole("listitem")).toHaveLength(MERGE_REVISIONS.length);
270
+ });
271
+ });
272
+
273
+ // ─── forwardRef / className / ...props ───────────────────────────────────────
274
+
275
+ describe("API surface", () => {
276
+ it("merges className onto the root element", () => {
277
+ render(
278
+ <RevisionTimeline
279
+ revisions={REVISIONS}
280
+ groupBy="none"
281
+ className="custom-class"
282
+ data-testid="root"
283
+ />,
284
+ );
285
+ const root = screen.getByTestId("root");
286
+ expect(root).toHaveClass("custom-class");
287
+ });
288
+
289
+ it("spreads extra props onto the root element", () => {
290
+ render(
291
+ <RevisionTimeline
292
+ revisions={REVISIONS}
293
+ groupBy="none"
294
+ data-testid="root"
295
+ aria-label="Git history"
296
+ />,
297
+ );
298
+ expect(screen.getByTestId("root")).toHaveAttribute("aria-label", "Git history");
299
+ });
300
+ });
301
+
302
+ // ─── Branch collapse ─────────────────────────────────────────────────────────
303
+
304
+ const CD1 = new Date("2024-07-02T10:00:00");
305
+ const CD2 = new Date("2024-07-01T10:00:00");
306
+
307
+ // Topology (newest-first): main (lane 0) with a feat branch (lane 1) that has a
308
+ // nested feat-sub branch (lane 2). `main` is intentionally NOT listed in
309
+ // `branches`, so the trunk stays permanently visible / non-collapsible.
310
+ const COLLAPSE_REVISIONS: Revision[] = [
311
+ { id: "m1", message: "Merge feat/work into main", timestamp: CD1, lane: 0, branchId: "main" },
312
+ { id: "s1", message: "feat-sub: refine sub work", timestamp: CD1, lane: 2, branchId: "feat-sub" },
313
+ { id: "f1", message: "feat: main feature work", timestamp: CD1, lane: 1, branchId: "feat" },
314
+ { id: "b1", message: "chore: bump deps", timestamp: CD1, lane: 0, branchId: "main" },
315
+ { id: "s0", message: "feat-sub: scaffold sub", timestamp: CD2, lane: 2, branchId: "feat-sub" },
316
+ { id: "f0", message: "feat: scaffold feature", timestamp: CD2, lane: 1, branchId: "feat" },
317
+ { id: "b0", message: "chore: initial", timestamp: CD2, lane: 0, branchId: "main" },
318
+ ];
319
+
320
+ const COLLAPSE_BRANCHES: RevisionBranch[] = [
321
+ { id: "feat", name: "feat/work" },
322
+ { id: "feat-sub", name: "feat/work/sub", parentBranchId: "feat" },
323
+ ];
324
+
325
+ const COLLAPSE_EDGES: RevisionEdge[] = [
326
+ { from: "m1", to: "f1" },
327
+ { from: "m1", to: "b1" },
328
+ { from: "f1", to: "f0" },
329
+ { from: "f0", to: "b0" },
330
+ { from: "b1", to: "b0" },
331
+ { from: "s1", to: "s0" },
332
+ { from: "s0", to: "f0" },
333
+ ];
334
+
335
+ describe("Branch collapse", () => {
336
+ it("hides a collapsed branch's commits behind a summary row", () => {
337
+ render(
338
+ <RevisionTimeline
339
+ revisions={COLLAPSE_REVISIONS}
340
+ edges={COLLAPSE_EDGES}
341
+ branches={COLLAPSE_BRANCHES}
342
+ defaultCollapsedBranchIds={["feat"]}
343
+ groupBy="none"
344
+ />,
345
+ );
346
+ expect(screen.queryByText("feat: main feature work")).toBeNull();
347
+ expect(screen.queryByText("feat: scaffold feature")).toBeNull();
348
+ const summary = screen.getByRole("button", {
349
+ name: /Collapsed branch feat\/work,.*expand/,
350
+ });
351
+ expect(summary).toHaveAttribute("aria-expanded", "false");
352
+ });
353
+
354
+ it("summary commit count equals the hidden subtree size", () => {
355
+ render(
356
+ <RevisionTimeline
357
+ revisions={COLLAPSE_REVISIONS}
358
+ branches={COLLAPSE_BRANCHES}
359
+ defaultCollapsedBranchIds={["feat"]}
360
+ groupBy="none"
361
+ />,
362
+ );
363
+ const summary = screen.getByRole("button", { name: /Collapsed branch feat\/work/ });
364
+ // feat + nested feat-sub = 4 hidden commits
365
+ expect(summary.getAttribute("aria-label")).toContain("4 commits");
366
+ expect(summary.textContent).toContain("4 commits");
367
+ });
368
+
369
+ it("collapsing a parent branch also hides its nested descendant branch", () => {
370
+ render(
371
+ <RevisionTimeline
372
+ revisions={COLLAPSE_REVISIONS}
373
+ branches={COLLAPSE_BRANCHES}
374
+ defaultCollapsedBranchIds={["feat"]}
375
+ groupBy="none"
376
+ />,
377
+ );
378
+ expect(screen.queryByText("feat-sub: scaffold sub")).toBeNull();
379
+ expect(screen.queryByText("feat-sub: refine sub work")).toBeNull();
380
+ // exactly one summary (feat) — feat-sub is folded into it, not its own row
381
+ expect(screen.getAllByRole("button", { name: /Collapsed branch/ })).toHaveLength(1);
382
+ });
383
+
384
+ it("does not double-count when a nested child is also collapsed", () => {
385
+ render(
386
+ <RevisionTimeline
387
+ revisions={COLLAPSE_REVISIONS}
388
+ branches={COLLAPSE_BRANCHES}
389
+ defaultCollapsedBranchIds={["feat", "feat-sub"]}
390
+ groupBy="none"
391
+ />,
392
+ );
393
+ const summaries = screen.getAllByRole("button", { name: /Collapsed branch/ });
394
+ expect(summaries).toHaveLength(1);
395
+ expect(summaries[0]!.getAttribute("aria-label")).toContain("4 commits");
396
+ });
397
+
398
+ it("renders a collapse toggle on each expanded branch tip", () => {
399
+ render(
400
+ <RevisionTimeline
401
+ revisions={COLLAPSE_REVISIONS}
402
+ branches={COLLAPSE_BRANCHES}
403
+ groupBy="none"
404
+ />,
405
+ );
406
+ expect(screen.getByRole("button", { name: "Collapse branch feat/work" })).toHaveAttribute(
407
+ "aria-expanded",
408
+ "true",
409
+ );
410
+ expect(screen.getByRole("button", { name: "Collapse branch feat/work/sub" })).toHaveAttribute(
411
+ "aria-expanded",
412
+ "true",
413
+ );
414
+ });
415
+
416
+ it("toggles a branch via the summary and tip buttons (uncontrolled)", async () => {
417
+ const user = userEvent.setup();
418
+ render(
419
+ <RevisionTimeline
420
+ revisions={COLLAPSE_REVISIONS}
421
+ branches={COLLAPSE_BRANCHES}
422
+ defaultCollapsedBranchIds={["feat"]}
423
+ groupBy="none"
424
+ />,
425
+ );
426
+ // expand via the summary
427
+ await user.click(screen.getByRole("button", { name: /Collapsed branch feat\/work/ }));
428
+ expect(screen.getByText("feat: main feature work")).toBeInTheDocument();
429
+ // re-collapse via the tip toggle
430
+ await user.click(screen.getByRole("button", { name: "Collapse branch feat/work" }));
431
+ expect(screen.queryByText("feat: main feature work")).toBeNull();
432
+ });
433
+
434
+ it("is controllable via collapsedBranchIds + onBranchToggle", async () => {
435
+ const onBranchToggle = vi.fn();
436
+ const user = userEvent.setup();
437
+ render(
438
+ <RevisionTimeline
439
+ revisions={COLLAPSE_REVISIONS}
440
+ branches={COLLAPSE_BRANCHES}
441
+ collapsedBranchIds={["feat"]}
442
+ onBranchToggle={onBranchToggle}
443
+ groupBy="none"
444
+ />,
445
+ );
446
+ await user.click(screen.getByRole("button", { name: /Collapsed branch feat\/work/ }));
447
+ expect(onBranchToggle).toHaveBeenCalledWith("feat", false);
448
+ // controlled — DOM does not change until the prop changes
449
+ expect(screen.queryByText("feat: main feature work")).toBeNull();
450
+ expect(screen.getByRole("button", { name: /Collapsed branch feat\/work/ })).toBeInTheDocument();
451
+ });
452
+
453
+ it("keeps the merge connector to a collapsed branch (edge remap renders a path)", () => {
454
+ render(
455
+ <RevisionTimeline
456
+ revisions={COLLAPSE_REVISIONS}
457
+ edges={COLLAPSE_EDGES}
458
+ branches={COLLAPSE_BRANCHES}
459
+ defaultCollapsedBranchIds={["feat"]}
460
+ groupBy="none"
461
+ />,
462
+ );
463
+ expect(document.querySelectorAll("svg path").length).toBeGreaterThanOrEqual(1);
464
+ });
465
+
466
+ it("does not change rendering when no branches are supplied (backward compatible)", () => {
467
+ render(
468
+ <RevisionTimeline revisions={COLLAPSE_REVISIONS} edges={COLLAPSE_EDGES} groupBy="none" />,
469
+ );
470
+ expect(screen.getAllByRole("button")).toHaveLength(COLLAPSE_REVISIONS.length);
471
+ expect(document.querySelector("[aria-expanded]")).toBeNull();
472
+ });
473
+
474
+ it("treats a branchId with no matching branches entry as a normal row", () => {
475
+ const revs: Revision[] = [
476
+ { id: "g1", message: "ghost branch commit", timestamp: CD1, lane: 1, branchId: "ghost" },
477
+ { id: "g0", message: "main commit", timestamp: CD1, lane: 0, branchId: "main" },
478
+ ];
479
+ render(
480
+ <RevisionTimeline
481
+ revisions={revs}
482
+ branches={COLLAPSE_BRANCHES}
483
+ defaultCollapsedBranchIds={["ghost"]}
484
+ groupBy="none"
485
+ />,
486
+ );
487
+ expect(screen.getByText("ghost branch commit")).toBeInTheDocument();
488
+ expect(document.querySelector("[aria-expanded]")).toBeNull();
489
+ expect(screen.queryByRole("button", { name: /Collapsed branch/ })).toBeNull();
490
+ });
491
+
492
+ it("uses singular 'commit' for a single-commit branch", () => {
493
+ const revs: Revision[] = [
494
+ { id: "x1", message: "main tip", timestamp: CD1, lane: 0, branchId: "main" },
495
+ { id: "x0", message: "solo feature commit", timestamp: CD1, lane: 1, branchId: "feat" },
496
+ ];
497
+ render(
498
+ <RevisionTimeline
499
+ revisions={revs}
500
+ branches={[{ id: "feat", name: "feat/solo" }]}
501
+ defaultCollapsedBranchIds={["feat"]}
502
+ groupBy="none"
503
+ />,
504
+ );
505
+ const summary = screen.getByRole("button", { name: /Collapsed branch feat\/solo/ });
506
+ expect(summary.getAttribute("aria-label")).toContain("1 commit,");
507
+ expect(summary.getAttribute("aria-label")).not.toContain("1 commits");
508
+ });
509
+ });