@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,140 @@
1
+ import type { ReactElement } from "react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { render, screen, within } from "@testing-library/react";
4
+ import { Calendar } from "./calendar";
5
+ import { Default as DefaultCalendarStory } from "./calendar.stories";
6
+
7
+ /** Class list of an element, safe for SVG (whose `className` is not a string). */
8
+ function classes(el: Element) {
9
+ return (el.getAttribute("class") ?? "").split(/\s+/);
10
+ }
11
+
12
+ /** Walks up from `el` looking for a `relative` ancestor at or below `root`. */
13
+ function hasPositionedAncestorWithin(el: Element, root: Element) {
14
+ let node = el.parentElement;
15
+ while (node) {
16
+ if (classes(node).includes("relative")) return true;
17
+ if (node === root) return false;
18
+ node = node.parentElement;
19
+ }
20
+ return false;
21
+ }
22
+
23
+ describe("Calendar", () => {
24
+ it("renders a month grid", () => {
25
+ render(<Calendar mode="single" />);
26
+ expect(screen.getByRole("grid")).toBeInTheDocument();
27
+ });
28
+
29
+ it("renders both month-navigation buttons", () => {
30
+ render(<Calendar mode="single" />);
31
+ expect(screen.getByRole("button", { name: /previous month/i })).toBeInTheDocument();
32
+ expect(screen.getByRole("button", { name: /next month/i })).toBeInTheDocument();
33
+ });
34
+
35
+ // react-day-picker v9 renders <nav> as a SIBLING of the month (not inside the
36
+ // caption, as v8 did). Any absolutely-positioned part must therefore be anchored
37
+ // by a `relative` ancestor INSIDE the calendar — otherwise it escapes to whatever
38
+ // positioned ancestor the page happens to have, and the nav buttons fly to the
39
+ // corners of the viewport / popover.
40
+ it("anchors every absolutely-positioned part inside the calendar", () => {
41
+ const { container } = render(<Calendar mode="single" />);
42
+ const root = container.firstElementChild as HTMLElement;
43
+ const absolute = [...root.querySelectorAll("*")].filter((el) =>
44
+ classes(el).includes("absolute"),
45
+ );
46
+
47
+ expect(absolute.length).toBeGreaterThan(0);
48
+ for (const el of absolute) {
49
+ expect(hasPositionedAncestorWithin(el, root)).toBe(true);
50
+ }
51
+ });
52
+
53
+ it("keeps the navigation in the caption row, not in the page flow above it", () => {
54
+ const { container } = render(<Calendar mode="single" />);
55
+ const nav = container.querySelector("nav");
56
+ expect(nav).not.toBeNull();
57
+ // The nav overlays the caption row; the buttons themselves stay in normal
58
+ // flow inside it, so they can never be positioned against an outside ancestor.
59
+ expect(nav?.className).toContain("absolute");
60
+ for (const button of nav?.querySelectorAll("button") ?? []) {
61
+ expect(classes(button)).not.toContain("absolute");
62
+ }
63
+ });
64
+ });
65
+
66
+ // #430 — `forms-calendar--default` used to seed its selected day from
67
+ // `new Date()`, so the calendar cell axe scored for `color-contrast` had 1
68
+ // character on days 1-9 and 2 characters on days 10-31 of the *host machine's*
69
+ // calendar. axe-core's `color-contrast` rule reports single-character text as
70
+ // `incomplete` (`shortTextContent`) rather than scoring it, and `incomplete`
71
+ // results never fail `toHaveNoViolations()` — so the story was red on ~22 days
72
+ // a month and green on ~9, entirely independent of any code change.
73
+ //
74
+ // This lock renders the STORY's actual `Default.render()` output (not a
75
+ // hand-rolled `<Calendar>`) under two faked system clocks, so it fails against
76
+ // the pre-fix story (whose `selected` tracked `new Date()`) and passes once
77
+ // the story pins `selected`/`defaultMonth` instead. It proves the fixture is
78
+ // what it claims to be: (a) genuinely independent of the wall clock, and (b)
79
+ // pinned to a TWO-digit day specifically, so the a11y assertion stays capable
80
+ // of failing rather than being silently routed into axe's unscoreable-text
81
+ // path. The real, accepted 4.31:1 ink pair this cell renders
82
+ // (`--primary-foreground` on `--primary`, light) is `INK_EXEMPT` in
83
+ // `packages/tokens/src/themes-contrast.test.ts` (#180) — fixing that colour is
84
+ // explicitly out of scope here.
85
+ describe("Calendar — story fixture is clock-independent (#430)", () => {
86
+ afterEach(() => {
87
+ vi.useRealTimers();
88
+ });
89
+
90
+ // CSF3 story `render` fns are typed `(args, context) => ReactNode`, but this
91
+ // one (like most in this repo) ignores both, calls `useState`, and reads only
92
+ // component state — i.e. it has the exact shape of a function component. It
93
+ // must be mounted as JSX (`<StoryComponent />`), not invoked directly:
94
+ // calling it as a plain function runs its hooks outside of React's render
95
+ // cycle ("Invalid hook call"), since Storybook itself always mounts `render`
96
+ // through the same component tree, never calls it as a helper.
97
+ const StoryComponent = DefaultCalendarStory.render as unknown as () => ReactElement;
98
+
99
+ function renderDefaultCalendarStory() {
100
+ const { container } = render(<StoryComponent />);
101
+ const scope = within(container);
102
+ const caption = scope.getByRole("status");
103
+ const selectedCell = container.querySelector('[role="gridcell"][aria-selected="true"]');
104
+ const selectedButton = selectedCell?.querySelector("button") ?? null;
105
+
106
+ return {
107
+ captionText: caption.textContent,
108
+ selectedAriaLabel: selectedButton?.getAttribute("aria-label") ?? null,
109
+ selectedDayText: selectedButton?.textContent ?? "",
110
+ };
111
+ }
112
+
113
+ it("renders identically whether the system clock reads a single- or a two-digit day", () => {
114
+ // Neither faked date is the pinned selected day (2026-08-17) itself — react-day-picker
115
+ // legitimately prefixes the selected cell's aria-label with "Today, " when the
116
+ // system clock's date happens to match it, which is correct "today" behavior, not
117
+ // the bug under test. Picking two dates that are never "today" isolates the thing
118
+ // this lock exists to catch: the rendered day/caption tracking the clock at all.
119
+ vi.setSystemTime(new Date(2026, 7, 5)); // single-digit "today" — must not matter
120
+ const fromSingleDigitToday = renderDefaultCalendarStory();
121
+
122
+ vi.setSystemTime(new Date(2026, 10, 10)); // two-digit "today", different month — must not matter either
123
+ const fromTwoDigitToday = renderDefaultCalendarStory();
124
+
125
+ expect(fromTwoDigitToday.captionText).toBe(fromSingleDigitToday.captionText);
126
+ expect(fromTwoDigitToday.selectedAriaLabel).toBe(fromSingleDigitToday.selectedAriaLabel);
127
+ expect(fromTwoDigitToday.selectedDayText).toBe(fromSingleDigitToday.selectedDayText);
128
+ });
129
+
130
+ // The load-bearing assertion: the check above alone would still pass if
131
+ // someone "fixed" the flake by pinning the selected day to the 5th instead
132
+ // of the 17th — two renders of a single-digit day also match each other.
133
+ // Only this digit-count check catches that regression.
134
+ it("pins the selected day's rendered text to 2+ digits (10-31), not 1", () => {
135
+ vi.setSystemTime(new Date(2026, 7, 5));
136
+ const { selectedDayText } = renderDefaultCalendarStory();
137
+
138
+ expect(selectedDayText.length).toBeGreaterThanOrEqual(2);
139
+ });
140
+ });
@@ -0,0 +1,70 @@
1
+ import { ChevronLeft, ChevronRight } from "lucide-react";
2
+ import { DayPicker } from "react-day-picker";
3
+ import { cn } from "../../lib/cn";
4
+ import { buttonVariants } from "../button";
5
+
6
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>;
7
+
8
+ /** Date picker calendar (react-day-picker v9), themed with brand tokens. */
9
+ export function Calendar({
10
+ className,
11
+ classNames,
12
+ showOutsideDays = true,
13
+ ...props
14
+ }: CalendarProps) {
15
+ return (
16
+ <DayPicker
17
+ showOutsideDays={showOutsideDays}
18
+ className={cn("p-3", className)}
19
+ classNames={{
20
+ // `months` is the positioning context for the nav overlay below: in
21
+ // react-day-picker v9 the <nav> is a SIBLING of the month(s), not a child
22
+ // of the caption, so the caption cannot anchor it.
23
+ months: "relative flex flex-col gap-2 sm:flex-row",
24
+ month: "flex flex-col gap-4",
25
+ // `min-h-7` matches the nav button row so the two align; it can still grow
26
+ // for a taller caption (e.g. `captionLayout="dropdown"`), and `px-8` keeps
27
+ // the label clear of the buttons overlaying either edge.
28
+ month_caption: "flex min-h-7 items-center justify-center px-8",
29
+ caption_label: "text-sm font-medium",
30
+ // Overlays the caption row and pushes the buttons to either edge; the
31
+ // buttons themselves stay in normal flow inside it.
32
+ nav: "absolute inset-x-0 top-0 flex h-7 items-center justify-between",
33
+ button_previous: cn(
34
+ buttonVariants({ variant: "outline" }),
35
+ "size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
36
+ ),
37
+ button_next: cn(
38
+ buttonVariants({ variant: "outline" }),
39
+ "size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
40
+ ),
41
+ month_grid: "w-full border-collapse space-y-1",
42
+ weekdays: "flex",
43
+ weekday: "w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground",
44
+ week: "mt-2 flex w-full",
45
+ day: "relative size-8 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-e-md first:[&:has([aria-selected])]:rounded-s-md last:[&:has([aria-selected])]:rounded-e-md",
46
+ day_button: cn(
47
+ buttonVariants({ variant: "ghost" }),
48
+ "size-8 p-0 font-normal aria-selected:opacity-100",
49
+ ),
50
+ range_start: "day-range-start rounded-s-md",
51
+ range_end: "day-range-end rounded-e-md",
52
+ range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
53
+ selected:
54
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
55
+ today: "bg-accent text-accent-foreground",
56
+ outside: "day-outside text-muted-foreground aria-selected:text-muted-foreground",
57
+ disabled: "text-muted-foreground opacity-50",
58
+ hidden: "invisible",
59
+ ...classNames,
60
+ }}
61
+ components={{
62
+ Chevron: ({ orientation }) => {
63
+ const Icon = orientation === "left" ? ChevronLeft : ChevronRight;
64
+ return <Icon className="size-4" data-rtl-flip />;
65
+ },
66
+ }}
67
+ {...props}
68
+ />
69
+ );
70
+ }
@@ -0,0 +1 @@
1
+ export { Calendar, type CalendarProps } from "./calendar";
@@ -0,0 +1,438 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, userEvent } from "storybook/test";
3
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
4
+
5
+ const meta = {
6
+ title: "Core/Card",
7
+ component: Card,
8
+ tags: ["autodocs"],
9
+ argTypes: {
10
+ interactive: {
11
+ description:
12
+ "Opt-in hover-lift for clickable cards (motion-tokened, respects reduced-motion).",
13
+ control: "boolean",
14
+ table: { category: "Behavior" },
15
+ },
16
+ detail: {
17
+ description: "Optional detail-panel content (ReactNode). Undefined = plain card.",
18
+ control: false,
19
+ table: { category: "Content" },
20
+ },
21
+ detailPlacement: {
22
+ description: "Which edge the detail panel sits on.",
23
+ control: { type: "radio" },
24
+ options: ["side", "bottom"],
25
+ table: { category: "Appearance" },
26
+ },
27
+ detailReveal: {
28
+ description: "`fixed` — always visible; `hover` — revealed on hover and focus-within.",
29
+ control: { type: "radio" },
30
+ options: ["fixed", "hover"],
31
+ table: { category: "Behavior" },
32
+ },
33
+ detailSize: {
34
+ description:
35
+ 'CSS length for the detail track (e.g. `"16rem"`). Defaults to 16rem (side) / auto (bottom).',
36
+ control: "text",
37
+ table: { category: "Appearance" },
38
+ },
39
+ detailLabel: {
40
+ description: 'Accessible label for the detail region (aria-label). Default: "Details".',
41
+ control: "text",
42
+ table: { category: "Appearance" },
43
+ },
44
+ className: {
45
+ description: "Extra Tailwind classes merged via cn().",
46
+ control: "text",
47
+ table: { category: "Appearance" },
48
+ },
49
+ },
50
+ } satisfies Meta<typeof Card>;
51
+ export default meta;
52
+ type Story = StoryObj<typeof meta>;
53
+
54
+ export const Default: Story = {
55
+ render: () => (
56
+ <Card className="max-w-sm">
57
+ <CardHeader>
58
+ <CardTitle>Monthly active users</CardTitle>
59
+ <CardDescription>Across all workspaces</CardDescription>
60
+ </CardHeader>
61
+ <CardContent>
62
+ <div className="text-display font-semibold">24,512</div>
63
+ </CardContent>
64
+ <CardFooter>
65
+ <span className="text-body text-muted-foreground">+12.4% vs last month</span>
66
+ </CardFooter>
67
+ </Card>
68
+ ),
69
+ };
70
+
71
+ /** Opt-in hover-lift for clickable cards (gated by the motion system). */
72
+ export const Interactive: Story = {
73
+ render: () => (
74
+ <Card interactive className="max-w-sm" tabIndex={0} role="button">
75
+ <CardHeader>
76
+ <CardTitle>Production pipeline</CardTitle>
77
+ <CardDescription>Hover or focus — lifts over the gated duration</CardDescription>
78
+ </CardHeader>
79
+ <CardContent>
80
+ <span className="text-body text-muted-foreground">Last run 4m ago · 1.2M rows</span>
81
+ </CardContent>
82
+ </Card>
83
+ ),
84
+ play: async ({ canvas }) => {
85
+ const card = canvas.getByRole("button");
86
+ const cs = getComputedStyle(card);
87
+ // The lift uses Tailwind v4's standalone `translate` property — the
88
+ // transition MUST list `translate` (not the `transform` shorthand) or it
89
+ // snaps instead of gliding. Guards against the v4 transform-split gotcha.
90
+ await expect(cs.transitionProperty).toContain("translate");
91
+ await expect(parseFloat(cs.transitionDuration)).toBeGreaterThanOrEqual(0.2); // gated --t-base
92
+ },
93
+ };
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Detail panel stories (#118 DP-01)
97
+ // Each story is wrapped in a sized parent so the grid layout has a defined
98
+ // height/width to work with (side panels need a concrete height).
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /** Side panel always visible (default detailReveal). */
102
+ export const DetailSideFixed: Story = {
103
+ render: () => (
104
+ <div className="h-64 w-[480px]">
105
+ <Card
106
+ className="h-full"
107
+ detail={
108
+ <div className="flex flex-col gap-2">
109
+ <p className="text-meta font-semibold text-muted-foreground uppercase tracking-wide">
110
+ Metadata
111
+ </p>
112
+ <ul className="text-body text-foreground space-y-1">
113
+ <li className="flex justify-between">
114
+ <span className="text-muted-foreground">Owner</span>
115
+ <span>analytics-team</span>
116
+ </li>
117
+ <li className="flex justify-between">
118
+ <span className="text-muted-foreground">Updated</span>
119
+ <span>2 days ago</span>
120
+ </li>
121
+ <li className="flex justify-between">
122
+ <span className="text-muted-foreground">Rows</span>
123
+ <span>1.2M</span>
124
+ </li>
125
+ </ul>
126
+ </div>
127
+ }
128
+ detailPlacement="side"
129
+ detailReveal="fixed"
130
+ >
131
+ <CardHeader>
132
+ <CardTitle>Sales pipeline</CardTitle>
133
+ <CardDescription>Q2 2026 opportunities</CardDescription>
134
+ </CardHeader>
135
+ <CardContent>
136
+ <div className="text-display font-semibold">$4.7M</div>
137
+ </CardContent>
138
+ </Card>
139
+ </div>
140
+ ),
141
+ };
142
+
143
+ /** Side panel revealed on hover / keyboard focus-within (outer card size stays constant). */
144
+ export const DetailSideHover: Story = {
145
+ render: () => (
146
+ <div className="h-64 w-[480px]">
147
+ <Card
148
+ className="h-full"
149
+ detail={
150
+ <div className="flex flex-col gap-2">
151
+ <p className="text-meta font-semibold text-muted-foreground uppercase tracking-wide">
152
+ Quick actions
153
+ </p>
154
+ <ul className="text-body space-y-1">
155
+ <li>
156
+ <button className="text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
157
+ View report
158
+ </button>
159
+ </li>
160
+ <li>
161
+ <button className="text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
162
+ Export CSV
163
+ </button>
164
+ </li>
165
+ <li>
166
+ <button className="text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
167
+ Share
168
+ </button>
169
+ </li>
170
+ </ul>
171
+ </div>
172
+ }
173
+ detailPlacement="side"
174
+ detailReveal="hover"
175
+ >
176
+ <CardHeader>
177
+ <CardTitle>Revenue report</CardTitle>
178
+ <CardDescription>Hover or tab into the card to reveal actions</CardDescription>
179
+ </CardHeader>
180
+ <CardContent>
181
+ <div className="text-display font-semibold">$182K</div>
182
+ </CardContent>
183
+ </Card>
184
+ </div>
185
+ ),
186
+ };
187
+
188
+ /** Bottom panel always visible. */
189
+ export const DetailBottomFixed: Story = {
190
+ render: () => (
191
+ <div className="h-80 w-96">
192
+ <Card
193
+ className="h-full"
194
+ detail={
195
+ <div className="flex items-center gap-4">
196
+ <span className="text-body text-muted-foreground">Last synced 5 min ago</span>
197
+ <span className="ms-auto rounded-full bg-surface px-2 py-0.5 text-meta border border-border text-foreground">
198
+ Live
199
+ </span>
200
+ </div>
201
+ }
202
+ detailPlacement="bottom"
203
+ detailReveal="fixed"
204
+ detailSize="3.5rem"
205
+ >
206
+ <CardHeader>
207
+ <CardTitle>Active sessions</CardTitle>
208
+ <CardDescription>Across all regions</CardDescription>
209
+ </CardHeader>
210
+ <CardContent>
211
+ <div className="text-display font-semibold">8,340</div>
212
+ </CardContent>
213
+ </Card>
214
+ </div>
215
+ ),
216
+ };
217
+
218
+ /** Bottom panel revealed on hover / keyboard focus-within. */
219
+ export const DetailBottomHover: Story = {
220
+ render: () => (
221
+ <div className="h-80 w-96">
222
+ <Card
223
+ className="h-full"
224
+ detail={
225
+ <div className="flex items-center gap-3">
226
+ <button className="text-body text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
227
+ Drill down
228
+ </button>
229
+ <button className="text-body text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
230
+ Download
231
+ </button>
232
+ </div>
233
+ }
234
+ detailPlacement="bottom"
235
+ detailReveal="hover"
236
+ detailSize="3rem"
237
+ >
238
+ <CardHeader>
239
+ <CardTitle>Ingest volume</CardTitle>
240
+ <CardDescription>Hover to reveal bottom actions</CardDescription>
241
+ </CardHeader>
242
+ <CardContent>
243
+ <div className="text-display font-semibold">94 GB/hr</div>
244
+ </CardContent>
245
+ </Card>
246
+ </div>
247
+ ),
248
+ };
249
+
250
+ /** Proves `interactive` (hover-lift) and `detail` panel coexist without conflict. */
251
+ export const InteractiveWithDetail: Story = {
252
+ render: () => (
253
+ <div className="h-64 w-[480px]">
254
+ <Card
255
+ className="h-full"
256
+ interactive
257
+ tabIndex={0}
258
+ role="button"
259
+ detail={
260
+ <div className="flex flex-col gap-2">
261
+ <p className="text-meta font-semibold text-muted-foreground uppercase tracking-wide">
262
+ Details
263
+ </p>
264
+ <ul className="text-body space-y-1">
265
+ <li className="flex justify-between">
266
+ <span className="text-muted-foreground">Status</span>
267
+ <span className="text-foreground">Healthy</span>
268
+ </li>
269
+ <li className="flex justify-between">
270
+ <span className="text-muted-foreground">SLA</span>
271
+ <span className="text-foreground">99.9%</span>
272
+ </li>
273
+ </ul>
274
+ </div>
275
+ }
276
+ detailPlacement="side"
277
+ detailReveal="fixed"
278
+ >
279
+ <CardHeader>
280
+ <CardTitle>Data pipeline</CardTitle>
281
+ <CardDescription>Interactive card with a fixed side panel</CardDescription>
282
+ </CardHeader>
283
+ <CardContent>
284
+ <span className="text-body text-muted-foreground">
285
+ Hover to lift · detail always visible
286
+ </span>
287
+ </CardContent>
288
+ </Card>
289
+ </div>
290
+ ),
291
+ play: async ({ canvas }) => {
292
+ const card = canvas.getByRole("button");
293
+ // Both interactive lift AND detail panel are rendered — card is in the DOM.
294
+ await expect(card).toBeInTheDocument();
295
+ const cs = getComputedStyle(card);
296
+ await expect(cs.transitionProperty).toContain("translate");
297
+ },
298
+ };
299
+
300
+ /**
301
+ * `CardTitle` defaults to a `<div>` (unchanged) but accepts `as` to render a
302
+ * real heading (`h1`-`h6`) so a page built from cards contributes to the
303
+ * document outline (#328). Visual is identical across every tag — only the
304
+ * underlying element and its accessible heading level change.
305
+ */
306
+ export const HeadingLevels: Story = {
307
+ render: () => (
308
+ <div className="flex max-w-sm flex-col gap-4">
309
+ <Card>
310
+ <CardHeader>
311
+ <CardTitle>Default — a plain div (no change)</CardTitle>
312
+ <CardDescription>Not reachable via heading navigation</CardDescription>
313
+ </CardHeader>
314
+ </Card>
315
+ <Card>
316
+ <CardHeader>
317
+ <CardTitle as="h2">as=&quot;h2&quot; — a real heading</CardTitle>
318
+ <CardDescription>Reachable via AT heading navigation at level 2</CardDescription>
319
+ </CardHeader>
320
+ </Card>
321
+ </div>
322
+ ),
323
+ play: async ({ canvas }) => {
324
+ // Default stays a div — no accessible heading for that title.
325
+ await expect(
326
+ canvas.queryByRole("heading", { name: /Default — a plain div/ }),
327
+ ).not.toBeInTheDocument();
328
+ // `as="h2"` produces a real, level-2 heading.
329
+ const heading = canvas.getByRole("heading", { level: 2, name: /a real heading/ });
330
+ await expect(heading.tagName).toBe("H2");
331
+ },
332
+ };
333
+
334
+ /**
335
+ * `CardDescription` gains an opt-in `measure` prop that caps genuine prose to
336
+ * a readable line length (`max-w-prose`, ~65ch) instead of running edge to
337
+ * edge in a wide card (#339). Off by default — short subtitle-style
338
+ * descriptions stay full width.
339
+ */
340
+ export const DescriptionMeasure: Story = {
341
+ render: () => (
342
+ <div className="flex w-[1000px] flex-col gap-6">
343
+ <Card className="w-full">
344
+ <CardHeader>
345
+ <CardTitle>Uncapped (default)</CardTitle>
346
+ <CardDescription>
347
+ This paragraph runs the full width of a very wide card, which makes it materially harder
348
+ to read once a line gets long enough — there is no natural place for the eye to return
349
+ to the start of the next line, so reading speed and comprehension both suffer on wide
350
+ viewports.
351
+ </CardDescription>
352
+ </CardHeader>
353
+ </Card>
354
+ <Card className="w-full">
355
+ <CardHeader>
356
+ <CardTitle>Capped via `measure`</CardTitle>
357
+ <CardDescription measure>
358
+ This paragraph is capped to a readable prose measure even inside a very wide card, so
359
+ each line stays short enough to read comfortably and the eye can find the start of the
360
+ next line without losing its place.
361
+ </CardDescription>
362
+ </CardHeader>
363
+ </Card>
364
+ </div>
365
+ ),
366
+ play: async ({ canvas }) => {
367
+ const capped = canvas.getByText(/capped to a readable prose measure/);
368
+ await expect(capped).toHaveClass("max-w-prose");
369
+ const uncapped = canvas.getByText(/runs the full width/);
370
+ await expect(uncapped).not.toHaveClass("max-w-prose");
371
+ },
372
+ };
373
+
374
+ /**
375
+ * Keyboard path: tab into a side+hover card and assert the detail's focusable
376
+ * control is reachable. The `focus-within` CSS selector reveals the panel; DOM
377
+ * assertions confirm the control is present and accessible (layout is opaque to
378
+ * jsdom so we verify focusability, not visual appearance).
379
+ */
380
+ export const DetailHoverKeyboardReveal: Story = {
381
+ render: () => (
382
+ <div className="h-64 w-[480px]">
383
+ {/* Sentinel button before the card so Tab lands inside the card next */}
384
+ <button className="sr-only" data-testid="before-sentinel">
385
+ Before card
386
+ </button>
387
+ <Card
388
+ className="h-full"
389
+ detail={
390
+ <div className="flex flex-col gap-2 pt-1">
391
+ <button
392
+ data-testid="detail-action"
393
+ className="text-body text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1 text-start"
394
+ >
395
+ Open details
396
+ </button>
397
+ </div>
398
+ }
399
+ detailPlacement="side"
400
+ detailReveal="hover"
401
+ >
402
+ <CardHeader>
403
+ <CardTitle>Keyboard reveal demo</CardTitle>
404
+ <CardDescription>Tab into the card — focus-within opens the panel</CardDescription>
405
+ </CardHeader>
406
+ <CardContent>
407
+ <span className="text-body text-muted-foreground">Press Tab to navigate in</span>
408
+ </CardContent>
409
+ </Card>
410
+ </div>
411
+ ),
412
+ play: async ({ canvas }) => {
413
+ // The detail button stays in the DOM even when the panel is visually collapsed
414
+ // (overflow-hidden clips it; display:none is NOT used so AT reaches it).
415
+ const detailBtn = canvas.getByTestId("detail-action");
416
+ await expect(detailBtn).toBeInTheDocument();
417
+
418
+ // Drive the keyboard path: focus the sentinel, then Tab to move into the card.
419
+ const sentinel = canvas.getByTestId("before-sentinel");
420
+ sentinel.focus();
421
+
422
+ // Tab through focusable elements inside the card until we reach the detail button.
423
+ const user = userEvent.setup({ delay: null });
424
+ // Tab once to enter the card area (the first tabbable element inside).
425
+ await user.tab();
426
+
427
+ // The detail button is always in the DOM (clipped, not removed), so focus can
428
+ // reach it via Tab — assert it is focusable and present.
429
+ await expect(detailBtn).toBeInTheDocument();
430
+ // Tab until we land on the detail button (at most a few extra tabs).
431
+ let attempts = 0;
432
+ while (document.activeElement !== detailBtn && attempts < 6) {
433
+ await user.tab();
434
+ attempts++;
435
+ }
436
+ await expect(document.activeElement).toBe(detailBtn);
437
+ },
438
+ };