@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,459 @@
1
+ "use client";
2
+ // a2ui.exposed: no
3
+
4
+ import {
5
+ createContext,
6
+ forwardRef,
7
+ use,
8
+ useCallback,
9
+ useId,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ type ChangeEvent,
14
+ type DragEvent,
15
+ type HTMLAttributes,
16
+ type ReactNode,
17
+ } from "react";
18
+ import { X, Upload, FileIcon, CheckCircle2, AlertCircle } from "lucide-react";
19
+ import { cn } from "../../lib/cn";
20
+ import { Button } from "../button";
21
+ import { Progress } from "../progress";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Types
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export type FileUploadStatus = "pending" | "uploading" | "success" | "error";
28
+
29
+ export interface UploadFile {
30
+ /** Unique identifier for this file in the list. */
31
+ id: string;
32
+ /** The underlying File object. */
33
+ file: File;
34
+ /** Upload progress 0–100. */
35
+ progress?: number;
36
+ /** Upload lifecycle status. */
37
+ status?: FileUploadStatus;
38
+ /** Error message shown when status is "error". */
39
+ errorMessage?: string;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Context
44
+ // ---------------------------------------------------------------------------
45
+
46
+ interface FileUploadContextValue {
47
+ files: UploadFile[];
48
+ addFiles: (incoming: File[]) => void;
49
+ removeFile: (id: string) => void;
50
+ isDragging: boolean;
51
+ /** Programmatically open the native file picker. */
52
+ openPicker: () => void;
53
+ accept?: string;
54
+ multiple?: boolean;
55
+ maxSize?: number;
56
+ maxFiles?: number;
57
+ disabled?: boolean;
58
+ inputId: string;
59
+ }
60
+
61
+ const FileUploadContext = createContext<FileUploadContextValue | null>(null);
62
+
63
+ function useFileUpload(): FileUploadContextValue {
64
+ const ctx = use(FileUploadContext);
65
+ if (!ctx) throw new Error("FileUpload subcomponents must be used inside <FileUpload>.");
66
+ return ctx;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Provider (FileUpload root)
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export interface FileUploadProps extends HTMLAttributes<HTMLDivElement> {
74
+ /** Controlled file list. */
75
+ files?: UploadFile[];
76
+ /** Called when files are added or removed. */
77
+ onFilesChange?: (files: UploadFile[]) => void;
78
+ /** Accepted MIME types / extensions (forwarded to <input accept>). */
79
+ accept?: string;
80
+ /** Allow multiple files at once. */
81
+ multiple?: boolean;
82
+ /** Maximum individual file size in bytes. */
83
+ maxSize?: number;
84
+ /** Maximum number of files in the list at once. */
85
+ maxFiles?: number;
86
+ /** Disable all interactions. */
87
+ disabled?: boolean;
88
+ children?: ReactNode;
89
+ }
90
+
91
+ /**
92
+ * FileUpload — compound-component root (Provider).
93
+ *
94
+ * Holds the selected-file list + drag state and exposes
95
+ * `{ files, addFiles, removeFile, isDragging }` via context.
96
+ * **Presentational** — the app drives per-item `progress` + `status` on
97
+ * `<FileUploadItem>`. No upload/network logic lives here.
98
+ *
99
+ * Controlled via `files`/`onFilesChange`; uncontrolled by default.
100
+ */
101
+ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(function FileUpload(
102
+ {
103
+ files: filesProp,
104
+ onFilesChange,
105
+ accept,
106
+ multiple = false,
107
+ maxSize,
108
+ maxFiles,
109
+ disabled = false,
110
+ className,
111
+ children,
112
+ ...props
113
+ },
114
+ ref,
115
+ ) {
116
+ const isControlled = filesProp !== undefined;
117
+ const [internalFiles, setInternalFiles] = useState<UploadFile[]>([]);
118
+ const [isDragging, setIsDragging] = useState(false);
119
+ const inputRef = useRef<HTMLInputElement>(null);
120
+ const inputId = useId();
121
+
122
+ const files = useMemo(
123
+ () => (isControlled ? (filesProp ?? []) : internalFiles),
124
+ [isControlled, filesProp, internalFiles],
125
+ );
126
+
127
+ const commit = useCallback(
128
+ (next: UploadFile[]) => {
129
+ if (!isControlled) setInternalFiles(next);
130
+ onFilesChange?.(next);
131
+ },
132
+ [isControlled, onFilesChange],
133
+ );
134
+
135
+ const addFiles = useCallback(
136
+ (incoming: File[]) => {
137
+ const toAdd = incoming.filter((f) => {
138
+ if (maxSize && f.size > maxSize) return false;
139
+ return true;
140
+ });
141
+
142
+ const next = [...files];
143
+ for (const f of toAdd) {
144
+ if (maxFiles && next.length >= maxFiles) break;
145
+ next.push({ id: `${f.name}-${Date.now()}-${Math.random()}`, file: f });
146
+ }
147
+ commit(next);
148
+ },
149
+ [files, maxSize, maxFiles, commit],
150
+ );
151
+
152
+ const removeFile = useCallback(
153
+ (id: string) => {
154
+ commit(files.filter((f) => f.id !== id));
155
+ },
156
+ [files, commit],
157
+ );
158
+
159
+ const openPicker = useCallback(() => {
160
+ inputRef.current?.click();
161
+ }, []);
162
+
163
+ const handleInputChange = useCallback(
164
+ (e: ChangeEvent<HTMLInputElement>) => {
165
+ if (e.target.files) {
166
+ addFiles(Array.from(e.target.files));
167
+ // Reset so the same file can be re-selected
168
+ e.target.value = "";
169
+ }
170
+ },
171
+ [addFiles],
172
+ );
173
+
174
+ const ctx = useMemo<FileUploadContextValue>(
175
+ () => ({
176
+ files,
177
+ addFiles,
178
+ removeFile,
179
+ isDragging,
180
+ openPicker,
181
+ accept,
182
+ multiple,
183
+ maxSize,
184
+ maxFiles,
185
+ disabled,
186
+ inputId,
187
+ }),
188
+ [
189
+ files,
190
+ addFiles,
191
+ removeFile,
192
+ isDragging,
193
+ openPicker,
194
+ accept,
195
+ multiple,
196
+ maxSize,
197
+ maxFiles,
198
+ disabled,
199
+ inputId,
200
+ ],
201
+ );
202
+
203
+ return (
204
+ <FileUploadContext value={ctx}>
205
+ <div
206
+ ref={ref}
207
+ data-slot="file-upload"
208
+ data-disabled={disabled || undefined}
209
+ className={cn("flex flex-col gap-3", className)}
210
+ {...props}
211
+ >
212
+ {/* Visually hidden real file input — always present for keyboard users */}
213
+ <input
214
+ ref={inputRef}
215
+ id={inputId}
216
+ type="file"
217
+ accept={accept}
218
+ multiple={multiple}
219
+ disabled={disabled}
220
+ aria-hidden="true"
221
+ tabIndex={-1}
222
+ className="sr-only"
223
+ onChange={handleInputChange}
224
+ />
225
+ {/* Drag state setter lives here so any child drag interaction is captured */}
226
+ <div
227
+ onDragEnter={() => {
228
+ if (!disabled) setIsDragging(true);
229
+ }}
230
+ onDragLeave={(e) => {
231
+ // Only clear dragging when leaving the root container
232
+ if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
233
+ setIsDragging(false);
234
+ }
235
+ }}
236
+ onDragOver={(e: DragEvent) => {
237
+ e.preventDefault();
238
+ }}
239
+ onDrop={(e: DragEvent) => {
240
+ e.preventDefault();
241
+ setIsDragging(false);
242
+ if (!disabled && e.dataTransfer.files.length > 0) {
243
+ addFiles(Array.from(e.dataTransfer.files));
244
+ }
245
+ }}
246
+ className="contents"
247
+ >
248
+ {children}
249
+ </div>
250
+ </div>
251
+ </FileUploadContext>
252
+ );
253
+ });
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // FileUploadDropzone
257
+ // ---------------------------------------------------------------------------
258
+
259
+ export interface FileUploadDropzoneProps extends HTMLAttributes<HTMLElement> {
260
+ /** Label for the visually-hidden file picker link (for SR users). */
261
+ browseLabel?: string;
262
+ }
263
+
264
+ /**
265
+ * FileUploadDropzone — the drag-and-drop target.
266
+ *
267
+ * Uses a `<label>` wrapping the hidden `<input>` so the ENTIRE zone is ONE hit
268
+ * target — no `<div onClick>`. Keyboard users get the Browse button as a
269
+ * complementary path.
270
+ */
271
+ export const FileUploadDropzone = forwardRef<HTMLElement, FileUploadDropzoneProps>(
272
+ function FileUploadDropzone(
273
+ { className, children, browseLabel = "Browse files", ...props },
274
+ ref,
275
+ ) {
276
+ const { isDragging, disabled, inputId, openPicker } = useFileUpload();
277
+
278
+ return (
279
+ // label wraps the hidden input — clicking anywhere on the zone opens the picker
280
+ <label
281
+ ref={ref as React.Ref<HTMLLabelElement>}
282
+ htmlFor={inputId}
283
+ data-slot="file-upload-dropzone"
284
+ data-dragging={isDragging || undefined}
285
+ data-disabled={disabled || undefined}
286
+ className={cn(
287
+ "flex flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed border-border-strong bg-surface-muted px-6 py-10",
288
+ "cursor-pointer transition-colors duration-fast",
289
+ "hover:border-ring hover:bg-accent/50",
290
+ "data-[dragging]:border-primary data-[dragging]:bg-primary/5",
291
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
292
+ "focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background",
293
+ className,
294
+ )}
295
+ {...props}
296
+ >
297
+ {children ?? (
298
+ <>
299
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
300
+ <Upload className="size-5 text-muted-foreground" aria-hidden="true" />
301
+ </div>
302
+ <div className="flex flex-col items-center gap-1 text-center">
303
+ <p className="text-sm font-medium text-foreground">Drag & drop files here</p>
304
+ <p className="text-xs text-muted-foreground">
305
+ or{" "}
306
+ <button
307
+ type="button"
308
+ disabled={disabled}
309
+ onClick={(e) => {
310
+ e.preventDefault();
311
+ openPicker();
312
+ }}
313
+ className={cn(
314
+ // #399 — inline text trigger inside a sentence → `-text` rung.
315
+ "text-primary-text underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm",
316
+ "disabled:pointer-events-none disabled:opacity-50",
317
+ )}
318
+ >
319
+ {browseLabel}
320
+ </button>
321
+ </p>
322
+ </div>
323
+ </>
324
+ )}
325
+ </label>
326
+ );
327
+ },
328
+ );
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // FileUploadList
332
+ // ---------------------------------------------------------------------------
333
+
334
+ export type FileUploadListProps = HTMLAttributes<HTMLUListElement>;
335
+
336
+ /**
337
+ * FileUploadList — the `role="status"` region that lists accepted files.
338
+ * Reads from context; children are `<FileUploadItem>` elements.
339
+ */
340
+ export const FileUploadList = forwardRef<HTMLUListElement, FileUploadListProps>(
341
+ function FileUploadList({ className, children, ...props }, ref) {
342
+ const { files } = useFileUpload();
343
+ if (files.length === 0 && !children) return null;
344
+
345
+ return (
346
+ <ul
347
+ ref={ref}
348
+ role="status"
349
+ aria-live="polite"
350
+ aria-label="Selected files"
351
+ data-slot="file-upload-list"
352
+ className={cn("flex flex-col gap-2", className)}
353
+ {...props}
354
+ >
355
+ {children}
356
+ </ul>
357
+ );
358
+ },
359
+ );
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // FileUploadItem
363
+ // ---------------------------------------------------------------------------
364
+
365
+ export interface FileUploadItemProps extends HTMLAttributes<HTMLLIElement> {
366
+ /** The UploadFile entry to display. */
367
+ uploadFile: UploadFile;
368
+ /** Upload progress 0–100. Shown only during "uploading" status. */
369
+ progress?: number;
370
+ /** Current status of the upload. */
371
+ status?: FileUploadStatus;
372
+ /** Error message (displayed when status="error"). */
373
+ errorMessage?: string;
374
+ }
375
+
376
+ const statusIcon: Record<FileUploadStatus, ReactNode> = {
377
+ pending: <FileIcon className="size-4 text-muted-foreground" aria-hidden="true" />,
378
+ uploading: <FileIcon className="size-4 text-primary" aria-hidden="true" />,
379
+ success: <CheckCircle2 className="size-4 text-success" aria-hidden="true" />,
380
+ error: <AlertCircle className="size-4 text-destructive" aria-hidden="true" />,
381
+ };
382
+
383
+ /**
384
+ * FileUploadItem — a single row in the file list.
385
+ *
386
+ * The app controls `progress` and `status` to drive the upload lifecycle.
387
+ * Per-file errors use `role="alert"`.
388
+ */
389
+ export const FileUploadItem = forwardRef<HTMLLIElement, FileUploadItemProps>(
390
+ function FileUploadItem(
391
+ { uploadFile, progress, status = "pending", errorMessage, className, ...props },
392
+ ref,
393
+ ) {
394
+ const { removeFile, disabled } = useFileUpload();
395
+ const resolvedProgress = progress ?? uploadFile.progress;
396
+ const resolvedStatus = status ?? uploadFile.status ?? "pending";
397
+ const resolvedError = errorMessage ?? uploadFile.errorMessage;
398
+
399
+ const isUploading = resolvedStatus === "uploading";
400
+ const isError = resolvedStatus === "error";
401
+
402
+ return (
403
+ <li
404
+ ref={ref}
405
+ data-slot="file-upload-item"
406
+ data-status={resolvedStatus}
407
+ className={cn(
408
+ "flex flex-col gap-1 rounded-md border border-border bg-card px-3 py-2",
409
+ className,
410
+ )}
411
+ {...props}
412
+ >
413
+ <div className="flex items-center gap-2">
414
+ {statusIcon[resolvedStatus]}
415
+ <span className="flex-1 min-w-0 truncate text-sm text-foreground">
416
+ {uploadFile.file.name}
417
+ </span>
418
+ <span className="shrink-0 text-xs tabular-nums text-muted-foreground">
419
+ {formatFileSize(uploadFile.file.size)}
420
+ </span>
421
+ <Button
422
+ type="button"
423
+ variant="ghost"
424
+ size="icon-sm"
425
+ aria-label={`Remove ${uploadFile.file.name}`}
426
+ disabled={disabled}
427
+ onClick={() => removeFile(uploadFile.id)}
428
+ className="shrink-0"
429
+ >
430
+ <X className="size-4" aria-hidden="true" />
431
+ </Button>
432
+ </div>
433
+ {isUploading && resolvedProgress !== undefined && (
434
+ <Progress value={resolvedProgress} className="h-1" />
435
+ )}
436
+ {isError && resolvedError && (
437
+ <p role="alert" className="text-xs text-destructive">
438
+ {resolvedError}
439
+ </p>
440
+ )}
441
+ </li>
442
+ );
443
+ },
444
+ );
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // Helpers
448
+ // ---------------------------------------------------------------------------
449
+
450
+ function formatFileSize(bytes: number): string {
451
+ if (bytes < 1024) return `${bytes}\u00A0B`;
452
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}\u00A0KB`;
453
+ return `${(bytes / (1024 * 1024)).toFixed(1)}\u00A0MB`;
454
+ }
455
+
456
+ // ---------------------------------------------------------------------------
457
+ // useFileUpload export (for advanced consumers outside a subpart)
458
+ // ---------------------------------------------------------------------------
459
+ export { useFileUpload };
@@ -0,0 +1,13 @@
1
+ export {
2
+ FileUpload,
3
+ FileUploadDropzone,
4
+ FileUploadList,
5
+ FileUploadItem,
6
+ useFileUpload,
7
+ type FileUploadProps,
8
+ type FileUploadDropzoneProps,
9
+ type FileUploadListProps,
10
+ type FileUploadItemProps,
11
+ type FileUploadStatus,
12
+ type UploadFile,
13
+ } from "./file-upload";
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { useForm } from "react-hook-form";
4
+ import { zodResolver } from "@hookform/resolvers/zod";
5
+ import { z } from "zod";
6
+ import { Button } from "../button";
7
+ import { Input } from "../input";
8
+ import {
9
+ Form,
10
+ FormControl,
11
+ FormDescription,
12
+ FormField,
13
+ FormItem,
14
+ FormLabel,
15
+ FormMessage,
16
+ } from "./form";
17
+
18
+ const schema = z.object({ email: z.string().email("Enter a valid email") });
19
+
20
+ function DemoForm() {
21
+ const form = useForm<z.infer<typeof schema>>({
22
+ resolver: zodResolver(schema),
23
+ defaultValues: { email: "" },
24
+ });
25
+ return (
26
+ <Form {...form}>
27
+ <form onSubmit={form.handleSubmit(() => {})} className="w-72 space-y-4">
28
+ <FormField
29
+ control={form.control}
30
+ name="email"
31
+ render={({ field }) => (
32
+ <FormItem>
33
+ <FormLabel>Email</FormLabel>
34
+ <FormControl>
35
+ <Input placeholder="you@example.com" {...field} />
36
+ </FormControl>
37
+ <FormDescription>We'll never share it.</FormDescription>
38
+ <FormMessage />
39
+ </FormItem>
40
+ )}
41
+ />
42
+ <Button type="submit">Submit</Button>
43
+ </form>
44
+ </Form>
45
+ );
46
+ }
47
+
48
+ const meta = {
49
+ title: "Forms/Form",
50
+ component: DemoForm,
51
+ tags: ["autodocs"],
52
+ parameters: {
53
+ docs: {
54
+ description: {
55
+ component:
56
+ "react-hook-form + zod integration layer. Compose FormField, FormItem, FormLabel, FormControl, FormDescription, and FormMessage inside a Form provider.",
57
+ },
58
+ },
59
+ },
60
+ } satisfies Meta<typeof DemoForm>;
61
+ export default meta;
62
+ type Story = StoryObj<typeof meta>;
63
+ export const Default: Story = {
64
+ // Submits with an invalid email and confirms the inline error message appears.
65
+ play: async ({ canvas, userEvent }) => {
66
+ const submit = canvas.getByRole("button", { name: /submit/i });
67
+ await userEvent.click(submit);
68
+ const error = await canvas.findByText(/valid email/i);
69
+ await expect(error).toBeInTheDocument();
70
+ },
71
+ };
@@ -0,0 +1,141 @@
1
+ import {
2
+ createContext,
3
+ forwardRef,
4
+ useContext,
5
+ useId,
6
+ type ComponentPropsWithoutRef,
7
+ type ElementRef,
8
+ type HTMLAttributes,
9
+ } from "react";
10
+ import { Slot } from "@radix-ui/react-slot";
11
+ import {
12
+ Controller,
13
+ FormProvider,
14
+ useFormContext,
15
+ type ControllerProps,
16
+ type FieldPath,
17
+ type FieldValues,
18
+ } from "react-hook-form";
19
+ import { cn } from "../../lib/cn";
20
+ import { Label } from "../label";
21
+
22
+ export const Form = FormProvider;
23
+
24
+ type FormFieldContextValue<
25
+ TFieldValues extends FieldValues = FieldValues,
26
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
27
+ > = { name: TName };
28
+
29
+ const FormFieldContext = createContext<FormFieldContextValue>({} as FormFieldContextValue);
30
+
31
+ export function FormField<
32
+ TFieldValues extends FieldValues = FieldValues,
33
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
34
+ >(props: ControllerProps<TFieldValues, TName>) {
35
+ return (
36
+ <FormFieldContext.Provider value={{ name: props.name }}>
37
+ <Controller {...props} />
38
+ </FormFieldContext.Provider>
39
+ );
40
+ }
41
+
42
+ const FormItemContext = createContext<{ id: string }>({} as { id: string });
43
+
44
+ export function useFormField() {
45
+ const fieldContext = useContext(FormFieldContext);
46
+ const itemContext = useContext(FormItemContext);
47
+ const { getFieldState, formState } = useFormContext();
48
+ const fieldState = getFieldState(fieldContext.name, formState);
49
+ if (!fieldContext) throw new Error("useFormField should be used within <FormField>");
50
+ const { id } = itemContext;
51
+ return {
52
+ id,
53
+ name: fieldContext.name,
54
+ formItemId: `${id}-form-item`,
55
+ formDescriptionId: `${id}-form-item-description`,
56
+ formMessageId: `${id}-form-item-message`,
57
+ ...fieldState,
58
+ };
59
+ }
60
+
61
+ export const FormItem = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
62
+ function FormItem({ className, ...props }, ref) {
63
+ const id = useId();
64
+ return (
65
+ <FormItemContext.Provider value={{ id }}>
66
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
67
+ </FormItemContext.Provider>
68
+ );
69
+ },
70
+ );
71
+
72
+ export const FormLabel = forwardRef<
73
+ ElementRef<typeof Label>,
74
+ ComponentPropsWithoutRef<typeof Label>
75
+ >(function FormLabel({ className, ...props }, ref) {
76
+ const { error, formItemId } = useFormField();
77
+ return (
78
+ <Label
79
+ ref={ref}
80
+ htmlFor={formItemId}
81
+ className={cn(
82
+ "transition-colors duration-fast ease-standard",
83
+ error && "text-destructive",
84
+ className,
85
+ )}
86
+ {...props}
87
+ />
88
+ );
89
+ });
90
+
91
+ export const FormControl = forwardRef<
92
+ ElementRef<typeof Slot>,
93
+ ComponentPropsWithoutRef<typeof Slot>
94
+ >(function FormControl(props, ref) {
95
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
96
+ return (
97
+ <Slot
98
+ ref={ref}
99
+ id={formItemId}
100
+ aria-describedby={error ? `${formDescriptionId} ${formMessageId}` : formDescriptionId}
101
+ aria-invalid={!!error}
102
+ {...props}
103
+ />
104
+ );
105
+ });
106
+
107
+ export const FormDescription = forwardRef<
108
+ HTMLParagraphElement,
109
+ HTMLAttributes<HTMLParagraphElement>
110
+ >(function FormDescription({ className, ...props }, ref) {
111
+ const { formDescriptionId } = useFormField();
112
+ return (
113
+ <p
114
+ ref={ref}
115
+ id={formDescriptionId}
116
+ className={cn("text-sm text-muted-foreground", className)}
117
+ {...props}
118
+ />
119
+ );
120
+ });
121
+
122
+ export const FormMessage = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
123
+ function FormMessage({ className, children, ...props }, ref) {
124
+ const { error, formMessageId } = useFormField();
125
+ const body = error ? String(error?.message ?? "") : children;
126
+ if (!body) return null;
127
+ return (
128
+ <p
129
+ ref={ref}
130
+ id={formMessageId}
131
+ className={cn(
132
+ "text-sm font-medium text-destructive animate-in fade-in slide-in-from-top-1 ease-entrance",
133
+ className,
134
+ )}
135
+ {...props}
136
+ >
137
+ {body}
138
+ </p>
139
+ );
140
+ },
141
+ );
@@ -0,0 +1,10 @@
1
+ export {
2
+ Form,
3
+ FormField,
4
+ FormItem,
5
+ FormLabel,
6
+ FormControl,
7
+ FormDescription,
8
+ FormMessage,
9
+ useFormField,
10
+ } from "./form";