@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,58 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { Switch } from "./switch";
4
+ import { Label } from "../label";
5
+ const meta = {
6
+ title: "Forms/Switch",
7
+ component: Switch,
8
+ tags: ["autodocs"],
9
+ argTypes: {
10
+ checked: {
11
+ description: "Controlled checked state.",
12
+ control: "boolean",
13
+ table: { category: "State" },
14
+ },
15
+ defaultChecked: {
16
+ description: "Uncontrolled initial checked state.",
17
+ control: "boolean",
18
+ table: { category: "State" },
19
+ },
20
+ disabled: {
21
+ description: "Disables the switch.",
22
+ control: "boolean",
23
+ table: { category: "State" },
24
+ },
25
+ required: {
26
+ description: "Marks the switch as required in a form context.",
27
+ control: "boolean",
28
+ table: { category: "Behavior" },
29
+ },
30
+ onCheckedChange: {
31
+ description: "Called when the checked state changes.",
32
+ control: false,
33
+ table: { category: "Behavior" },
34
+ },
35
+ className: {
36
+ description: "Additional CSS classes applied to the root element.",
37
+ control: "text",
38
+ table: { category: "Appearance" },
39
+ },
40
+ },
41
+ } satisfies Meta<typeof Switch>;
42
+ export default meta;
43
+ type Story = StoryObj<typeof meta>;
44
+ export const Default: Story = {
45
+ render: () => (
46
+ <div className="flex items-center gap-2">
47
+ <Switch id="airplane" defaultChecked />
48
+ <Label htmlFor="airplane">Airplane mode</Label>
49
+ </div>
50
+ ),
51
+ // Starts on (defaultChecked); clicking toggles the reflected aria-checked state.
52
+ play: async ({ canvas, userEvent }) => {
53
+ const sw = canvas.getByRole("switch");
54
+ await expect(sw).toHaveAttribute("aria-checked", "true");
55
+ await userEvent.click(sw);
56
+ await expect(sw).toHaveAttribute("aria-checked", "false");
57
+ },
58
+ };
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Switch } from "./switch";
5
+ describe("Switch", () => {
6
+ it("toggles on click", async () => {
7
+ render(<Switch aria-label="s" />);
8
+ const sw = screen.getByRole("switch", { name: "s" });
9
+ await userEvent.click(sw);
10
+ expect(sw).toBeChecked();
11
+ });
12
+ });
@@ -0,0 +1,29 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
3
+ import { cn } from "../../lib/cn";
4
+
5
+ export const Switch = forwardRef<
6
+ ElementRef<typeof SwitchPrimitive.Root>,
7
+ ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
8
+ >(function Switch({ className, ...props }, ref) {
9
+ return (
10
+ <SwitchPrimitive.Root
11
+ ref={ref}
12
+ className={cn(
13
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors duration-fast ease-standard",
14
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
15
+ "disabled:cursor-not-allowed disabled:opacity-50",
16
+ "data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
17
+ className,
18
+ )}
19
+ {...props}
20
+ >
21
+ <SwitchPrimitive.Thumb
22
+ className={cn(
23
+ "pointer-events-none block size-4 rounded-full bg-background shadow-xs ring-0 transition-transform duration-fast ease-standard",
24
+ "data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
25
+ )}
26
+ />
27
+ </SwitchPrimitive.Root>
28
+ );
29
+ });
@@ -0,0 +1,10 @@
1
+ export {
2
+ Table,
3
+ TableHeader,
4
+ TableBody,
5
+ TableFooter,
6
+ TableRow,
7
+ TableHead,
8
+ TableCell,
9
+ TableCaption,
10
+ } from "./table";
@@ -0,0 +1,53 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table";
3
+ const meta = {
4
+ title: "Data/Table",
5
+ component: Table,
6
+ tags: ["autodocs"],
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component:
11
+ "Low-level static table primitives (header/body/row/cell styling). For sorting, filtering, " +
12
+ "pagination, virtualization, row selection and column management, use the TanStack-powered " +
13
+ "DataTable (see Data/DataTable, @elabs-ai/components-data) instead.",
14
+ },
15
+ },
16
+ },
17
+ argTypes: {
18
+ className: {
19
+ description: "Additional CSS classes applied to the `<table>` element.",
20
+ control: "text",
21
+ table: { category: "Styling" },
22
+ },
23
+ children: {
24
+ description: "Table content — compose with TableHeader, TableBody, TableRow, TableCell, etc.",
25
+ control: false,
26
+ table: { category: "Content" },
27
+ },
28
+ },
29
+ } satisfies Meta<typeof Table>;
30
+ export default meta;
31
+ type Story = StoryObj<typeof meta>;
32
+ export const Default: Story = {
33
+ render: () => (
34
+ <Table>
35
+ <TableHeader>
36
+ <TableRow>
37
+ <TableHead>Service</TableHead>
38
+ <TableHead>Status</TableHead>
39
+ </TableRow>
40
+ </TableHeader>
41
+ <TableBody>
42
+ <TableRow>
43
+ <TableCell>api-gateway</TableCell>
44
+ <TableCell>healthy</TableCell>
45
+ </TableRow>
46
+ <TableRow>
47
+ <TableCell>billing</TableCell>
48
+ <TableCell>degraded</TableCell>
49
+ </TableRow>
50
+ </TableBody>
51
+ </Table>
52
+ ),
53
+ };
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Table, TableBody, TableCell, TableRow } from "./table";
4
+ describe("Table", () => {
5
+ it("renders cells", () => {
6
+ render(
7
+ <Table>
8
+ <TableBody>
9
+ <TableRow>
10
+ <TableCell>Alpha</TableCell>
11
+ </TableRow>
12
+ </TableBody>
13
+ </Table>,
14
+ );
15
+ expect(screen.getByText("Alpha")).toBeInTheDocument();
16
+ });
17
+ });
@@ -0,0 +1,106 @@
1
+ import {
2
+ forwardRef,
3
+ type HTMLAttributes,
4
+ type TdHTMLAttributes,
5
+ type ThHTMLAttributes,
6
+ } from "react";
7
+ import { cn } from "../../lib/cn";
8
+
9
+ export const Table = forwardRef<HTMLTableElement, HTMLAttributes<HTMLTableElement>>(function Table(
10
+ { className, ...props },
11
+ ref,
12
+ ) {
13
+ return (
14
+ <div className="relative w-full overflow-auto">
15
+ <table ref={ref} className={cn("w-full caption-bottom text-body", className)} {...props} />
16
+ </div>
17
+ );
18
+ });
19
+ export const TableHeader = forwardRef<
20
+ HTMLTableSectionElement,
21
+ HTMLAttributes<HTMLTableSectionElement>
22
+ >(function TableHeader({ className, ...props }, ref) {
23
+ // border-border-strong: the header-row bottom divider is the ONLY structural
24
+ // cue between the header and the first data row on a shared card/background
25
+ // surface (TableHeader itself carries no fill). Decision test: "If deleted,
26
+ // could a sighted user still tell header from body?" No → border-strong.
27
+ return (
28
+ <thead
29
+ ref={ref}
30
+ className={cn("[&_tr]:border-b [&_tr]:border-border-strong", className)}
31
+ {...props}
32
+ />
33
+ );
34
+ });
35
+ export const TableBody = forwardRef<
36
+ HTMLTableSectionElement,
37
+ HTMLAttributes<HTMLTableSectionElement>
38
+ >(function TableBody({ className, ...props }, ref) {
39
+ return <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
40
+ });
41
+ export const TableFooter = forwardRef<
42
+ HTMLTableSectionElement,
43
+ HTMLAttributes<HTMLTableSectionElement>
44
+ >(function TableFooter({ className, ...props }, ref) {
45
+ // border-t stays on plain `border`: the footer's bg-surface-muted/50 fill is
46
+ // a redundant cue — deleting the border still leaves a visible region change.
47
+ // Decision test: "If deleted, could a sighted user still tell footer from
48
+ // body?" Yes (fill differs) → redundant boundary → border.
49
+ return (
50
+ <tfoot
51
+ ref={ref}
52
+ className={cn("border-t bg-surface-muted/50 font-medium", className)}
53
+ {...props}
54
+ />
55
+ );
56
+ });
57
+ export const TableRow = forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTableRowElement>>(
58
+ function TableRow({ className, ...props }, ref) {
59
+ return (
60
+ <tr
61
+ ref={ref}
62
+ className={cn(
63
+ // border-border-strong: each row divider is the ONLY structural cue
64
+ // between adjacent rows on a shared card/background surface (no fill
65
+ // alternation on the plain Table primitive). Decision test: "If
66
+ // deleted, could a sighted user still tell the two rows apart?" No
67
+ // (no fill change) → load-bearing → border-strong.
68
+ "border-b border-border-strong transition-colors duration-fast ease-standard hover:bg-surface-muted/50 data-[state=selected]:bg-accent",
69
+ className,
70
+ )}
71
+ {...props}
72
+ />
73
+ );
74
+ },
75
+ );
76
+ export const TableHead = forwardRef<HTMLTableCellElement, ThHTMLAttributes<HTMLTableCellElement>>(
77
+ function TableHead({ className, ...props }, ref) {
78
+ return (
79
+ <th
80
+ ref={ref}
81
+ className={cn(
82
+ "h-10 px-3 text-start align-middle font-medium text-muted-foreground",
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
+ );
88
+ },
89
+ );
90
+ export const TableCell = forwardRef<HTMLTableCellElement, TdHTMLAttributes<HTMLTableCellElement>>(
91
+ function TableCell({ className, ...props }, ref) {
92
+ return <td ref={ref} className={cn("px-3 py-2 align-middle", className)} {...props} />;
93
+ },
94
+ );
95
+ export const TableCaption = forwardRef<
96
+ HTMLTableCaptionElement,
97
+ HTMLAttributes<HTMLTableCaptionElement>
98
+ >(function TableCaption({ className, ...props }, ref) {
99
+ return (
100
+ <caption
101
+ ref={ref}
102
+ className={cn("mt-4 text-body text-muted-foreground", className)}
103
+ {...props}
104
+ />
105
+ );
106
+ });
@@ -0,0 +1 @@
1
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
@@ -0,0 +1,267 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { expect, waitFor } from "storybook/test";
4
+ import { Button } from "../button";
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
6
+
7
+ const meta = {
8
+ title: "Core/Tabs",
9
+ component: Tabs,
10
+ tags: ["autodocs"],
11
+ argTypes: {
12
+ defaultValue: {
13
+ description: "Uncontrolled: the tab value active on first render.",
14
+ control: "text",
15
+ table: { category: "State" },
16
+ },
17
+ value: {
18
+ description: "Controlled active tab value.",
19
+ control: "text",
20
+ table: { category: "State" },
21
+ },
22
+ onValueChange: {
23
+ description: "Callback fired when the active tab changes.",
24
+ control: false,
25
+ table: { category: "Behavior" },
26
+ },
27
+ orientation: {
28
+ description: "Layout orientation of the tab list.",
29
+ control: { type: "radio" },
30
+ options: ["horizontal", "vertical"],
31
+ table: { category: "Appearance" },
32
+ },
33
+ activationMode: {
34
+ description: "`automatic` activates on focus; `manual` requires an explicit click/Enter.",
35
+ control: { type: "radio" },
36
+ options: ["automatic", "manual"],
37
+ table: { category: "Behavior" },
38
+ },
39
+ className: {
40
+ description: "Extra Tailwind classes merged via cn().",
41
+ control: "text",
42
+ table: { category: "Appearance" },
43
+ },
44
+ },
45
+ } satisfies Meta<typeof Tabs>;
46
+ export default meta;
47
+ type Story = StoryObj<typeof meta>;
48
+
49
+ export const Default: Story = {
50
+ render: () => (
51
+ <Tabs defaultValue="overview" className="w-80">
52
+ <TabsList>
53
+ <TabsTrigger value="overview">Overview</TabsTrigger>
54
+ <TabsTrigger value="activity">Activity</TabsTrigger>
55
+ <TabsTrigger value="settings">Settings</TabsTrigger>
56
+ </TabsList>
57
+ <TabsContent value="overview" className="text-body text-muted-foreground">
58
+ Overview panel.
59
+ </TabsContent>
60
+ <TabsContent value="activity" className="text-body text-muted-foreground">
61
+ Activity panel.
62
+ </TabsContent>
63
+ <TabsContent value="settings" className="text-body text-muted-foreground">
64
+ Settings panel.
65
+ </TabsContent>
66
+ </Tabs>
67
+ ),
68
+ // Selecting a tab reveals its panel and marks the trigger aria-selected — the
69
+ // panel content is unmounted until its tab is active, so this proves the
70
+ // switch actually happened.
71
+ play: async ({ canvas, userEvent }) => {
72
+ const activity = canvas.getByRole("tab", { name: "Activity" });
73
+ await userEvent.click(activity);
74
+ await expect(activity).toHaveAttribute("aria-selected", "true");
75
+ // The panel mounts on switch and fades in (gated entrance) — wait for it to settle.
76
+ const panel = await canvas.findByText("Activity panel.");
77
+ await waitFor(() => expect(panel).toBeVisible());
78
+ },
79
+ };
80
+
81
+ const OVERFLOW_TABS = Array.from({ length: 8 }, (_, i) => `tab-${i + 1}`);
82
+
83
+ /**
84
+ * An 8-tab strip inside a 320px-wide container (phone-width) — narrower than
85
+ * the strip's natural width, so it must scroll rather than clip. Confirms
86
+ * every tab is reachable by keyboard: repeated `ArrowRight` walks focus/
87
+ * selection all the way to the last tab, and the newly-active tab scrolls
88
+ * itself into the container's visible bounds (real layout — this assertion
89
+ * only means anything in a real browser, which is how `test-storybook` /
90
+ * `run-story-tests` execute it).
91
+ */
92
+ export const OverflowScrollable: Story = {
93
+ render: () => (
94
+ <div className="w-[320px]">
95
+ <Tabs defaultValue="tab-1">
96
+ <TabsList>
97
+ {OVERFLOW_TABS.map((value, i) => (
98
+ <TabsTrigger key={value} value={value}>
99
+ Tab {i + 1}
100
+ </TabsTrigger>
101
+ ))}
102
+ </TabsList>
103
+ {OVERFLOW_TABS.map((value, i) => (
104
+ <TabsContent key={value} value={value} className="text-body text-muted-foreground">
105
+ Panel {i + 1}.
106
+ </TabsContent>
107
+ ))}
108
+ </Tabs>
109
+ </div>
110
+ ),
111
+ play: async ({ canvas, userEvent }) => {
112
+ const tablist = canvas.getByRole("tablist");
113
+ // The strip is wider than its 320px container — it must scroll, not clip.
114
+ await expect(tablist.scrollWidth).toBeGreaterThan(tablist.clientWidth);
115
+
116
+ const first = canvas.getByRole("tab", { name: "Tab 1" });
117
+ // `justify-center-safe` (not `justify-center`) — the initially-active
118
+ // first tab must be fully within the visible scroll area on mount, not
119
+ // stranded off the left edge by a naive center-alignment of the
120
+ // overflowing strip.
121
+ await waitFor(() => {
122
+ const firstRect = first.getBoundingClientRect();
123
+ const containerRect = tablist.getBoundingClientRect();
124
+ expect(firstRect.left).toBeGreaterThanOrEqual(containerRect.left - 1);
125
+ expect(firstRect.right).toBeLessThanOrEqual(containerRect.right + 1);
126
+ });
127
+
128
+ first.focus();
129
+ await expect(first).toHaveFocus();
130
+
131
+ for (let i = 0; i < OVERFLOW_TABS.length - 1; i++) {
132
+ await userEvent.keyboard("{ArrowRight}");
133
+ }
134
+
135
+ const last = canvas.getByRole("tab", { name: "Tab 8" });
136
+ await waitFor(() => expect(last).toHaveFocus());
137
+ await expect(last).toHaveAttribute("aria-selected", "true");
138
+
139
+ // Real-layout assertion: the newly-active tab scrolled itself into the
140
+ // scroll container's visible bounds (a ±1px tolerance for rounding).
141
+ await waitFor(
142
+ () => {
143
+ const triggerRect = last.getBoundingClientRect();
144
+ const containerRect = tablist.getBoundingClientRect();
145
+ expect(triggerRect.left).toBeGreaterThanOrEqual(containerRect.left - 1);
146
+ expect(triggerRect.right).toBeLessThanOrEqual(containerRect.right + 1);
147
+ },
148
+ { timeout: 3000 },
149
+ );
150
+ },
151
+ };
152
+
153
+ /**
154
+ * Regression guard: a strip that FITS its container renders exactly as
155
+ * before — `justify-center-safe` behaves identically to `justify-center`
156
+ * when there's nothing to overflow, so no scrollbar appears — AND mounting it
157
+ * moves NOTHING.
158
+ *
159
+ * The tabs sit below a tall spacer inside a scroll region, out of view. The
160
+ * first cut of #344 called `Element.scrollIntoView()` on mount, which walks
161
+ * every scrollable ancestor: it scrolled this region (and the page) to reveal
162
+ * the tabs nobody had asked for. Asserting "no scrollbar" alone did not catch
163
+ * that — asserting the scroll POSITION does.
164
+ */
165
+ export const WideViewportFits: Story = {
166
+ render: () => (
167
+ <div data-testid="scroll-region" className="h-[220px] w-[640px] overflow-y-auto">
168
+ <p className="h-[600px] text-body text-muted-foreground">
169
+ This scroll region starts at the top, and must stay there. The tabs below are deliberately
170
+ out of view on mount.
171
+ </p>
172
+ <Tabs defaultValue="overview">
173
+ <TabsList>
174
+ <TabsTrigger value="overview">Overview</TabsTrigger>
175
+ <TabsTrigger value="activity">Activity</TabsTrigger>
176
+ <TabsTrigger value="settings">Settings</TabsTrigger>
177
+ </TabsList>
178
+ <TabsContent value="overview" className="text-body text-muted-foreground">
179
+ Overview panel.
180
+ </TabsContent>
181
+ <TabsContent value="activity" className="text-body text-muted-foreground">
182
+ Activity panel.
183
+ </TabsContent>
184
+ <TabsContent value="settings" className="text-body text-muted-foreground">
185
+ Settings panel.
186
+ </TabsContent>
187
+ </Tabs>
188
+ </div>
189
+ ),
190
+ play: async ({ canvas }) => {
191
+ const tablist = canvas.getByRole("tablist");
192
+ await waitFor(() => expect(tablist.scrollWidth).toBeLessThanOrEqual(tablist.clientWidth + 1));
193
+
194
+ // A smooth scroll is asynchronous — wait long enough for a stray one to
195
+ // land before asserting it never happened.
196
+ const region = canvas.getByTestId("scroll-region");
197
+ await new Promise((resolve) => setTimeout(resolve, 400));
198
+ await expect(region.scrollTop).toBe(0);
199
+ await expect(window.scrollY).toBe(0);
200
+ },
201
+ };
202
+
203
+ /**
204
+ * The scroll-on-activation behaviour proved WITHOUT the browser's own
205
+ * focus-scrolling doing the work: a button beside the strip sets the value
206
+ * programmatically, so the newly-active tab is never focused and nothing but
207
+ * `TabsTrigger`'s own effect can bring it into view.
208
+ *
209
+ * The strip starts at scroll 0 (mount is not an activation), then moves — and
210
+ * only the strip moves.
211
+ */
212
+ export const ProgrammaticActivation: Story = {
213
+ render: function ProgrammaticActivationStory() {
214
+ const [value, setValue] = useState("tab-1");
215
+ return (
216
+ <div className="w-[320px]">
217
+ <Button
218
+ type="button"
219
+ size="sm"
220
+ variant="secondary"
221
+ className="mb-2"
222
+ onClick={() => setValue("tab-8")}
223
+ >
224
+ Jump to Tab 8
225
+ </Button>
226
+ <Tabs value={value} onValueChange={setValue}>
227
+ <TabsList>
228
+ {OVERFLOW_TABS.map((tab, i) => (
229
+ <TabsTrigger key={tab} value={tab}>
230
+ Tab {i + 1}
231
+ </TabsTrigger>
232
+ ))}
233
+ </TabsList>
234
+ {OVERFLOW_TABS.map((tab, i) => (
235
+ <TabsContent key={tab} value={tab} className="text-body text-muted-foreground">
236
+ Panel {i + 1}.
237
+ </TabsContent>
238
+ ))}
239
+ </Tabs>
240
+ </div>
241
+ );
242
+ },
243
+ play: async ({ canvas, userEvent }) => {
244
+ const tablist = canvas.getByRole("tablist");
245
+ // Mount is a state, not an activation: the strip never scrolled itself.
246
+ await new Promise((resolve) => setTimeout(resolve, 400));
247
+ await expect(tablist.scrollLeft).toBe(0);
248
+
249
+ await userEvent.click(canvas.getByRole("button", { name: "Jump to Tab 8" }));
250
+
251
+ const last = canvas.getByRole("tab", { name: "Tab 8" });
252
+ await expect(last).toHaveAttribute("aria-selected", "true");
253
+ // The activated tab scrolled itself into the STRIP's visible bounds…
254
+ await waitFor(
255
+ () => {
256
+ const triggerRect = last.getBoundingClientRect();
257
+ const stripRect = tablist.getBoundingClientRect();
258
+ expect(triggerRect.left).toBeGreaterThanOrEqual(stripRect.left - 1);
259
+ expect(triggerRect.right).toBeLessThanOrEqual(stripRect.right + 1);
260
+ },
261
+ { timeout: 3000 },
262
+ );
263
+ await expect(tablist.scrollLeft).toBeGreaterThan(0);
264
+ // …and only the strip: the page itself never moved.
265
+ await expect(window.scrollY).toBe(0);
266
+ },
267
+ };
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
5
+
6
+ describe("Tabs", () => {
7
+ // jsdom implements neither `Element.prototype.scrollIntoView` nor a real
8
+ // layout, so a component that calls a scroll API unconditionally CRASHES
9
+ // every jsdom consumer of @elabs-ai/components-ui (it did: 8/8
10
+ // of @elabs-ai/components-editor's CodeWorkspace tests died on
11
+ // `node.scrollIntoView is not a function`). There is deliberately no stub in
12
+ // `vitest.setup.ts` — this renders on bare jsdom, which is the assertion.
13
+ it("mounts under jsdom without calling any scroll API", async () => {
14
+ const scrollTo = vi.fn();
15
+ const originalScrollTo = Element.prototype.scrollTo;
16
+ Element.prototype.scrollTo = scrollTo;
17
+ try {
18
+ // No stub in vitest.setup.ts — the guard has to live in the component.
19
+ expect(Element.prototype.scrollIntoView).toBeUndefined();
20
+ render(
21
+ <Tabs defaultValue="a">
22
+ <TabsList>
23
+ <TabsTrigger value="a">A</TabsTrigger>
24
+ <TabsTrigger value="b">B</TabsTrigger>
25
+ </TabsList>
26
+ <TabsContent value="a">Panel A</TabsContent>
27
+ </Tabs>,
28
+ );
29
+ // Mount is a state, not an activation transition: nothing scrolls. (The
30
+ // observers are asynchronous — let a macrotask pass before asserting.)
31
+ await new Promise((resolve) => setTimeout(resolve, 0));
32
+ expect(scrollTo).not.toHaveBeenCalled();
33
+ } finally {
34
+ Element.prototype.scrollTo = originalScrollTo;
35
+ }
36
+ });
37
+
38
+ // The activation path itself is layout-dependent (jsdom reports every
39
+ // scrollWidth/clientWidth as 0, so the overflow gate always short-circuits);
40
+ // it is measured for real in the `ProgrammaticActivation` /
41
+ // `OverflowScrollable` Storybook interaction tests.
42
+ it("does not throw when a tab is activated under jsdom", async () => {
43
+ const user = userEvent.setup();
44
+ render(
45
+ <Tabs defaultValue="a">
46
+ <TabsList>
47
+ <TabsTrigger value="a">A</TabsTrigger>
48
+ <TabsTrigger value="b">B</TabsTrigger>
49
+ </TabsList>
50
+ <TabsContent value="a">Panel A</TabsContent>
51
+ <TabsContent value="b">Panel B</TabsContent>
52
+ </Tabs>,
53
+ );
54
+ await user.click(screen.getByRole("tab", { name: "B" }));
55
+ expect(await screen.findByText("Panel B")).toBeInTheDocument();
56
+ });
57
+
58
+ it("switches the active panel", () => {
59
+ render(
60
+ <Tabs defaultValue="a">
61
+ <TabsList>
62
+ <TabsTrigger value="a">A</TabsTrigger>
63
+ <TabsTrigger value="b">B</TabsTrigger>
64
+ </TabsList>
65
+ <TabsContent value="a">Panel A</TabsContent>
66
+ <TabsContent value="b">Panel B</TabsContent>
67
+ </Tabs>,
68
+ );
69
+ expect(screen.getByText("Panel A")).toBeInTheDocument();
70
+ });
71
+
72
+ // Cheap class-level regression guard for #344: TabsList must be a scroll
73
+ // container (`overflow-x-auto`, bounded by `max-w-full`) with the
74
+ // overflow-safe centering utility, not the plain `justify-center` that
75
+ // strands the first tab off-screen once the strip overflows. The real
76
+ // scroll-into-view/320px-container behavior is layout-dependent and is
77
+ // covered by the `OverflowScrollable` Storybook interaction test instead
78
+ // (jsdom doesn't lay out real scroll geometry).
79
+ it("renders TabsList as a bounded scroll container with safe centering", () => {
80
+ render(
81
+ <Tabs defaultValue="a">
82
+ <TabsList data-testid="list">
83
+ <TabsTrigger value="a">A</TabsTrigger>
84
+ </TabsList>
85
+ <TabsContent value="a">Panel A</TabsContent>
86
+ </Tabs>,
87
+ );
88
+ const list = screen.getByTestId("list");
89
+ expect(list.className).toContain("overflow-x-auto");
90
+ expect(list.className).toContain("max-w-full");
91
+ expect(list.className).toContain("justify-center-safe");
92
+ expect(list.className).not.toMatch(/(?<!-)justify-center(?!-safe)/);
93
+ });
94
+ });