@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,153 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Card, CardDescription, CardTitle } from "./card";
4
+
5
+ describe("Card", () => {
6
+ it("renders a nested title", () => {
7
+ render(
8
+ <Card>
9
+ <CardTitle>Hello</CardTitle>
10
+ </Card>,
11
+ );
12
+ expect(screen.getByText("Hello")).toBeInTheDocument();
13
+ });
14
+
15
+ // --- CardTitle `as` seam (#328) ---
16
+ it("CardTitle defaults to a <div> (byte-identical to pre-#328 output)", () => {
17
+ render(<CardTitle>Title</CardTitle>);
18
+ const el = screen.getByText("Title");
19
+ expect(el.tagName).toBe("DIV");
20
+ });
21
+
22
+ it("CardTitle renders the requested heading level via `as`, joining the document outline (#328)", () => {
23
+ render(<CardTitle as="h2">Section title</CardTitle>);
24
+ const heading = screen.getByRole("heading", { level: 2, name: "Section title" });
25
+ expect(heading.tagName).toBe("H2");
26
+ expect(heading).toHaveClass("text-title", "leading-none");
27
+ });
28
+
29
+ // --- CardDescription `text-muted-foreground` drop (#336) ---
30
+ it("CardDescription keeps its muted-foreground color class and actually balances (#336)", () => {
31
+ render(<CardDescription>Body copy</CardDescription>);
32
+ const el = screen.getByText("Body copy");
33
+ // The real #336 regression was an invalid, misspelled utility name that
34
+ // does not exist in Tailwind and emits zero CSS — tailwind-merge
35
+ // classified it as an unrecognized text color and dropped
36
+ // `text-muted-foreground` in favor of it. Assert the REAL rendered class
37
+ // list (not a mocked cn()) contains the color AND the real `text-balance`
38
+ // utility (which actually sets `text-wrap: balance`, finally also
39
+ // delivering #148).
40
+ expect(el).toHaveClass("text-muted-foreground");
41
+ expect(el).toHaveClass("text-balance");
42
+ });
43
+
44
+ // --- CardDescription `measure` cap (#339) ---
45
+ it("CardDescription has no max-w cap by default (#339)", () => {
46
+ render(<CardDescription>Short subtitle</CardDescription>);
47
+ expect(screen.getByText("Short subtitle")).not.toHaveClass("max-w-prose");
48
+ });
49
+
50
+ it("CardDescription caps to a readable measure when `measure` is set (#339)", () => {
51
+ render(
52
+ <CardDescription measure>Genuine prose that should not run edge to edge.</CardDescription>,
53
+ );
54
+ const el = screen.getByText(/Genuine prose/);
55
+ expect(el).toHaveClass("max-w-prose");
56
+ // measure must not regress the #336 fix
57
+ expect(el).toHaveClass("text-muted-foreground");
58
+ });
59
+
60
+ it("uses the shared container radius rung (rounded-lg, not the ambiguous rounded-xl) (#19)", () => {
61
+ render(<Card data-testid="card">x</Card>);
62
+ const card = screen.getByTestId("card");
63
+ expect(card).toHaveClass("rounded-lg");
64
+ expect(card).not.toHaveClass("rounded-xl");
65
+ });
66
+
67
+ // --- optional detail panel (#117) ---
68
+ it("without `detail`, renders a plain card — no grid, no panel (backwards compatible)", () => {
69
+ render(<Card data-testid="card">body</Card>);
70
+ const card = screen.getByTestId("card");
71
+ expect(card).not.toHaveClass("grid");
72
+ expect(card.textContent).toBe("body");
73
+ });
74
+
75
+ it("with `detail` (fixed, the default), renders the panel beside the main content", () => {
76
+ render(
77
+ <Card data-testid="card" detail={<span>DETAIL</span>}>
78
+ <span>MAIN</span>
79
+ </Card>,
80
+ );
81
+ const card = screen.getByTestId("card");
82
+ expect(card).toHaveClass("grid");
83
+ expect(screen.getByText("MAIN")).toBeInTheDocument();
84
+ expect(screen.getByText("DETAIL")).toBeInTheDocument();
85
+ expect(card.className).toContain("grid-cols-[minmax(0,1fr)_var(--card-detail-size)]");
86
+ });
87
+
88
+ it("with `detail` + hover, keeps the panel in the DOM but hides it from AT when collapsed", () => {
89
+ render(
90
+ <Card data-testid="card" detailReveal="hover" detail={<button>More</button>}>
91
+ main
92
+ </Card>,
93
+ );
94
+ const card = screen.getByTestId("card");
95
+ // collapsed at rest; revealed on hover AND keyboard focus-within
96
+ expect(card.className).toContain("grid-cols-[minmax(0,1fr)_0px]");
97
+ expect(card.className).toContain(
98
+ "focus-within:grid-cols-[minmax(0,1fr)_var(--card-detail-size)]",
99
+ );
100
+ // detail panel is in the DOM but hidden from AT at rest (aria-hidden=true, #141)
101
+ const region = card.querySelector("[role='region']");
102
+ expect(region).not.toBeNull();
103
+ expect(region).toHaveAttribute("aria-hidden", "true");
104
+ });
105
+
106
+ it("with `detail` + hover, detail region is labelled (landmark navigation, #141)", () => {
107
+ render(
108
+ <Card detailReveal="hover" detail={<span>info</span>} detailLabel="Card details">
109
+ main
110
+ </Card>,
111
+ );
112
+ const region = document.querySelector("[role='region']");
113
+ expect(region).toHaveAttribute("aria-label", "Card details");
114
+ });
115
+
116
+ it("with `detail` (fixed), detail region is labelled and always visible to AT", () => {
117
+ render(
118
+ <Card data-testid="card" detail={<span>D</span>} detailLabel="More info">
119
+ main
120
+ </Card>,
121
+ );
122
+ // getByRole respects aria-hidden; fixed panel should NOT be aria-hidden
123
+ const region = screen.getByRole("region", { name: "More info" });
124
+ expect(region).toBeInTheDocument();
125
+ expect(region).not.toHaveAttribute("aria-hidden");
126
+ });
127
+
128
+ it("supports bottom placement (a row grid track instead of a column)", () => {
129
+ render(
130
+ <Card data-testid="card" detailPlacement="bottom" detail={<span>D</span>}>
131
+ m
132
+ </Card>,
133
+ );
134
+ expect(screen.getByTestId("card").className).toContain(
135
+ "grid-rows-[minmax(0,1fr)_var(--card-detail-size)]",
136
+ );
137
+ });
138
+
139
+ it("drives the panel track from the --card-detail-size custom property (default + override)", () => {
140
+ const { rerender } = render(
141
+ <Card data-testid="card" detail="d">
142
+ m
143
+ </Card>,
144
+ );
145
+ expect(screen.getByTestId("card").style.getPropertyValue("--card-detail-size")).toBe("16rem");
146
+ rerender(
147
+ <Card data-testid="card" detail="d" detailSize="20rem">
148
+ m
149
+ </Card>,
150
+ );
151
+ expect(screen.getByTestId("card").style.getPropertyValue("--card-detail-size")).toBe("20rem");
152
+ });
153
+ });
@@ -0,0 +1,332 @@
1
+ "use client";
2
+
3
+ import {
4
+ forwardRef,
5
+ useRef,
6
+ useState,
7
+ useCallback,
8
+ type CSSProperties,
9
+ type HTMLAttributes,
10
+ type ReactNode,
11
+ } from "react";
12
+ import { cva, type VariantProps } from "class-variance-authority";
13
+ import { cn } from "../../lib/cn";
14
+
15
+ // `rounded-lg` is the shared "container radius" rung (= --radius-lg = var(--radius),
16
+ // 4px). Card, Alert (rounded-lg), and the markdown editor directive chrome all read
17
+ // this one rung so the SAME logical block shows ONE radius across editor↔preview
18
+ // (was rounded-xl = 8px, whose meaning silently changed with the --radius-xl override
19
+ // — see #19). Overlays (Dialog/AlertDialog) deliberately use a larger rung.
20
+ export const cardVariants = cva("rounded-lg border bg-card text-card-foreground shadow-sm", {
21
+ variants: {
22
+ // Opt-in hover-lift for clickable cards. Gated by --motion-factor via the
23
+ // duration-* utility; the transform is neutralized under OS reduced-motion
24
+ // (the shadow/border still respond — "reduced != none").
25
+ interactive: {
26
+ true: "cursor-pointer transition-[translate,box-shadow,border-color] duration-base ease-standard hover:-translate-y-1 hover:border-ring/40 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:hover:translate-y-0",
27
+ false: "",
28
+ },
29
+ },
30
+ defaultVariants: { interactive: false },
31
+ });
32
+
33
+ // Grid container for the OPTIONAL detail panel — applied only when `detail` is set.
34
+ // `fixed` keeps the panel track at its full size; `hover` collapses it to 0 at rest and
35
+ // animates it open on the card's own hover AND keyboard `focus-within`, while the `main`
36
+ // region (overflow-hidden) yields the space so the OUTER card size stays constant.
37
+ // Layout-animation trade-off: prefer `fixed` in very dense grids (animating grid tracks
38
+ // is more expensive than a static layout). Motion is tokened + snaps under reduced-motion.
39
+ const cardDetailVariants = cva("group grid overflow-hidden", {
40
+ variants: {
41
+ detailPlacement: { side: "", bottom: "" },
42
+ detailReveal: { fixed: "", hover: "" },
43
+ },
44
+ compoundVariants: [
45
+ {
46
+ detailPlacement: "side",
47
+ detailReveal: "fixed",
48
+ class: "grid-cols-[minmax(0,1fr)_var(--card-detail-size)]",
49
+ },
50
+ {
51
+ detailPlacement: "bottom",
52
+ detailReveal: "fixed",
53
+ class: "grid-rows-[minmax(0,1fr)_var(--card-detail-size)]",
54
+ },
55
+ {
56
+ detailPlacement: "side",
57
+ detailReveal: "hover",
58
+ class:
59
+ "grid-cols-[minmax(0,1fr)_0px] transition-[grid-template-columns] duration-base ease-standard hover:grid-cols-[minmax(0,1fr)_var(--card-detail-size)] focus-within:grid-cols-[minmax(0,1fr)_var(--card-detail-size)] motion-reduce:transition-none",
60
+ },
61
+ {
62
+ detailPlacement: "bottom",
63
+ detailReveal: "hover",
64
+ class:
65
+ "grid-rows-[minmax(0,1fr)_0px] transition-[grid-template-rows] duration-base ease-standard hover:grid-rows-[minmax(0,1fr)_var(--card-detail-size)] focus-within:grid-rows-[minmax(0,1fr)_var(--card-detail-size)] motion-reduce:transition-none",
66
+ },
67
+ ],
68
+ defaultVariants: { detailPlacement: "side", detailReveal: "fixed" },
69
+ });
70
+
71
+ export interface CardProps
72
+ extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> {
73
+ /**
74
+ * Optional detail-panel content. **Undefined → a plain card** (today's single `<div>` —
75
+ * no grid, no panel, zero behaviour change).
76
+ */
77
+ detail?: ReactNode;
78
+ /** Which edge the detail panel sits on. @default "side" */
79
+ detailPlacement?: "side" | "bottom";
80
+ /**
81
+ * `fixed` (default, accessible) → panel always visible. `hover` → hidden at rest, revealed
82
+ * on hover **and** keyboard `focus-within` at a FIXED outer footprint (the main content
83
+ * yields the space; no surrounding reflow). Use `hover` for **supplementary** detail only —
84
+ * essential content must use `fixed`. @default "fixed"
85
+ */
86
+ detailReveal?: "fixed" | "hover";
87
+ /** Detail track size (any CSS length). @default "16rem" (side) / "auto" (bottom) */
88
+ detailSize?: string;
89
+ /** Accessible label for the detail region. @default "Details" */
90
+ detailLabel?: string;
91
+ }
92
+
93
+ /**
94
+ * Inner component for `detailReveal="hover"` — needs hooks to track
95
+ * hover/focus-within and drive `aria-hidden` on the detail panel so the
96
+ * collapsed state is correctly hidden from the accessibility tree (WCAG 1.3.1 /
97
+ * 2.4.3). When collapsed, AT cannot reach the detail controls.
98
+ */
99
+ function CardWithHoverDetail({
100
+ className,
101
+ interactive,
102
+ detail,
103
+ detailPlacement = "side",
104
+ detailReveal: _detailReveal,
105
+ detailSize,
106
+ detailLabel = "Details",
107
+ style,
108
+ children,
109
+ ...props
110
+ }: CardProps) {
111
+ const [isRevealed, setIsRevealed] = useState(false);
112
+ const containerRef = useRef<HTMLDivElement>(null);
113
+
114
+ const reveal = useCallback(() => setIsRevealed(true), []);
115
+ const conceal = useCallback(() => {
116
+ // Only conceal if focus has truly left the card (not moved to detail).
117
+ // Use requestAnimationFrame so the new focusIn event fires first.
118
+ requestAnimationFrame(() => {
119
+ if (containerRef.current && !containerRef.current.contains(document.activeElement)) {
120
+ setIsRevealed(false);
121
+ }
122
+ });
123
+ }, []);
124
+
125
+ const handleMouseEnter = reveal;
126
+ const handleMouseLeave = useCallback(() => {
127
+ // Conceal on mouse-leave only if focus is not inside the card.
128
+ if (containerRef.current && !containerRef.current.contains(document.activeElement)) {
129
+ setIsRevealed(false);
130
+ }
131
+ }, []);
132
+
133
+ const handleFocusIn = reveal;
134
+ const handleFocusOut = conceal;
135
+
136
+ const size = detailSize ?? (detailPlacement === "side" ? "16rem" : "auto");
137
+ const dividerEdge = detailPlacement === "side" ? "border-s" : "border-t";
138
+ const dividerColor = isRevealed
139
+ ? "border-border transition-colors duration-base ease-standard motion-reduce:transition-none"
140
+ : "border-transparent transition-colors duration-base ease-standard motion-reduce:transition-none";
141
+
142
+ return (
143
+ <div
144
+ ref={containerRef}
145
+ className={cn(
146
+ cardVariants({ interactive }),
147
+ cardDetailVariants({ detailPlacement, detailReveal: "hover" }),
148
+ className,
149
+ )}
150
+ style={{ "--card-detail-size": size, ...style } as CSSProperties}
151
+ onMouseEnter={handleMouseEnter}
152
+ onMouseLeave={handleMouseLeave}
153
+ onFocus={handleFocusIn}
154
+ onBlur={handleFocusOut}
155
+ {...props}
156
+ >
157
+ <div className="min-h-0 min-w-0 overflow-hidden">{children}</div>
158
+ {/* aria-hidden mirrors the visual collapsed state so AT does not reach
159
+ hidden controls. When revealed, it is removed so AT can navigate in. */}
160
+ <div
161
+ role="region"
162
+ aria-label={detailLabel}
163
+ aria-hidden={!isRevealed}
164
+ className={cn("overflow-hidden bg-background p-6", dividerEdge, dividerColor)}
165
+ >
166
+ {detail}
167
+ </div>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
173
+ {
174
+ className,
175
+ interactive,
176
+ detail,
177
+ detailPlacement = "side",
178
+ detailReveal = "fixed",
179
+ detailSize,
180
+ detailLabel = "Details",
181
+ style,
182
+ children,
183
+ ...props
184
+ },
185
+ ref,
186
+ ) {
187
+ // No detail → today's plain card, byte-for-byte (no grid, no panel, no behaviour change).
188
+ if (detail == null) {
189
+ return (
190
+ <div
191
+ ref={ref}
192
+ className={cn(cardVariants({ interactive }), className)}
193
+ style={style}
194
+ {...props}
195
+ >
196
+ {children}
197
+ </div>
198
+ );
199
+ }
200
+
201
+ // `hover` mode uses the inner component with JS-tracked reveal state so we can
202
+ // correctly set aria-hidden on the collapsed panel (WCAG 1.3.1 / 2.4.3 fix, #141).
203
+ if (detailReveal === "hover") {
204
+ // Note: ref is forwarded to the outer div inside CardWithHoverDetail via a
205
+ // callback approach — consumers rarely need a ref on a hover-detail card, but
206
+ // we still accept the prop and pass it down as a forwarded ref is not directly
207
+ // composable with an inner function component. We use a workaround via the
208
+ // ref prop name to keep the public API consistent.
209
+ return (
210
+ <CardWithHoverDetail
211
+ className={className}
212
+ interactive={interactive}
213
+ detail={detail}
214
+ detailPlacement={detailPlacement}
215
+ detailSize={detailSize}
216
+ detailLabel={detailLabel}
217
+ style={style}
218
+ {...props}
219
+ >
220
+ {children}
221
+ </CardWithHoverDetail>
222
+ );
223
+ }
224
+
225
+ const size = detailSize ?? (detailPlacement === "side" ? "16rem" : "auto");
226
+ const dividerEdge = detailPlacement === "side" ? "border-s" : "border-t";
227
+ // In `fixed` mode the divider is always the hairline border.
228
+ const dividerColor = "border-border";
229
+
230
+ return (
231
+ <div
232
+ ref={ref}
233
+ className={cn(
234
+ cardVariants({ interactive }),
235
+ cardDetailVariants({ detailPlacement, detailReveal }),
236
+ className,
237
+ )}
238
+ style={{ "--card-detail-size": size, ...style } as CSSProperties}
239
+ {...props}
240
+ >
241
+ <div className="min-h-0 min-w-0 overflow-hidden">{children}</div>
242
+ {/* Fixed panel: always visible. Labelled region for landmark navigation (#141 P3). */}
243
+ <div
244
+ role="region"
245
+ aria-label={detailLabel}
246
+ className={cn("overflow-hidden bg-background p-6", dividerEdge, dividerColor)}
247
+ >
248
+ {detail}
249
+ </div>
250
+ </div>
251
+ );
252
+ });
253
+
254
+ export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
255
+ function CardHeader({ className, ...props }, ref) {
256
+ return <div ref={ref} className={cn("flex flex-col gap-1.5 p-6", className)} {...props} />;
257
+ },
258
+ );
259
+
260
+ export interface CardTitleProps extends HTMLAttributes<HTMLElement> {
261
+ /**
262
+ * Render as a different element — pick the semantic heading level so a
263
+ * card that titles a real page section contributes to the document outline
264
+ * (#328). The visual (`text-title leading-none`) is identical regardless of
265
+ * tag. @default "div"
266
+ */
267
+ as?: "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
268
+ }
269
+
270
+ // Ref generic is `HTMLDivElement`, not `HTMLElement`: every member of the `as`
271
+ // union (div, h1-h6) carries the same deprecated `align` member, so they are
272
+ // structurally interchangeable — but the bare `HTMLElement` supertype is NOT
273
+ // (it lacks `align`), which is what the polymorphic <Tag ref={ref}> below
274
+ // requires TS to check against for every possible tag.
275
+ export const CardTitle = forwardRef<HTMLDivElement, CardTitleProps>(function CardTitle(
276
+ { className, as: Tag = "div", ...props },
277
+ ref,
278
+ ) {
279
+ return <Tag ref={ref} className={cn("text-title leading-none", className)} {...props} />;
280
+ });
281
+
282
+ export interface CardDescriptionProps extends HTMLAttributes<HTMLParagraphElement> {
283
+ /**
284
+ * Cap the line length to a readable prose measure (`max-w-prose`, ~65ch)
285
+ * for genuine paragraph content (#339). Off by default — short,
286
+ * subtitle-style descriptions should stay full width.
287
+ */
288
+ measure?: boolean;
289
+ }
290
+
291
+ export const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(
292
+ function CardDescription({ className, measure, ...props }, ref) {
293
+ return (
294
+ <p
295
+ ref={ref}
296
+ // `text-balance` sets `text-wrap: balance`. #336's real defect was an
297
+ // invalid, misspelled utility name that emitted zero CSS (NOT a
298
+ // tailwind-merge conflict) — this is the real, registered class.
299
+ className={cn(
300
+ "text-sm text-balance text-muted-foreground",
301
+ measure && "max-w-prose",
302
+ className,
303
+ )}
304
+ {...props}
305
+ />
306
+ );
307
+ },
308
+ );
309
+
310
+ export const CardAction = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
311
+ function CardAction({ className, ...props }, ref) {
312
+ return (
313
+ <div
314
+ ref={ref}
315
+ className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
316
+ {...props}
317
+ />
318
+ );
319
+ },
320
+ );
321
+
322
+ export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
323
+ function CardContent({ className, ...props }, ref) {
324
+ return <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />;
325
+ },
326
+ );
327
+
328
+ export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
329
+ function CardFooter({ className, ...props }, ref) {
330
+ return <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />;
331
+ },
332
+ );
@@ -0,0 +1,13 @@
1
+ export {
2
+ Card,
3
+ cardVariants,
4
+ type CardProps,
5
+ CardHeader,
6
+ CardTitle,
7
+ type CardTitleProps,
8
+ CardDescription,
9
+ type CardDescriptionProps,
10
+ CardAction,
11
+ CardContent,
12
+ CardFooter,
13
+ } from "./card";
@@ -0,0 +1,102 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor } from "storybook/test";
3
+ import { Card, CardContent } from "../card";
4
+ import {
5
+ Carousel,
6
+ CarouselContent,
7
+ CarouselItem,
8
+ CarouselNext,
9
+ CarouselPrevious,
10
+ } from "./carousel";
11
+ const meta = {
12
+ title: "Display/Carousel",
13
+ component: Carousel,
14
+ parameters: { layout: "centered" },
15
+ argTypes: {
16
+ orientation: {
17
+ description: "Scroll axis of the carousel.",
18
+ control: { type: "radio" },
19
+ options: ["horizontal", "vertical"],
20
+ table: { category: "Appearance" },
21
+ },
22
+ opts: {
23
+ description: "Embla Carousel options object (loop, align, etc.).",
24
+ control: false,
25
+ table: { category: "Behavior" },
26
+ },
27
+ plugins: {
28
+ description: "Embla Carousel plugins (autoplay, etc.).",
29
+ control: false,
30
+ table: { category: "Behavior" },
31
+ },
32
+ setApi: {
33
+ description: "Callback to receive the Embla API instance.",
34
+ control: false,
35
+ table: { category: "Behavior" },
36
+ },
37
+ className: {
38
+ description: "Extra Tailwind classes merged via cn().",
39
+ control: "text",
40
+ table: { category: "Appearance" },
41
+ },
42
+ },
43
+ } satisfies Meta<typeof Carousel>;
44
+ export default meta;
45
+ type Story = StoryObj<typeof meta>;
46
+ export const Default: Story = {
47
+ render: () => (
48
+ <Carousel className="w-64">
49
+ <CarouselContent>
50
+ {[1, 2, 3, 4].map((n) => (
51
+ <CarouselItem key={n}>
52
+ <Card>
53
+ <CardContent className="flex h-28 items-center justify-center p-6 text-display font-semibold">
54
+ {n}
55
+ </CardContent>
56
+ </Card>
57
+ </CarouselItem>
58
+ ))}
59
+ </CarouselContent>
60
+ <CarouselPrevious />
61
+ <CarouselNext />
62
+ </Carousel>
63
+ ),
64
+ play: async ({ canvas }) => {
65
+ // The carousel region and navigation buttons are present and keyboard-labelled.
66
+ const region = canvas.getByRole("region", { name: /carousel/i });
67
+ await expect(region).toBeInTheDocument();
68
+ const nextBtn = canvas.getByRole("button", { name: /next slide/i });
69
+ await expect(nextBtn).toBeInTheDocument();
70
+ const prevBtn = canvas.getByRole("button", { name: /previous slide/i });
71
+ // First slide: Previous is disabled, Next is enabled. canScroll* flips on
72
+ // Embla's async post-mount onSelect — waitFor rides out that init (#279).
73
+ await waitFor(async () => {
74
+ await expect(prevBtn).toBeDisabled();
75
+ await expect(nextBtn).not.toBeDisabled();
76
+ });
77
+ },
78
+ };
79
+
80
+ /** A consumer-supplied `aria-label` overrides the default region name. */
81
+ export const CustomLabel: Story = {
82
+ render: () => (
83
+ <Carousel className="w-64" aria-label="Product photos">
84
+ <CarouselContent>
85
+ {[1, 2, 3].map((n) => (
86
+ <CarouselItem key={n}>
87
+ <Card>
88
+ <CardContent className="flex h-28 items-center justify-center p-6 text-display font-semibold">
89
+ {n}
90
+ </CardContent>
91
+ </Card>
92
+ </CarouselItem>
93
+ ))}
94
+ </CarouselContent>
95
+ <CarouselPrevious />
96
+ <CarouselNext />
97
+ </Carousel>
98
+ ),
99
+ play: async ({ canvas }) => {
100
+ await expect(canvas.getByRole("region", { name: /product photos/i })).toBeInTheDocument();
101
+ },
102
+ };