@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,219 @@
1
+ // a2ui.exposed: yes
2
+ import { forwardRef, useCallback, useRef, useState, type HTMLAttributes } from "react";
3
+ import { Minus, Plus } from "lucide-react";
4
+ import { cn } from "../../lib/cn";
5
+ import { InputGroup, InputGroupButton, InputGroupInput } from "../input-group";
6
+
7
+ export interface NumberInputProps extends Omit<
8
+ HTMLAttributes<HTMLDivElement>,
9
+ "onChange" | "defaultValue"
10
+ > {
11
+ /** Controlled value. Pass `null` to represent an empty state. */
12
+ value?: number | null;
13
+ /** Uncontrolled default value. */
14
+ defaultValue?: number | null;
15
+ /** Called when the numeric value changes. */
16
+ onValueChange?: (value: number | null) => void;
17
+ /** Minimum allowed value. */
18
+ min?: number;
19
+ /** Maximum allowed value. */
20
+ max?: number;
21
+ /** Step increment/decrement amount (default 1). */
22
+ step?: number;
23
+ /**
24
+ * When true (default), the value is clamped to [min, max] on blur.
25
+ * Clamping does NOT happen per-keystroke — only on blur.
26
+ */
27
+ clamp?: boolean;
28
+ /** Intl.NumberFormat options used to format the displayed value. */
29
+ formatOptions?: Intl.NumberFormatOptions;
30
+ /** Placeholder text for the underlying input. */
31
+ placeholder?: string;
32
+ /** Whether the input is disabled. */
33
+ disabled?: boolean;
34
+ /** Whether the input is read-only. */
35
+ readOnly?: boolean;
36
+ /** Name attribute forwarded to the underlying <input>. */
37
+ name?: string;
38
+ /** id attribute forwarded to the underlying <input>. */
39
+ id?: string;
40
+ /** aria-label forwarded to the underlying <input>. */
41
+ "aria-label"?: string;
42
+ /** aria-labelledby forwarded to the underlying <input>. */
43
+ "aria-labelledby"?: string;
44
+ /** aria-describedby forwarded to the underlying <input>. */
45
+ "aria-describedby"?: string;
46
+ /** aria-invalid forwarded to the underlying <input>. */
47
+ "aria-invalid"?: boolean | "true" | "false" | "grammar" | "spelling";
48
+ /** aria-required forwarded to the underlying <input>. */
49
+ "aria-required"?: boolean | "true" | "false";
50
+ }
51
+
52
+ function clampValue(val: number, min?: number, max?: number): number {
53
+ let result = val;
54
+ if (min !== undefined) result = Math.max(min, result);
55
+ if (max !== undefined) result = Math.min(max, result);
56
+ return result;
57
+ }
58
+
59
+ function formatNum(value: number, formatOptions?: Intl.NumberFormatOptions): string {
60
+ if (formatOptions) {
61
+ return new Intl.NumberFormat(undefined, formatOptions).format(value);
62
+ }
63
+ return String(value);
64
+ }
65
+
66
+ /**
67
+ * NumberInput — a numeric stepper that composes InputGroup + two step buttons.
68
+ *
69
+ * Controlled via `value`/`onValueChange`; uncontrolled via `defaultValue`.
70
+ * Keyboard ↑/↓ = ±step; Shift+↑/↓ = ±step×10. Clamps on blur (not per keystroke).
71
+ */
72
+ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function NumberInput(
73
+ {
74
+ value: valueProp,
75
+ defaultValue,
76
+ onValueChange,
77
+ min,
78
+ max,
79
+ step = 1,
80
+ clamp = true,
81
+ formatOptions,
82
+ placeholder,
83
+ disabled,
84
+ readOnly,
85
+ name,
86
+ id,
87
+ "aria-label": ariaLabel,
88
+ "aria-labelledby": ariaLabelledby,
89
+ "aria-describedby": ariaDescribedby,
90
+ "aria-invalid": ariaInvalid,
91
+ "aria-required": ariaRequired,
92
+ className,
93
+ ...props
94
+ },
95
+ ref,
96
+ ) {
97
+ const isControlled = valueProp !== undefined;
98
+
99
+ const [internalValue, setInternalValue] = useState<number | null>(
100
+ defaultValue !== undefined ? defaultValue : null,
101
+ );
102
+ const [inputText, setInputText] = useState<string>(() => {
103
+ const initial = isControlled ? valueProp : defaultValue;
104
+ return initial != null ? formatNum(initial, formatOptions) : "";
105
+ });
106
+ // Track whether the user is actively editing to avoid overwriting their input
107
+ const isEditing = useRef(false);
108
+
109
+ const currentValue = isControlled ? valueProp : internalValue;
110
+
111
+ const commit = useCallback(
112
+ (next: number | null) => {
113
+ if (!isControlled) setInternalValue(next);
114
+ onValueChange?.(next);
115
+ },
116
+ [isControlled, onValueChange],
117
+ );
118
+
119
+ const applyStep = useCallback(
120
+ (delta: number) => {
121
+ if (disabled || readOnly) return;
122
+ const base = currentValue ?? 0;
123
+ let next = base + delta;
124
+ if (clamp) next = clampValue(next, min, max);
125
+ setInputText(next != null ? formatNum(next, formatOptions) : "");
126
+ commit(next);
127
+ },
128
+ [disabled, readOnly, currentValue, clamp, min, max, formatOptions, commit],
129
+ );
130
+
131
+ const handleKeyDown = useCallback(
132
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
133
+ if (e.key === "ArrowUp" || e.key === "ArrowDown") {
134
+ e.preventDefault();
135
+ const multiplier = e.shiftKey ? 10 : 1;
136
+ const delta = e.key === "ArrowUp" ? step * multiplier : -step * multiplier;
137
+ applyStep(delta);
138
+ }
139
+ },
140
+ [applyStep, step],
141
+ );
142
+
143
+ const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
144
+ isEditing.current = true;
145
+ setInputText(e.target.value);
146
+ }, []);
147
+
148
+ const handleBlur = useCallback(() => {
149
+ isEditing.current = false;
150
+ const raw = inputText.trim();
151
+ if (raw === "") {
152
+ commit(null);
153
+ return;
154
+ }
155
+ const parsed = parseFloat(raw);
156
+ if (isNaN(parsed)) {
157
+ // Revert to last known good value
158
+ setInputText(currentValue != null ? formatNum(currentValue, formatOptions) : "");
159
+ return;
160
+ }
161
+ const clamped = clamp ? clampValue(parsed, min, max) : parsed;
162
+ setInputText(formatNum(clamped, formatOptions));
163
+ commit(clamped);
164
+ }, [inputText, currentValue, clamp, min, max, formatOptions, commit]);
165
+
166
+ // Keep display text in sync with controlled value changes (only when not editing)
167
+ const prevControlledValue = useRef(valueProp);
168
+ if (isControlled && valueProp !== prevControlledValue.current && !isEditing.current) {
169
+ prevControlledValue.current = valueProp;
170
+ const nextText = valueProp != null ? formatNum(valueProp, formatOptions) : "";
171
+ if (nextText !== inputText) setInputText(nextText);
172
+ }
173
+
174
+ const decrementDisabled =
175
+ disabled || readOnly || (min !== undefined && (currentValue ?? 0) <= min);
176
+ const incrementDisabled =
177
+ disabled || readOnly || (max !== undefined && (currentValue ?? 0) >= max);
178
+
179
+ return (
180
+ <InputGroup ref={ref} className={cn("w-36", className)} {...props}>
181
+ <InputGroupButton
182
+ aria-label="Decrease"
183
+ tabIndex={-1}
184
+ disabled={decrementDisabled}
185
+ onClick={() => applyStep(-step)}
186
+ className="rounded-r-none"
187
+ >
188
+ <Minus aria-hidden="true" />
189
+ </InputGroupButton>
190
+ <InputGroupInput
191
+ type="number"
192
+ id={id}
193
+ name={name}
194
+ value={inputText}
195
+ placeholder={placeholder}
196
+ disabled={disabled}
197
+ readOnly={readOnly}
198
+ aria-label={ariaLabel}
199
+ aria-labelledby={ariaLabelledby}
200
+ aria-describedby={ariaDescribedby}
201
+ aria-invalid={ariaInvalid}
202
+ aria-required={ariaRequired}
203
+ className="text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
204
+ onChange={handleChange}
205
+ onKeyDown={handleKeyDown}
206
+ onBlur={handleBlur}
207
+ />
208
+ <InputGroupButton
209
+ aria-label="Increase"
210
+ tabIndex={-1}
211
+ disabled={incrementDisabled}
212
+ onClick={() => applyStep(step)}
213
+ className="rounded-l-none"
214
+ >
215
+ <Plus aria-hidden="true" />
216
+ </InputGroupButton>
217
+ </InputGroup>
218
+ );
219
+ });
@@ -0,0 +1 @@
1
+ export { PageShell, type PageShellProps } from "./page-shell";
@@ -0,0 +1,105 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Plus } from "lucide-react";
3
+ import { expect, waitFor } from "storybook/test";
4
+ import { PageShell } from "./page-shell";
5
+ import { SectionHeader } from "../section-header";
6
+ import { Button } from "../button";
7
+ import { StatusBadge } from "../status-badge";
8
+ import { ResultCount, ViewToolbar } from "../view-toolbar";
9
+
10
+ const meta = {
11
+ title: "Layout/PageShell",
12
+ component: PageShell,
13
+ parameters: { layout: "fullscreen" },
14
+ } satisfies Meta<typeof PageShell>;
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ render: () => (
20
+ <PageShell header={<SectionHeader title="Settings" description="Manage your workspace." />}>
21
+ <div className="rounded-xl border bg-card p-6 text-body text-muted-foreground">Content</div>
22
+ </PageShell>
23
+ ),
24
+ };
25
+
26
+ /**
27
+ * `headerVariant="toolbar"` places a real `<ViewToolbar>` in `PageShell`'s
28
+ * header slot and pins it in POSITION (never a fixed height — see
29
+ * `Docs/View Toolbar Contract` R7) while the body scrolls beneath it. The
30
+ * outer `h-[420px] overflow-y-auto` div stands in for the `SidebarInset`
31
+ * scroll region `PageShell` is normally embedded in — `PageShell` itself
32
+ * never owns a scroll container (#367).
33
+ */
34
+ export const ToolbarSticky: Story = {
35
+ render: function ToolbarStickyStory() {
36
+ return (
37
+ <div data-testid="scroll-region" className="h-[420px] overflow-y-auto">
38
+ <PageShell
39
+ headerVariant="toolbar"
40
+ header={
41
+ <ViewToolbar
42
+ info="Every pipeline run in this workspace from the last 30 days, newest first."
43
+ actions={
44
+ <Button size="sm">
45
+ <Plus aria-hidden="true" />
46
+ New
47
+ </Button>
48
+ }
49
+ >
50
+ <StatusBadge status="running" size="sm" />
51
+ <ResultCount count={24} total={128}>
52
+ runs
53
+ </ResultCount>
54
+ </ViewToolbar>
55
+ }
56
+ >
57
+ <div className="flex flex-col gap-3">
58
+ {Array.from({ length: 40 }, (_, i) => (
59
+ <div
60
+ key={i}
61
+ data-testid={i === 20 ? "marker-row" : undefined}
62
+ className="rounded-lg border bg-card p-4 text-body"
63
+ >
64
+ Row {i + 1}
65
+ </div>
66
+ ))}
67
+ </div>
68
+ </PageShell>
69
+ </div>
70
+ );
71
+ },
72
+ play: async ({ canvasElement }) => {
73
+ const scrollRegion = canvasElement.querySelector<HTMLElement>('[data-testid="scroll-region"]');
74
+ const header = canvasElement.querySelector<HTMLElement>(
75
+ '[data-slot="page-shell-toolbar-header"]',
76
+ );
77
+ const marker = canvasElement.querySelector<HTMLElement>('[data-testid="marker-row"]');
78
+
79
+ await expect(scrollRegion).not.toBeNull();
80
+ await expect(header).not.toBeNull();
81
+ await expect(marker).not.toBeNull();
82
+
83
+ await expect(getComputedStyle(header!).position).toBe("sticky");
84
+
85
+ // Scroll past PageShell's own top padding first, so the header is already
86
+ // in its "stuck" (top: 0) state before the two measurements below — at
87
+ // scrollTop 0 the header still sits at its natural, un-stuck flow
88
+ // position (offset by the page padding), which is not what "pinned" means.
89
+ scrollRegion!.scrollTop = 100;
90
+ const headerTopStuck = header!.getBoundingClientRect().top;
91
+ const markerTopFirstScroll = marker!.getBoundingClientRect().top;
92
+
93
+ // Scroll the body further beneath the already-pinned header.
94
+ scrollRegion!.scrollTop = 600;
95
+
96
+ // The body actually moved underneath the header.
97
+ await waitFor(() => {
98
+ expect(marker!.getBoundingClientRect().top).toBeLessThan(markerTopFirstScroll);
99
+ });
100
+
101
+ // The header stayed pinned: its position is unchanged across the further scroll.
102
+ const headerTopAfterScroll = header!.getBoundingClientRect().top;
103
+ await expect(Math.abs(headerTopAfterScroll - headerTopStuck)).toBeLessThanOrEqual(1);
104
+ },
105
+ };
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { PageShell } from "./page-shell";
4
+
5
+ describe("PageShell", () => {
6
+ it("headerVariant omitted: renders the header inline with no sticky wrapper (AC2 — byte-identical default)", () => {
7
+ render(<PageShell header={<div data-testid="hdr">H</div>}>body</PageShell>);
8
+
9
+ const header = screen.getByTestId("hdr");
10
+ expect(screen.queryByTestId("page-shell-toolbar-header")).toBeNull();
11
+ expect(document.querySelector('[data-slot="page-shell-toolbar-header"]')).toBeNull();
12
+
13
+ let node: HTMLElement | null = header;
14
+ while (node) {
15
+ expect(node.className).not.toMatch(/(^|\s)sticky(\s|$)/);
16
+ node = node.parentElement;
17
+ }
18
+ });
19
+
20
+ it('headerVariant="default": same as omitted — no sticky wrapper', () => {
21
+ render(
22
+ <PageShell header={<div data-testid="hdr">H</div>} headerVariant="default">
23
+ body
24
+ </PageShell>,
25
+ );
26
+
27
+ expect(document.querySelector('[data-slot="page-shell-toolbar-header"]')).toBeNull();
28
+ });
29
+
30
+ it('headerVariant="toolbar": wraps the header in a sticky, bordered, blurred bar', () => {
31
+ render(
32
+ <PageShell header={<div data-testid="hdr">H</div>} headerVariant="toolbar">
33
+ body
34
+ </PageShell>,
35
+ );
36
+
37
+ const wrapper = document.querySelector('[data-slot="page-shell-toolbar-header"]');
38
+ expect(wrapper).not.toBeNull();
39
+ expect(wrapper).toContainElement(screen.getByTestId("hdr"));
40
+
41
+ const className = (wrapper as HTMLElement).className;
42
+ expect(className).toMatch(/(^|\s)sticky(\s|$)/);
43
+ expect(className).toContain("top-0");
44
+ expect(className).toContain("border-b");
45
+ expect(className).toContain("bg-background/95");
46
+
47
+ // Regression guard for ViewToolbar's R7 (wrap, never clip/fixed-height).
48
+ expect(className).not.toMatch(/(^|\s)h-\d/);
49
+ expect(className).not.toMatch(/overflow-hidden/);
50
+ });
51
+
52
+ it('headerVariant="toolbar" with no header prop: renders nothing, no empty sticky bar', () => {
53
+ render(<PageShell headerVariant="toolbar">body</PageShell>);
54
+
55
+ expect(document.querySelector('[data-slot="page-shell-toolbar-header"]')).toBeNull();
56
+ });
57
+ });
@@ -0,0 +1,62 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export interface PageShellProps {
5
+ children: ReactNode;
6
+ /** Optional header row (often a <SectionHeader />, or a <ViewToolbar> — see headerVariant). */
7
+ header?: ReactNode;
8
+ /**
9
+ * "default" (unset) — `header` renders inline, scrolls with the body.
10
+ * Byte-identical to today.
11
+ *
12
+ * "toolbar" — for a `<ViewToolbar>` header (`@elabs-ai/components-ui`,
13
+ * `Docs/View Toolbar Contract`): wraps `header` in a `sticky top-0` container
14
+ * with an opaque/blurred fill and a bottom hairline, so it stays pinned while
15
+ * the body scrolls beneath it. PLACES whatever node `header` is — it does not
16
+ * reimplement ViewToolbar's own info/left-cluster/actions grammar, and it does
17
+ * NOT cap the row's height (a `<ViewToolbar>` wraps at narrow widths; see
18
+ * `Docs/View Toolbar Contract` R7).
19
+ */
20
+ headerVariant?: "default" | "toolbar";
21
+ /** Constrain content width. Defaults to "xl". */
22
+ width?: "md" | "lg" | "xl" | "full";
23
+ className?: string;
24
+ contentClassName?: string;
25
+ }
26
+
27
+ const widthMap = {
28
+ md: "max-w-3xl",
29
+ lg: "max-w-5xl",
30
+ xl: "max-w-7xl",
31
+ full: "max-w-none",
32
+ } as const;
33
+
34
+ /** Page-level content container with consistent padding and max width. */
35
+ export function PageShell({
36
+ children,
37
+ header,
38
+ headerVariant = "default",
39
+ width = "xl",
40
+ className,
41
+ contentClassName,
42
+ }: PageShellProps) {
43
+ return (
44
+ <div className={cn("w-full px-4 py-6 sm:px-6 lg:px-8", className)}>
45
+ <div className={cn("mx-auto w-full space-y-6", widthMap[width], contentClassName)}>
46
+ {header ? (
47
+ headerVariant === "toolbar" ? (
48
+ <div
49
+ data-slot="page-shell-toolbar-header"
50
+ className="sticky top-0 z-10 border-b border-border bg-background/95 py-3 backdrop-blur"
51
+ >
52
+ {header}
53
+ </div>
54
+ ) : (
55
+ header
56
+ )
57
+ ) : null}
58
+ {children}
59
+ </div>
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ Pagination,
3
+ PaginationContent,
4
+ PaginationItem,
5
+ PaginationLink,
6
+ PaginationPrevious,
7
+ PaginationNext,
8
+ PaginationEllipsis,
9
+ } from "./pagination";
@@ -0,0 +1,57 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import {
3
+ Pagination,
4
+ PaginationContent,
5
+ PaginationEllipsis,
6
+ PaginationItem,
7
+ PaginationLink,
8
+ PaginationNext,
9
+ PaginationPrevious,
10
+ } from "./pagination";
11
+ const meta = {
12
+ title: "Navigation/Pagination",
13
+ component: Pagination,
14
+ tags: ["autodocs"],
15
+ argTypes: {
16
+ className: {
17
+ description: "Additional CSS classes applied to the pagination nav element.",
18
+ control: "text",
19
+ table: { category: "Styling" },
20
+ },
21
+ children: {
22
+ description: "Pagination content — compose with PaginationContent, PaginationItem, etc.",
23
+ control: false,
24
+ table: { category: "Content" },
25
+ },
26
+ },
27
+ } satisfies Meta<typeof Pagination>;
28
+ export default meta;
29
+ type Story = StoryObj<typeof meta>;
30
+ export const Default: Story = {
31
+ render: () => (
32
+ <Pagination>
33
+ <PaginationContent>
34
+ <PaginationItem>
35
+ <PaginationPrevious href="#" />
36
+ </PaginationItem>
37
+ <PaginationItem>
38
+ <PaginationLink href="#">1</PaginationLink>
39
+ </PaginationItem>
40
+ <PaginationItem>
41
+ <PaginationLink href="#" isActive>
42
+ 2
43
+ </PaginationLink>
44
+ </PaginationItem>
45
+ <PaginationItem>
46
+ <PaginationLink href="#">3</PaginationLink>
47
+ </PaginationItem>
48
+ <PaginationItem>
49
+ <PaginationEllipsis />
50
+ </PaginationItem>
51
+ <PaginationItem>
52
+ <PaginationNext href="#" />
53
+ </PaginationItem>
54
+ </PaginationContent>
55
+ </Pagination>
56
+ ),
57
+ };
@@ -0,0 +1,77 @@
1
+ import { forwardRef, type ComponentProps } from "react";
2
+ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
3
+ import { cn } from "../../lib/cn";
4
+ import { buttonVariants } from "../button";
5
+
6
+ export function Pagination({ className, ...props }: ComponentProps<"nav">) {
7
+ return (
8
+ <nav
9
+ role="navigation"
10
+ aria-label="pagination"
11
+ className={cn("mx-auto flex w-full justify-center", className)}
12
+ {...props}
13
+ />
14
+ );
15
+ }
16
+ export const PaginationContent = forwardRef<HTMLUListElement, ComponentProps<"ul">>(
17
+ function PaginationContent({ className, ...props }, ref) {
18
+ return (
19
+ <ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
20
+ );
21
+ },
22
+ );
23
+ export const PaginationItem = forwardRef<HTMLLIElement, ComponentProps<"li">>(
24
+ function PaginationItem(props, ref) {
25
+ return <li ref={ref} {...props} />;
26
+ },
27
+ );
28
+ type PaginationLinkProps = { isActive?: boolean } & ComponentProps<"a">;
29
+ export function PaginationLink({ className, isActive, ...props }: PaginationLinkProps) {
30
+ return (
31
+ <a
32
+ aria-current={isActive ? "page" : undefined}
33
+ className={cn(
34
+ buttonVariants({ variant: isActive ? "outline" : "ghost", size: "icon" }),
35
+ "cursor-pointer",
36
+ className,
37
+ )}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+ export function PaginationPrevious({ className, ...props }: ComponentProps<"a">) {
43
+ return (
44
+ <a
45
+ aria-label="Go to previous page"
46
+ className={cn(buttonVariants({ variant: "ghost" }), "gap-1 ps-2.5 cursor-pointer", className)}
47
+ {...props}
48
+ >
49
+ <ChevronLeft className="size-4" data-rtl-flip />
50
+ <span>Previous</span>
51
+ </a>
52
+ );
53
+ }
54
+ export function PaginationNext({ className, ...props }: ComponentProps<"a">) {
55
+ return (
56
+ <a
57
+ aria-label="Go to next page"
58
+ className={cn(buttonVariants({ variant: "ghost" }), "gap-1 pe-2.5 cursor-pointer", className)}
59
+ {...props}
60
+ >
61
+ <span>Next</span>
62
+ <ChevronRight className="size-4" data-rtl-flip />
63
+ </a>
64
+ );
65
+ }
66
+ export function PaginationEllipsis({ className, ...props }: ComponentProps<"span">) {
67
+ return (
68
+ <span
69
+ aria-hidden
70
+ className={cn("flex size-9 items-center justify-center", className)}
71
+ {...props}
72
+ >
73
+ <MoreHorizontal className="size-4" />
74
+ <span className="sr-only">More pages</span>
75
+ </span>
76
+ );
77
+ }
@@ -0,0 +1 @@
1
+ export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent } from "./popover";
@@ -0,0 +1,28 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import { Button } from "../button";
4
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
5
+ const meta = { title: "Overlays/Popover", component: Popover, tags: ["autodocs"] } satisfies Meta<
6
+ typeof Popover
7
+ >;
8
+ export default meta;
9
+ type Story = StoryObj<typeof meta>;
10
+ export const Default: Story = {
11
+ render: () => (
12
+ <Popover>
13
+ <PopoverTrigger asChild>
14
+ <Button variant="outline">Open</Button>
15
+ </PopoverTrigger>
16
+ <PopoverContent>Place content for the popover here.</PopoverContent>
17
+ </Popover>
18
+ ),
19
+ // Clicking the trigger mounts the popover content into the Radix portal.
20
+ play: async ({ canvas, canvasElement, userEvent }) => {
21
+ await userEvent.click(canvas.getByRole("button", { name: "Open" }));
22
+ const body = within(canvasElement.ownerDocument.body);
23
+ const content = await body.findByText("Place content for the popover here.");
24
+ // waitFor rides out the Radix fade-in/zoom entrance animation (opacity
25
+ // starts at 0, which jest-dom counts as not-visible).
26
+ await waitFor(() => expect(content).toBeVisible());
27
+ },
28
+ };
@@ -0,0 +1,16 @@
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 { Popover, PopoverContent, PopoverTrigger } from "./popover";
5
+ describe("Popover", () => {
6
+ it("opens content on trigger click", async () => {
7
+ render(
8
+ <Popover>
9
+ <PopoverTrigger>Open</PopoverTrigger>
10
+ <PopoverContent>Inside</PopoverContent>
11
+ </Popover>,
12
+ );
13
+ await userEvent.click(screen.getByText("Open"));
14
+ expect(await screen.findByText("Inside")).toBeInTheDocument();
15
+ });
16
+ });
@@ -0,0 +1,29 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
3
+ import { cn } from "../../lib/cn";
4
+
5
+ export const Popover = PopoverPrimitive.Root;
6
+ export const PopoverTrigger = PopoverPrimitive.Trigger;
7
+ export const PopoverAnchor = PopoverPrimitive.Anchor;
8
+
9
+ export const PopoverContent = forwardRef<
10
+ ElementRef<typeof PopoverPrimitive.Content>,
11
+ ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
12
+ >(function PopoverContent({ className, align = "center", sideOffset = 4, ...props }, ref) {
13
+ return (
14
+ <PopoverPrimitive.Portal>
15
+ <PopoverPrimitive.Content
16
+ ref={ref}
17
+ align={align}
18
+ sideOffset={sideOffset}
19
+ className={cn(
20
+ "z-50 w-72 rounded-md bg-popover p-4 text-popover-foreground shadow-ring-md outline-none",
21
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
22
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ </PopoverPrimitive.Portal>
28
+ );
29
+ });
@@ -0,0 +1 @@
1
+ export { Progress, progressIndicatorVariants, type ProgressProps } from "./progress";