@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,185 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { Dialog } from "../dialog";
4
+ import { ExpandDialog, ExpandDialogPanes } from "./expand-dialog";
5
+
6
+ /** Radix portals the content, so assertions run against `document.body`. */
7
+ function renderOpen(ui: React.ReactNode) {
8
+ return render(<Dialog open>{ui}</Dialog>);
9
+ }
10
+
11
+ describe("ExpandDialogPanes", () => {
12
+ it("renders one full-width track and no region when there is no detail", () => {
13
+ render(
14
+ <ExpandDialogPanes>
15
+ <p>view</p>
16
+ </ExpandDialogPanes>,
17
+ );
18
+ expect(screen.getByText("view")).toBeInTheDocument();
19
+ expect(screen.queryByRole("region")).toBeNull();
20
+ expect(document.querySelector('[data-slot="expand-dialog-detail"]')).toBeNull();
21
+ });
22
+
23
+ it("names the detail region from the shared default", () => {
24
+ render(
25
+ <ExpandDialogPanes detail={<p>context</p>}>
26
+ <p>view</p>
27
+ </ExpandDialogPanes>,
28
+ );
29
+ expect(screen.getByRole("region", { name: "Details" })).toBeInTheDocument();
30
+ });
31
+
32
+ it("takes an explicit detailLabel", () => {
33
+ render(
34
+ <ExpandDialogPanes detail={<p>context</p>} detailLabel="Chart summary">
35
+ <p>view</p>
36
+ </ExpandDialogPanes>,
37
+ );
38
+ expect(screen.getByRole("region", { name: "Chart summary" })).toBeInTheDocument();
39
+ });
40
+
41
+ /*
42
+ * Both panes are `overflow-auto`. axe's `scrollable-region-focusable` fires
43
+ * on a scroll container with no focusable child, and a detail pane holding
44
+ * only text is exactly that — so each pane takes its own tab stop unless the
45
+ * caller opts out.
46
+ */
47
+ it("gives each pane a tab stop, with an opt-out", () => {
48
+ const { rerender } = render(
49
+ <ExpandDialogPanes detail={<p>context</p>}>
50
+ <p>view</p>
51
+ </ExpandDialogPanes>,
52
+ );
53
+ expect(document.querySelector('[data-slot="expand-dialog-view"]')).toHaveAttribute(
54
+ "tabindex",
55
+ "0",
56
+ );
57
+ expect(document.querySelector('[data-slot="expand-dialog-detail"]')).toHaveAttribute(
58
+ "tabindex",
59
+ "0",
60
+ );
61
+
62
+ rerender(
63
+ <ExpandDialogPanes detail={<p>context</p>} viewTabIndex={-1} detailTabIndex={-1}>
64
+ <p>view</p>
65
+ </ExpandDialogPanes>,
66
+ );
67
+ expect(document.querySelector('[data-slot="expand-dialog-view"]')).toHaveAttribute(
68
+ "tabindex",
69
+ "-1",
70
+ );
71
+ });
72
+
73
+ it("writes the track sizes as custom properties so a runtime string can size the grid", () => {
74
+ render(
75
+ <ExpandDialogPanes detail={<p>context</p>} viewSize="3fr" detailSize="20rem">
76
+ <p>view</p>
77
+ </ExpandDialogPanes>,
78
+ );
79
+ const panes = document.querySelector('[data-slot="expand-dialog-panes"]') as HTMLElement;
80
+ expect(panes.style.getPropertyValue("--expand-dialog-view-size")).toBe("3fr");
81
+ expect(panes.style.getPropertyValue("--expand-dialog-detail-size")).toBe("20rem");
82
+ });
83
+
84
+ it("switches the grid axis with detailPlacement", () => {
85
+ const { rerender } = render(
86
+ <ExpandDialogPanes detail={<p>context</p>}>
87
+ <p>view</p>
88
+ </ExpandDialogPanes>,
89
+ );
90
+ const side = document.querySelector('[data-slot="expand-dialog-panes"]')!.className;
91
+ expect(side).toContain("grid-cols-[var(--expand-dialog-view-size)");
92
+
93
+ rerender(
94
+ <ExpandDialogPanes detail={<p>context</p>} detailPlacement="bottom">
95
+ <p>view</p>
96
+ </ExpandDialogPanes>,
97
+ );
98
+ const bottom = document.querySelector('[data-slot="expand-dialog-panes"]')!.className;
99
+ expect(bottom).not.toContain("grid-cols-[var(--expand-dialog-view-size)");
100
+ expect(bottom).toContain("grid-rows-[var(--expand-dialog-view-size)");
101
+ });
102
+
103
+ it("stacks at the breakpoint and restores the side split above it", () => {
104
+ render(
105
+ <ExpandDialogPanes detail={<p>context</p>} stackBelow="md">
106
+ <p>view</p>
107
+ </ExpandDialogPanes>,
108
+ );
109
+ const cls = document.querySelector('[data-slot="expand-dialog-panes"]')!.className;
110
+ expect(cls).toContain("grid-rows-[var(--expand-dialog-view-size)");
111
+ expect(cls).toContain("md:grid-cols-[var(--expand-dialog-view-size)");
112
+ });
113
+ });
114
+
115
+ describe("ExpandDialog", () => {
116
+ it("names the dialog by its title", () => {
117
+ renderOpen(
118
+ <ExpandDialog title="Revenue by region">
119
+ <p>view</p>
120
+ </ExpandDialog>,
121
+ );
122
+ expect(screen.getByRole("dialog", { name: "Revenue by region" })).toBeInTheDocument();
123
+ });
124
+
125
+ it("describes the dialog only when a description is supplied", () => {
126
+ const { rerender } = renderOpen(
127
+ <ExpandDialog title="T">
128
+ <p>view</p>
129
+ </ExpandDialog>,
130
+ );
131
+ expect(screen.getByRole("dialog")).not.toHaveAttribute("aria-describedby");
132
+
133
+ rerender(
134
+ <Dialog open>
135
+ <ExpandDialog title="T" description="Trailing twelve months.">
136
+ <p>view</p>
137
+ </ExpandDialog>
138
+ </Dialog>,
139
+ );
140
+ expect(screen.getByRole("dialog")).toHaveAttribute("aria-describedby");
141
+ expect(screen.getByText("Trailing twelve months.")).toBeInTheDocument();
142
+ });
143
+
144
+ /*
145
+ * Radix positions the close button absolutely in the top inline-end corner.
146
+ * Without the inline-end padding a long title runs underneath it — the single
147
+ * easiest thing to drop when this anatomy moves between files.
148
+ */
149
+ it("keeps the header clear of the close button", () => {
150
+ renderOpen(
151
+ <ExpandDialog title="T">
152
+ <p>view</p>
153
+ </ExpandDialog>,
154
+ );
155
+ expect(document.querySelector('[data-slot="dialog-header"]')!.className).toContain("pe-12");
156
+ });
157
+
158
+ it("renders header actions on the title row", () => {
159
+ renderOpen(
160
+ <ExpandDialog title="T" actions={<button type="button">Download</button>}>
161
+ <p>view</p>
162
+ </ExpandDialog>,
163
+ );
164
+ const header = document.querySelector('[data-slot="dialog-header"]')!;
165
+ expect(header.querySelector("button")).toHaveTextContent("Download");
166
+ });
167
+
168
+ /*
169
+ * The body must NOT take a tab stop of its own: the two panes inside it are
170
+ * the real scroll owners and each already takes one, so a focusable body
171
+ * would add a third that scrolls nothing.
172
+ */
173
+ it("delegates scrolling to the panes, not the dialog body", () => {
174
+ renderOpen(
175
+ <ExpandDialog title="T" detail={<p>context</p>}>
176
+ <p>view</p>
177
+ </ExpandDialog>,
178
+ );
179
+ expect(document.querySelector('[data-slot="dialog-body"]')).toHaveAttribute("tabindex", "-1");
180
+ expect(document.querySelector('[data-slot="expand-dialog-view"]')).toHaveAttribute(
181
+ "tabindex",
182
+ "0",
183
+ );
184
+ });
185
+ });
@@ -0,0 +1,387 @@
1
+ "use client";
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import {
5
+ type ComponentPropsWithoutRef,
6
+ type CSSProperties,
7
+ type ElementRef,
8
+ forwardRef,
9
+ type HTMLAttributes,
10
+ type ReactNode,
11
+ } from "react";
12
+ import { cn } from "../../lib/cn";
13
+ import { DialogBody, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../dialog";
14
+ import { useLocale } from "../locale-provider";
15
+
16
+ /**
17
+ * ExpandDialog — the one "make this bigger" modal: enlarged content on the
18
+ * inline-start, its context on the inline-end.
19
+ *
20
+ * The shape was invented inside `@elabs-ai/components-charts`'
21
+ * `ChartFrame` (chart left, data summary right) and then hand-rolled a second
22
+ * and third time — a chat lightbox, an in-place CSS breakout in a consuming
23
+ * app — because it was trapped in a package the other surfaces cannot import.
24
+ * It lives here so every "expand" affordance in the system opens the SAME
25
+ * two-pane surface: a chart, a table, an image, a document.
26
+ *
27
+ * It is a LAYOUT, not a viewer. What goes in each pane is the caller's
28
+ * business; this component owns the dialog anatomy, the pane geometry, the
29
+ * scroll ownership and the region labelling.
30
+ */
31
+
32
+ /**
33
+ * The panes grid.
34
+ *
35
+ * `detailPlacement` picks the axis; `stackBelow` re-runs the same decision at a
36
+ * breakpoint, because a 2.5fr/1fr split on a 375px viewport leaves the detail
37
+ * pane about 107px wide — technically present, practically unreadable. The 2×4
38
+ * compound matrix mirrors `cardDetailVariants` (`card.tsx`) so the two
39
+ * detail-panel surfaces in this package resolve their tracks the same way.
40
+ *
41
+ * Track sizes arrive as CSS custom properties (`--expand-dialog-view-size` /
42
+ * `--expand-dialog-detail-size`) rather than arbitrary-value classes, because a
43
+ * runtime string cannot be a Tailwind class — the same technique `Card` uses
44
+ * for `--card-detail-size`. The one pair of properties drives whichever axis is
45
+ * active: columns when the detail is beside, rows when it is below.
46
+ */
47
+ export const expandDialogPanesVariants = cva("grid min-h-0 gap-3 overflow-hidden", {
48
+ variants: {
49
+ detailPlacement: { side: "", bottom: "" },
50
+ /**
51
+ * Below this breakpoint the panes stack, whatever `detailPlacement` says.
52
+ * A no-op (documented, not an impossible state) when the detail is already
53
+ * placed at the bottom.
54
+ */
55
+ stackBelow: { never: "", sm: "", md: "", lg: "" },
56
+ },
57
+ compoundVariants: [
58
+ {
59
+ detailPlacement: "side",
60
+ stackBelow: "never",
61
+ class:
62
+ "grid-cols-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)] grid-rows-[minmax(0,1fr)]",
63
+ },
64
+ {
65
+ detailPlacement: "side",
66
+ stackBelow: "sm",
67
+ class:
68
+ "grid-rows-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)] sm:grid-cols-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)] sm:grid-rows-[minmax(0,1fr)]",
69
+ },
70
+ {
71
+ detailPlacement: "side",
72
+ stackBelow: "md",
73
+ class:
74
+ "grid-rows-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)] md:grid-cols-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)] md:grid-rows-[minmax(0,1fr)]",
75
+ },
76
+ {
77
+ detailPlacement: "side",
78
+ stackBelow: "lg",
79
+ class:
80
+ "grid-rows-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)] lg:grid-cols-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)] lg:grid-rows-[minmax(0,1fr)]",
81
+ },
82
+ {
83
+ detailPlacement: "bottom",
84
+ stackBelow: "never",
85
+ class: "grid-rows-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)]",
86
+ },
87
+ {
88
+ detailPlacement: "bottom",
89
+ stackBelow: "sm",
90
+ class: "grid-rows-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)]",
91
+ },
92
+ {
93
+ detailPlacement: "bottom",
94
+ stackBelow: "md",
95
+ class: "grid-rows-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)]",
96
+ },
97
+ {
98
+ detailPlacement: "bottom",
99
+ stackBelow: "lg",
100
+ class: "grid-rows-[var(--expand-dialog-view-size)_var(--expand-dialog-detail-size)]",
101
+ },
102
+ ],
103
+ defaultVariants: { detailPlacement: "side", stackBelow: "never" },
104
+ });
105
+
106
+ /**
107
+ * A scrollable pane.
108
+ *
109
+ * `tabIndex ?? 0` follows `DialogBody`'s shipped precedent: a scroll container
110
+ * has to be keyboard-operable (WCAG 2.1.1, axe `scrollable-region-focusable`),
111
+ * and a pane holding only text has no focusable child to scroll from. Pass
112
+ * `tabIndex={-1}` when the pane always contains its own focusable content.
113
+ */
114
+ const paneClass =
115
+ "min-h-0 min-w-0 overflow-auto overscroll-contain focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
116
+
117
+ export interface ExpandDialogPanesProps
118
+ extends
119
+ Omit<HTMLAttributes<HTMLDivElement>, "children">,
120
+ VariantProps<typeof expandDialogPanesVariants> {
121
+ /** The enlarged content — the reason the dialog opened. */
122
+ children: ReactNode;
123
+ /**
124
+ * Context for what is in the view pane. Omit for a single-pane dialog: the
125
+ * grid then renders one full-width track and no region wrapper at all.
126
+ */
127
+ detail?: ReactNode;
128
+ /** Accessible name for the detail region. @default `t("ui.expandDialog.detail")` */
129
+ detailLabel?: string;
130
+ /** View track size (any CSS length). @default `"minmax(0, 2.5fr)"` */
131
+ viewSize?: string;
132
+ /** Detail track size (any CSS length). @default `"minmax(0, 1fr)"` */
133
+ detailSize?: string;
134
+ /** Class on the view pane. */
135
+ viewClassName?: string;
136
+ /** Class on the detail pane. */
137
+ detailClassName?: string;
138
+ /** Tab index for the view pane. Pass `-1` when it owns focusable content. */
139
+ viewTabIndex?: number;
140
+ /** Tab index for the detail pane. Pass `-1` when it owns focusable content. */
141
+ detailTabIndex?: number;
142
+ }
143
+
144
+ /**
145
+ * The two-pane body. Usable on its own for a headerless surface (an image
146
+ * lightbox), or via `ExpandDialog`, which supplies the dialog around it.
147
+ */
148
+ export const ExpandDialogPanes = forwardRef<HTMLDivElement, ExpandDialogPanesProps>(
149
+ function ExpandDialogPanes(
150
+ {
151
+ children,
152
+ className,
153
+ detail,
154
+ detailClassName,
155
+ detailLabel,
156
+ detailPlacement = "side",
157
+ detailSize = "minmax(0, 1fr)",
158
+ detailTabIndex,
159
+ stackBelow = "never",
160
+ style,
161
+ viewClassName,
162
+ viewSize = "minmax(0, 2.5fr)",
163
+ viewTabIndex,
164
+ ...props
165
+ },
166
+ ref,
167
+ ) {
168
+ const { t } = useLocale();
169
+
170
+ /*
171
+ * The raised view floats on the recessed dialog field — structure by ground
172
+ * offset plus elevation rather than a border, so it survives a theme swap
173
+ * and the shadowless decoration dial (`shadow-ring-md` bakes the hairline
174
+ * into the shadow's last layer, so nothing gains a second crisp stroke).
175
+ */
176
+ const view = (
177
+ <div
178
+ data-slot="expand-dialog-view"
179
+ tabIndex={viewTabIndex ?? 0}
180
+ className={cn(paneClass, "rounded-lg bg-card p-4 shadow-ring-md", viewClassName)}
181
+ >
182
+ {children}
183
+ </div>
184
+ );
185
+
186
+ // No detail → a single full-width track. Mirrors `Card`'s `detail == null`
187
+ // path: the optional pane costs nothing when it is not asked for.
188
+ if (detail == null) {
189
+ return (
190
+ <div
191
+ ref={ref}
192
+ data-slot="expand-dialog-panes"
193
+ className={cn("grid min-h-0 grid-rows-[minmax(0,1fr)] overflow-hidden", className)}
194
+ style={style}
195
+ {...props}
196
+ >
197
+ {view}
198
+ </div>
199
+ );
200
+ }
201
+
202
+ return (
203
+ <div
204
+ ref={ref}
205
+ data-slot="expand-dialog-panes"
206
+ className={cn(expandDialogPanesVariants({ detailPlacement, stackBelow }), className)}
207
+ style={
208
+ {
209
+ "--expand-dialog-view-size": viewSize,
210
+ "--expand-dialog-detail-size": detailSize,
211
+ ...style,
212
+ } as CSSProperties
213
+ }
214
+ {...props}
215
+ >
216
+ {view}
217
+ <div
218
+ data-slot="expand-dialog-detail"
219
+ role="region"
220
+ aria-label={detailLabel ?? t("ui.expandDialog.detail")}
221
+ tabIndex={detailTabIndex ?? 0}
222
+ className={cn(paneClass, detailClassName)}
223
+ >
224
+ {detail}
225
+ </div>
226
+ </div>
227
+ );
228
+ },
229
+ );
230
+
231
+ export interface ExpandDialogHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
232
+ /** Names the dialog. Rendered as the `<h2>` Radix labels the dialog by. */
233
+ title: ReactNode;
234
+ /** One line of supporting context under the title. */
235
+ description?: ReactNode;
236
+ /** Controls aligned to the inline end of the title row. */
237
+ actions?: ReactNode;
238
+ }
239
+
240
+ /**
241
+ * The dialog's title row.
242
+ *
243
+ * `pe-12` is load-bearing: Radix positions the close button absolutely in the
244
+ * top inline-end corner, so without the padding a long title runs underneath
245
+ * it.
246
+ */
247
+ // Not `forwardRef`: it renders `DialogHeader`, which is a plain function
248
+ // component with no ref to forward (the base slot stays `dialog-header`).
249
+ export function ExpandDialogHeader({
250
+ actions,
251
+ className,
252
+ description,
253
+ title,
254
+ ...props
255
+ }: ExpandDialogHeaderProps) {
256
+ return (
257
+ <DialogHeader className={cn("shrink-0 border-b px-6 py-4 pe-12", className)} {...props}>
258
+ <div className="flex items-center justify-between gap-3">
259
+ <div className="flex min-w-0 flex-col gap-1.5">
260
+ <DialogTitle className="truncate">{title}</DialogTitle>
261
+ {description ? <DialogDescription>{description}</DialogDescription> : null}
262
+ </div>
263
+ {actions ? <div className="flex shrink-0 items-center gap-1">{actions}</div> : null}
264
+ </div>
265
+ </DialogHeader>
266
+ );
267
+ }
268
+
269
+ export type ExpandDialogContentProps = ComponentPropsWithoutRef<typeof DialogContent>;
270
+
271
+ /**
272
+ * The full-bleed content box every expand surface uses.
273
+ *
274
+ * `size="full"` plus a `DialogBody` child puts `DialogContent` into its
275
+ * fixed-header regime (a 3-row grid, header pinned, body owning the scroll) —
276
+ * the anatomy IS the switch, so nothing here hand-rolls a flex column.
277
+ */
278
+ export const ExpandDialogContent = forwardRef<
279
+ ElementRef<typeof DialogContent>,
280
+ ExpandDialogContentProps
281
+ >(function ExpandDialogContent({ className, ...props }, ref) {
282
+ return <DialogContent ref={ref} size="full" className={cn("p-0", className)} {...props} />;
283
+ });
284
+
285
+ export interface ExpandDialogProps
286
+ extends
287
+ Omit<ExpandDialogContentProps, "title" | "children">,
288
+ Pick<ExpandDialogHeaderProps, "title" | "description" | "actions">,
289
+ Pick<
290
+ ExpandDialogPanesProps,
291
+ | "children"
292
+ | "detail"
293
+ | "detailClassName"
294
+ | "detailLabel"
295
+ | "detailPlacement"
296
+ | "detailSize"
297
+ | "detailTabIndex"
298
+ | "stackBelow"
299
+ | "viewClassName"
300
+ | "viewSize"
301
+ | "viewTabIndex"
302
+ > {
303
+ /** Class on the panes grid. */
304
+ panesClassName?: string;
305
+ /** Class on the recessed field the panes sit in. */
306
+ fieldClassName?: string;
307
+ }
308
+
309
+ /**
310
+ * The batteries-included composition: content box → header → scroll body →
311
+ * panes. Place it inside a `Dialog` you control:
312
+ *
313
+ * ```tsx
314
+ * <Dialog open={open} onOpenChange={setOpen}>
315
+ * <ExpandDialog title={chart.title} detail={<Summary />}>
316
+ * <Chart />
317
+ * </ExpandDialog>
318
+ * </Dialog>
319
+ * ```
320
+ *
321
+ * `Dialog`/`DialogTrigger` are deliberately NOT re-exported under an
322
+ * `ExpandDialog*` name — they would be zero-behaviour aliases, and open state
323
+ * is the consumer's already.
324
+ */
325
+ export const ExpandDialog = forwardRef<ElementRef<typeof DialogContent>, ExpandDialogProps>(
326
+ function ExpandDialog(
327
+ {
328
+ actions,
329
+ children,
330
+ className,
331
+ description,
332
+ detail,
333
+ detailClassName,
334
+ detailLabel,
335
+ detailPlacement,
336
+ detailSize,
337
+ detailTabIndex,
338
+ fieldClassName,
339
+ panesClassName,
340
+ stackBelow,
341
+ title,
342
+ viewClassName,
343
+ viewSize,
344
+ viewTabIndex,
345
+ ...props
346
+ },
347
+ ref,
348
+ ) {
349
+ return (
350
+ <ExpandDialogContent
351
+ ref={ref}
352
+ className={className}
353
+ /*
354
+ * With no description the dialog is still named by its title, so opt
355
+ * out of `aria-describedby` explicitly rather than let Radix warn about
356
+ * a missing description.
357
+ */
358
+ {...(description ? {} : { "aria-describedby": undefined })}
359
+ {...props}
360
+ >
361
+ <ExpandDialogHeader actions={actions} description={description} title={title} />
362
+ {/* `tabIndex={-1}`: the panes below are the real scroll owners and take
363
+ their own tab stops, so the body must not add a third. */}
364
+ <DialogBody
365
+ tabIndex={-1}
366
+ className={cn("overflow-hidden bg-background p-3", fieldClassName)}
367
+ >
368
+ <ExpandDialogPanes
369
+ className={cn("h-full", panesClassName)}
370
+ detail={detail}
371
+ detailClassName={detailClassName}
372
+ detailLabel={detailLabel}
373
+ detailPlacement={detailPlacement}
374
+ detailSize={detailSize}
375
+ detailTabIndex={detailTabIndex}
376
+ stackBelow={stackBelow}
377
+ viewClassName={viewClassName}
378
+ viewSize={viewSize}
379
+ viewTabIndex={viewTabIndex}
380
+ >
381
+ {children}
382
+ </ExpandDialogPanes>
383
+ </DialogBody>
384
+ </ExpandDialogContent>
385
+ );
386
+ },
387
+ );
@@ -0,0 +1,11 @@
1
+ export {
2
+ ExpandDialog,
3
+ ExpandDialogContent,
4
+ ExpandDialogHeader,
5
+ ExpandDialogPanes,
6
+ expandDialogPanesVariants,
7
+ type ExpandDialogContentProps,
8
+ type ExpandDialogHeaderProps,
9
+ type ExpandDialogPanesProps,
10
+ type ExpandDialogProps,
11
+ } from "./expand-dialog";
@@ -0,0 +1,114 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { Input } from "../input";
4
+ import { Textarea } from "../textarea";
5
+ import { FieldRow } from "./field-row";
6
+
7
+ const meta = {
8
+ title: "Forms/FieldRow",
9
+ component: FieldRow,
10
+ tags: ["autodocs"],
11
+ parameters: {
12
+ docs: {
13
+ description: {
14
+ component:
15
+ "Label/description/error/aria-describedby wiring for a single field OUTSIDE a react-hook-form context. See Forms/Form for the RHF-bound equivalent.",
16
+ },
17
+ },
18
+ },
19
+ args: {
20
+ label: "Name",
21
+ },
22
+ argTypes: {
23
+ label: { control: "text", table: { category: "Content" } },
24
+ description: { control: "text", table: { category: "Content" } },
25
+ error: { control: "text", table: { category: "Content" } },
26
+ children: { control: false, table: { category: "Content" } },
27
+ },
28
+ } satisfies Meta<typeof FieldRow>;
29
+ export default meta;
30
+ type Story = StoryObj<typeof meta>;
31
+
32
+ export const Default: Story = {
33
+ render: (args) => (
34
+ <div className="w-72">
35
+ <FieldRow {...args}>
36
+ <Input placeholder="Jane Doe" />
37
+ </FieldRow>
38
+ </div>
39
+ ),
40
+ play: async ({ canvas }) => {
41
+ const input = canvas.getByRole("textbox");
42
+ expect(input).not.toHaveAttribute("aria-describedby");
43
+ expect(input).toHaveAttribute("aria-invalid", "false");
44
+ },
45
+ };
46
+
47
+ export const WithHelp: Story = {
48
+ name: "With help",
49
+ render: (args) => (
50
+ <div className="w-72">
51
+ <FieldRow {...args} description="As it appears on your ID.">
52
+ <Input placeholder="Jane Doe" />
53
+ </FieldRow>
54
+ </div>
55
+ ),
56
+ play: async ({ canvas }) => {
57
+ const input = canvas.getByRole("textbox");
58
+ const describedBy = input.getAttribute("aria-describedby");
59
+ expect(describedBy).toBeTruthy();
60
+ expect(canvas.getByText("As it appears on your ID.").id).toBe(describedBy);
61
+ },
62
+ };
63
+
64
+ export const WithError: Story = {
65
+ name: "With error",
66
+ render: (args) => (
67
+ <div className="w-72">
68
+ <FieldRow {...args} error="Name is required.">
69
+ <Input placeholder="Jane Doe" />
70
+ </FieldRow>
71
+ </div>
72
+ ),
73
+ play: async ({ canvas }) => {
74
+ const input = canvas.getByRole("textbox");
75
+ expect(input).toHaveAttribute("aria-invalid", "true");
76
+ const alert = canvas.getByRole("alert");
77
+ expect(alert).toHaveTextContent("Name is required.");
78
+ expect(input.getAttribute("aria-describedby")).toBe(alert.id);
79
+ },
80
+ };
81
+
82
+ export const WithBoth: Story = {
83
+ name: "With help and error",
84
+ render: (args) => (
85
+ <div className="w-72">
86
+ <FieldRow {...args} description="As it appears on your ID." error="Name is required.">
87
+ <Input placeholder="Jane Doe" />
88
+ </FieldRow>
89
+ </div>
90
+ ),
91
+ play: async ({ canvas }) => {
92
+ const input = canvas.getByRole("textbox");
93
+ const ids = input.getAttribute("aria-describedby")?.split(" ") ?? [];
94
+ expect(ids).toHaveLength(2);
95
+ expect(canvas.getByText("As it appears on your ID.").id).toBe(ids[0]);
96
+ expect(canvas.getByRole("alert").id).toBe(ids[1]);
97
+ },
98
+ };
99
+
100
+ /** Composes with a Textarea just as readily as an Input — any single-element control works. */
101
+ export const WithTextarea: Story = {
102
+ name: "With a Textarea control",
103
+ render: (args) => (
104
+ <div className="w-72">
105
+ <FieldRow {...args} label="Bio" description="A short introduction.">
106
+ <Textarea placeholder="Tell us about yourself…" />
107
+ </FieldRow>
108
+ </div>
109
+ ),
110
+ play: async ({ canvas, userEvent }) => {
111
+ await userEvent.click(canvas.getByText("Bio"));
112
+ expect(canvas.getByRole("textbox")).toHaveFocus();
113
+ },
114
+ };