@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,361 @@
1
+ // a2ui.exposed: yes
2
+ "use client";
3
+
4
+ import {
5
+ forwardRef,
6
+ useCallback,
7
+ useEffect,
8
+ useId,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ type ButtonHTMLAttributes,
13
+ type KeyboardEvent,
14
+ } from "react";
15
+ import { Check, ChevronsUpDown } from "lucide-react";
16
+ import { useVirtualizer } from "@tanstack/react-virtual";
17
+ import { cn } from "../../lib/cn";
18
+ import { Badge } from "../badge";
19
+ import { Button } from "../button";
20
+ import { Input } from "../input";
21
+ import { Popover, PopoverContent, PopoverTrigger } from "../popover";
22
+ import { type VirtualSelectOption, useVirtualListbox } from "./use-virtual-listbox";
23
+
24
+ export type { VirtualSelectOption };
25
+
26
+ export interface VirtualSelectProps extends Omit<
27
+ ButtonHTMLAttributes<HTMLButtonElement>,
28
+ "value" | "onChange" | "defaultValue"
29
+ > {
30
+ /** The full option list. Only visible items are rendered in the DOM. */
31
+ options: VirtualSelectOption[];
32
+ /** Allow selecting multiple values. Default `false`. */
33
+ multiple?: boolean;
34
+ /** Controlled value — `string` in single mode, `string[]` in multiple mode. */
35
+ value?: string | string[];
36
+ /** Uncontrolled initial value. */
37
+ defaultValue?: string | string[];
38
+ /** Called when the selection changes. Emits `string` (single) or `string[]` (multiple). */
39
+ onValueChange?: (value: string | string[]) => void;
40
+ /** Trigger placeholder when nothing is selected. Default "Select…" */
41
+ placeholder?: string;
42
+ /** Search-input placeholder. Default "Search…" */
43
+ searchPlaceholder?: string;
44
+ /** Empty-state message when no options match the query. Default "No results." */
45
+ emptyText?: string;
46
+ /**
47
+ * Estimated row height used by the virtualizer (px).
48
+ * Increase for taller rows (e.g. with descriptions). Default 32.
49
+ */
50
+ estimateOptionHeight?: number;
51
+ /**
52
+ * CSS max-height for the scrollable listbox. Default "18rem".
53
+ */
54
+ maxListHeight?: string;
55
+ className?: string;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Helpers
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function toArray(v: string | string[] | undefined): string[] {
63
+ if (v === undefined) return [];
64
+ if (Array.isArray(v)) return v;
65
+ return v === "" ? [] : [v];
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Component
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /**
73
+ * VirtualSelect — a combobox that renders only the visible window of options
74
+ * in the DOM, making it suitable for option lists of 10,000+ items.
75
+ *
76
+ * Uses `@tanstack/react-virtual` for windowed rendering and a purpose-built
77
+ * `aria-activedescendant` keyboard model so focus stays on the search input
78
+ * while navigating the listbox.
79
+ */
80
+ export const VirtualSelect = forwardRef<HTMLButtonElement, VirtualSelectProps>(
81
+ function VirtualSelect(
82
+ {
83
+ options,
84
+ multiple = false,
85
+ value,
86
+ defaultValue,
87
+ onValueChange,
88
+ placeholder = "Select…",
89
+ searchPlaceholder = "Search…",
90
+ emptyText = "No results.",
91
+ estimateOptionHeight = 32,
92
+ maxListHeight = "18rem",
93
+ className,
94
+ ...props
95
+ },
96
+ ref,
97
+ ) {
98
+ // ------------------------------------------------------------------
99
+ // Controlled / uncontrolled
100
+ // ------------------------------------------------------------------
101
+ const isControlled = value !== undefined;
102
+ const [internal, setInternal] = useState<string[]>(() => toArray(defaultValue));
103
+ const selected: string[] = isControlled ? toArray(value) : internal;
104
+
105
+ const emit = useCallback(
106
+ (next: string[]) => {
107
+ if (!isControlled) setInternal(next);
108
+ if (multiple) {
109
+ onValueChange?.(next);
110
+ } else {
111
+ onValueChange?.(next[0] ?? "");
112
+ }
113
+ },
114
+ [isControlled, multiple, onValueChange],
115
+ );
116
+
117
+ // ------------------------------------------------------------------
118
+ // Popover state
119
+ // ------------------------------------------------------------------
120
+ const [open, setOpen] = useState(false);
121
+
122
+ // ------------------------------------------------------------------
123
+ // Search / filter
124
+ // ------------------------------------------------------------------
125
+ const [query, setQuery] = useState("");
126
+ const searchRef = useRef<HTMLInputElement>(null);
127
+
128
+ // Reset query when popover toggles
129
+ useEffect(() => {
130
+ setQuery("");
131
+ }, [open]);
132
+
133
+ const filtered = useMemo(
134
+ () => options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase())),
135
+ [options, query],
136
+ );
137
+
138
+ // ------------------------------------------------------------------
139
+ // Virtualizer
140
+ // ------------------------------------------------------------------
141
+ // State-backed scroll element (NOT a ref): the listbox mounts only when the
142
+ // Popover opens, so a plain ref would leave the virtualizer with a null
143
+ // scroll element and render zero options. A state callback ref forces the
144
+ // re-render that lets the virtualizer measure once the element appears.
145
+ const [scrollEl, setScrollEl] = useState<HTMLDivElement | null>(null);
146
+ const virtualizer = useVirtualizer({
147
+ count: filtered.length,
148
+ getScrollElement: () => scrollEl,
149
+ estimateSize: () => estimateOptionHeight,
150
+ overscan: 5,
151
+ });
152
+
153
+ // ------------------------------------------------------------------
154
+ // Keyboard navigation
155
+ // ------------------------------------------------------------------
156
+ const handleSelect = useCallback(
157
+ (optValue: string) => {
158
+ if (multiple) {
159
+ const next = selected.includes(optValue)
160
+ ? selected.filter((v) => v !== optValue)
161
+ : [...selected, optValue];
162
+ emit(next);
163
+ // Keep open in multi mode
164
+ } else {
165
+ emit(selected[0] === optValue ? [] : [optValue]);
166
+ setOpen(false);
167
+ }
168
+ },
169
+ [multiple, selected, emit],
170
+ );
171
+
172
+ const { activeIndex, setActiveIndex, onInputKeyDown } = useVirtualListbox({
173
+ count: filtered.length,
174
+ filtered,
175
+ isOpen: open,
176
+ onSelect: handleSelect,
177
+ virtualizer,
178
+ });
179
+
180
+ // ------------------------------------------------------------------
181
+ // IDs for ARIA wiring
182
+ // ------------------------------------------------------------------
183
+ const uid = useId();
184
+ const listboxId = `${uid}-listbox`;
185
+ const optionId = (index: number) => `${uid}-option-${index}`;
186
+
187
+ // Focus the search input when the popover opens
188
+ const handleOpenChange = useCallback((next: boolean) => {
189
+ setOpen(next);
190
+ if (next) {
191
+ // Defer one frame so Radix has mounted the portal
192
+ setTimeout(() => searchRef.current?.focus(), 0);
193
+ }
194
+ }, []);
195
+
196
+ // ------------------------------------------------------------------
197
+ // Trigger label
198
+ // ------------------------------------------------------------------
199
+ const selectedLabels = selected.map((v) => {
200
+ const opt = options.find((o) => o.value === v);
201
+ return { value: v, label: opt?.label ?? v };
202
+ });
203
+
204
+ const triggerContent =
205
+ selectedLabels.length === 0 ? (
206
+ <span className="text-muted-foreground">{placeholder}</span>
207
+ ) : multiple ? (
208
+ <>
209
+ {selectedLabels.slice(0, 2).map((l) => (
210
+ <Badge
211
+ key={l.value}
212
+ variant="secondary"
213
+ className="max-w-[8rem] truncate border-border"
214
+ >
215
+ {l.label}
216
+ </Badge>
217
+ ))}
218
+ {selectedLabels.length > 2 ? (
219
+ <Badge
220
+ variant="secondary"
221
+ aria-label={`and ${selectedLabels.length - 2} more selected`}
222
+ >
223
+ +{selectedLabels.length - 2}
224
+ </Badge>
225
+ ) : null}
226
+ </>
227
+ ) : (
228
+ <span className="truncate">{selectedLabels[0]?.label}</span>
229
+ );
230
+
231
+ // ------------------------------------------------------------------
232
+ // Render
233
+ // ------------------------------------------------------------------
234
+ const virtualItems = virtualizer.getVirtualItems();
235
+ // Only reference the active option from aria-activedescendant when it is
236
+ // actually mounted in the virtual window (else the id would dangle).
237
+ const activeMounted = virtualItems.some((v) => v.index === activeIndex);
238
+
239
+ return (
240
+ <Popover open={open} onOpenChange={handleOpenChange}>
241
+ <PopoverTrigger asChild>
242
+ <Button
243
+ ref={ref}
244
+ type="button"
245
+ variant="outline"
246
+ role="combobox"
247
+ aria-expanded={open}
248
+ aria-haspopup="listbox"
249
+ aria-controls={listboxId}
250
+ className={cn("h-auto min-h-9 w-64 justify-between font-normal", className)}
251
+ {...props}
252
+ >
253
+ <span className="flex min-w-0 flex-1 flex-wrap items-center gap-1 text-start">
254
+ {triggerContent}
255
+ </span>
256
+ <ChevronsUpDown className="ms-2 size-4 shrink-0 opacity-50" aria-hidden="true" />
257
+ </Button>
258
+ </PopoverTrigger>
259
+
260
+ <PopoverContent
261
+ className="w-64 p-0"
262
+ align="start"
263
+ // Prevent Radix from stealing focus so it can go to the input
264
+ onOpenAutoFocus={(e) => e.preventDefault()}
265
+ >
266
+ {/* Search input */}
267
+ <div className="border-b p-1.5">
268
+ <Input
269
+ ref={searchRef}
270
+ type="text"
271
+ autoComplete="off"
272
+ spellCheck={false}
273
+ placeholder={searchPlaceholder}
274
+ value={query}
275
+ onChange={(e) => setQuery(e.target.value)}
276
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => onInputKeyDown(e)}
277
+ aria-label={searchPlaceholder}
278
+ aria-autocomplete="list"
279
+ aria-controls={listboxId}
280
+ aria-activedescendant={
281
+ activeIndex >= 0 && activeMounted ? optionId(activeIndex) : undefined
282
+ }
283
+ className="h-8 border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
284
+ />
285
+ </div>
286
+
287
+ {/* Listbox — always mounted so aria-controls never dangles (even when empty) */}
288
+ <div
289
+ ref={setScrollEl}
290
+ id={listboxId}
291
+ role="listbox"
292
+ aria-multiselectable={multiple || undefined}
293
+ style={{ maxHeight: maxListHeight }}
294
+ className="overflow-auto p-1"
295
+ >
296
+ {filtered.length > 0 ? (
297
+ // Spacer gives the virtualizer its full scroll height
298
+ <div style={{ height: virtualizer.getTotalSize() }} className="relative">
299
+ {virtualItems.map((virtualRow) => {
300
+ const opt = filtered[virtualRow.index];
301
+ if (!opt) return null;
302
+ const isSelected = selected.includes(opt.value);
303
+ const isActive = activeIndex === virtualRow.index;
304
+
305
+ return (
306
+ <div
307
+ key={virtualRow.key}
308
+ id={optionId(virtualRow.index)}
309
+ role="option"
310
+ aria-selected={isSelected}
311
+ aria-disabled={opt.disabled || undefined}
312
+ style={{
313
+ position: "absolute",
314
+ top: 0,
315
+ left: 0,
316
+ width: "100%",
317
+ height: `${virtualRow.size}px`,
318
+ transform: `translateY(${virtualRow.start}px)`,
319
+ }}
320
+ className={cn(
321
+ "flex cursor-pointer select-none items-center rounded-sm px-2 text-sm outline-none",
322
+ "transition-colors duration-fast motion-reduce:transition-none",
323
+ isActive && "bg-accent text-accent-foreground",
324
+ !isActive && "hover:bg-accent hover:text-accent-foreground",
325
+ opt.disabled && "pointer-events-none cursor-not-allowed opacity-50",
326
+ )}
327
+ onClick={() => {
328
+ if (!opt.disabled) handleSelect(opt.value);
329
+ }}
330
+ onMouseEnter={() => {
331
+ if (!opt.disabled) setActiveIndex(virtualRow.index);
332
+ }}
333
+ >
334
+ <Check
335
+ className={cn(
336
+ "me-2 size-4 shrink-0",
337
+ isSelected ? "opacity-100" : "opacity-0",
338
+ )}
339
+ aria-hidden="true"
340
+ />
341
+ <span className="truncate">{opt.label}</span>
342
+ </div>
343
+ );
344
+ })}
345
+ </div>
346
+ ) : null}
347
+ </div>
348
+ {filtered.length === 0 ? (
349
+ <div
350
+ role="status"
351
+ aria-live="polite"
352
+ className="px-2 py-6 text-center text-sm text-muted-foreground"
353
+ >
354
+ {emptyText}
355
+ </div>
356
+ ) : null}
357
+ </PopoverContent>
358
+ </Popover>
359
+ );
360
+ },
361
+ );
@@ -0,0 +1,12 @@
1
+ export {
2
+ Wizard,
3
+ WizardSteps,
4
+ WizardStep,
5
+ WizardNav,
6
+ type WizardProps,
7
+ type WizardStepsProps,
8
+ type WizardStepProps,
9
+ type WizardNavProps,
10
+ type WizardStepMeta,
11
+ type WizardValidator,
12
+ } from "./wizard";
@@ -0,0 +1,148 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import { useState } from "react";
4
+ import { Wizard, WizardSteps, WizardStep, WizardNav, type WizardStepMeta } from "./wizard";
5
+
6
+ const meta = {
7
+ title: "Navigation/Wizard",
8
+ component: Wizard,
9
+ tags: ["autodocs"],
10
+ argTypes: {
11
+ steps: {
12
+ description: "Ordered step metadata (id, title, description) shown in the step indicator.",
13
+ control: false,
14
+ table: { category: "Data" },
15
+ },
16
+ step: {
17
+ description: "Controlled active step index (0-based).",
18
+ control: "number",
19
+ table: { category: "State" },
20
+ },
21
+ defaultStep: {
22
+ description: "Uncontrolled initial step index. Default 0.",
23
+ control: "number",
24
+ table: { category: "State" },
25
+ },
26
+ onStepChange: {
27
+ description: "Callback fired when the active step changes.",
28
+ control: false,
29
+ table: { category: "Events" },
30
+ },
31
+ orientation: {
32
+ description: "Layout direction for the step indicator and content.",
33
+ control: { type: "radio" },
34
+ options: ["horizontal", "vertical"],
35
+ table: { category: "Layout" },
36
+ },
37
+ className: {
38
+ description: "Additional CSS classes applied to the wizard root.",
39
+ control: "text",
40
+ table: { category: "Styling" },
41
+ },
42
+ children: {
43
+ description: "WizardSteps, WizardStep panels, and WizardNav composed inside.",
44
+ control: false,
45
+ table: { category: "Content" },
46
+ },
47
+ },
48
+ } satisfies Meta<typeof Wizard>;
49
+
50
+ export default meta;
51
+ type Story = StoryObj<typeof meta>;
52
+
53
+ const steps: WizardStepMeta[] = [
54
+ { id: "account", title: "Account", description: "Login details" },
55
+ { id: "profile", title: "Profile", description: "About you" },
56
+ { id: "review", title: "Review", description: "Confirm" },
57
+ ];
58
+
59
+ const Panel = ({ children }: { children: React.ReactNode }) => (
60
+ <div className="rounded-md border border-border bg-card p-4 text-card-foreground">{children}</div>
61
+ );
62
+
63
+ export const Default: Story = {
64
+ render: () => (
65
+ <Wizard steps={steps} className="max-w-xl">
66
+ <WizardSteps />
67
+ <WizardStep step={0}>
68
+ <Panel>Step 1 — account details.</Panel>
69
+ </WizardStep>
70
+ <WizardStep step={1}>
71
+ <Panel>Step 2 — profile details.</Panel>
72
+ </WizardStep>
73
+ <WizardStep step={2}>
74
+ <Panel>Step 3 — review and confirm.</Panel>
75
+ </WizardStep>
76
+ <WizardNav onFinish={() => alert("Finished!")} />
77
+ </Wizard>
78
+ ),
79
+ // Advances through all three steps via Next, then asserts the Finish button
80
+ // appears on the last step and Back brings us back one step.
81
+ play: async ({ canvasElement, userEvent }) => {
82
+ const canvas = within(canvasElement);
83
+ // Step 1 is visible
84
+ await waitFor(() => expect(canvas.getByText("Step 1 — account details.")).toBeVisible());
85
+ // Advance to step 2
86
+ await userEvent.click(canvas.getByRole("button", { name: /next/i }));
87
+ await waitFor(() => expect(canvas.getByText("Step 2 — profile details.")).toBeVisible());
88
+ // Advance to step 3
89
+ await userEvent.click(canvas.getByRole("button", { name: /next/i }));
90
+ await waitFor(() => expect(canvas.getByText("Step 3 — review and confirm.")).toBeVisible());
91
+ // On the last step, "Finish" replaces "Next"
92
+ await waitFor(() => expect(canvas.getByRole("button", { name: /finish/i })).toBeVisible());
93
+ // Back returns to step 2
94
+ await userEvent.click(canvas.getByRole("button", { name: /back/i }));
95
+ await waitFor(() => expect(canvas.getByText("Step 2 — profile details.")).toBeVisible());
96
+ },
97
+ };
98
+
99
+ export const Vertical: Story = {
100
+ render: () => (
101
+ <Wizard steps={steps} orientation="vertical" className="max-w-2xl">
102
+ <WizardSteps />
103
+ <div className="flex flex-1 flex-col gap-4">
104
+ <WizardStep step={0}>
105
+ <Panel>Account details.</Panel>
106
+ </WizardStep>
107
+ <WizardStep step={1}>
108
+ <Panel>Profile details.</Panel>
109
+ </WizardStep>
110
+ <WizardStep step={2}>
111
+ <Panel>Review.</Panel>
112
+ </WizardStep>
113
+ <WizardNav />
114
+ </div>
115
+ </Wizard>
116
+ ),
117
+ };
118
+
119
+ export const WithValidationGating: Story = {
120
+ render: () => {
121
+ // eslint-disable-next-line react-hooks/rules-of-hooks
122
+ const [agreed, setAgreed] = useState(false);
123
+ return (
124
+ <Wizard steps={steps} className="max-w-xl">
125
+ <WizardSteps />
126
+ <WizardStep step={0} validate={() => agreed}>
127
+ <Panel>
128
+ <label className="flex items-center gap-2">
129
+ <input
130
+ type="checkbox"
131
+ checked={agreed}
132
+ onChange={(e) => setAgreed(e.target.checked)}
133
+ />
134
+ I agree to continue (Next is blocked until checked).
135
+ </label>
136
+ </Panel>
137
+ </WizardStep>
138
+ <WizardStep step={1}>
139
+ <Panel>Profile details.</Panel>
140
+ </WizardStep>
141
+ <WizardStep step={2}>
142
+ <Panel>Review.</Panel>
143
+ </WizardStep>
144
+ <WizardNav />
145
+ </Wizard>
146
+ );
147
+ },
148
+ };
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Wizard, WizardSteps, WizardStep, WizardNav, type WizardStepMeta } from "./wizard";
5
+
6
+ const steps: WizardStepMeta[] = [
7
+ { id: "a", title: "Account" },
8
+ { id: "b", title: "Profile" },
9
+ { id: "c", title: "Review" },
10
+ ];
11
+
12
+ function renderWizard(extra?: { validate?: () => boolean | Promise<boolean> }) {
13
+ return render(
14
+ <Wizard steps={steps}>
15
+ <WizardSteps />
16
+ <WizardStep step={0} validate={extra?.validate}>
17
+ <p>Account content</p>
18
+ </WizardStep>
19
+ <WizardStep step={1}>
20
+ <p>Profile content</p>
21
+ </WizardStep>
22
+ <WizardStep step={2}>
23
+ <p>Review content</p>
24
+ </WizardStep>
25
+ <WizardNav />
26
+ </Wizard>,
27
+ );
28
+ }
29
+
30
+ describe("Wizard", () => {
31
+ it("shows only the active step's content and marks the indicator", () => {
32
+ renderWizard();
33
+ expect(screen.getByText("Account content")).toBeInTheDocument();
34
+ expect(screen.queryByText("Profile content")).not.toBeInTheDocument();
35
+ expect(screen.getByRole("listitem", { current: "step" })).toHaveTextContent("Account");
36
+ });
37
+
38
+ it("advances and goes back", async () => {
39
+ const user = userEvent.setup();
40
+ renderWizard();
41
+ await user.click(screen.getByRole("button", { name: "Next" }));
42
+ expect(screen.getByText("Profile content")).toBeInTheDocument();
43
+ await user.click(screen.getByRole("button", { name: "Back" }));
44
+ expect(screen.getByText("Account content")).toBeInTheDocument();
45
+ });
46
+
47
+ it("disables Back on the first step", () => {
48
+ renderWizard();
49
+ expect(screen.getByRole("button", { name: "Back" })).toBeDisabled();
50
+ });
51
+
52
+ it("blocks advance when validate returns false", async () => {
53
+ const user = userEvent.setup();
54
+ renderWizard({ validate: () => false });
55
+ await user.click(screen.getByRole("button", { name: "Next" }));
56
+ expect(screen.getByText("Account content")).toBeInTheDocument();
57
+ expect(screen.queryByText("Profile content")).not.toBeInTheDocument();
58
+ });
59
+
60
+ it("shows Finish on the last step", async () => {
61
+ const user = userEvent.setup();
62
+ renderWizard();
63
+ await user.click(screen.getByRole("button", { name: "Next" }));
64
+ await user.click(screen.getByRole("button", { name: "Next" }));
65
+ await waitFor(() => expect(screen.getByRole("button", { name: "Finish" })).toBeInTheDocument());
66
+ });
67
+ });