@boxcustodia/library 2.0.0-alpha.11 → 2.0.0-alpha.12

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 (349) hide show
  1. package/dist/index.css +1 -1
  2. package/package.json +4 -3
  3. package/src/__doc__/Changelog.mdx +6 -0
  4. package/src/__doc__/Components.mdx +73 -0
  5. package/src/__doc__/Examples.tsx +69 -0
  6. package/src/__doc__/Icons.mdx +41 -0
  7. package/src/__doc__/Intro.mdx +138 -0
  8. package/src/__doc__/MCP.mdx +71 -0
  9. package/src/__doc__/Migration.mdx +475 -0
  10. package/src/__doc__/Theme.mdx +132 -0
  11. package/src/__doc__/Types.mdx +252 -0
  12. package/src/components/alert/alert.stories.tsx +142 -0
  13. package/src/components/alert/alert.tsx +109 -0
  14. package/src/components/alert/index.ts +7 -0
  15. package/src/components/alert-dialog/alert-dialog.stories.tsx +173 -0
  16. package/src/components/alert-dialog/alert-dialog.test.tsx +49 -0
  17. package/src/components/alert-dialog/alert-dialog.tsx +265 -0
  18. package/src/components/alert-dialog/index.ts +1 -0
  19. package/src/components/auto-complete/auto-complete-primitives.tsx +155 -0
  20. package/src/components/auto-complete/auto-complete.stories.tsx +241 -0
  21. package/src/components/auto-complete/auto-complete.tsx +82 -0
  22. package/src/components/auto-complete/index.ts +2 -0
  23. package/src/components/avatar/avatar.stories.tsx +84 -0
  24. package/src/components/avatar/avatar.test.tsx +61 -0
  25. package/src/components/avatar/avatar.tsx +104 -0
  26. package/src/components/avatar/index.ts +1 -0
  27. package/src/components/background-image/background-image.stories.tsx +21 -0
  28. package/src/components/background-image/background-image.test.tsx +29 -0
  29. package/src/components/background-image/background-image.tsx +23 -0
  30. package/src/components/background-image/index.ts +1 -0
  31. package/src/components/button/button.stories.tsx +396 -0
  32. package/src/components/button/button.test.tsx +58 -0
  33. package/src/components/button/button.tsx +31 -0
  34. package/src/components/button/button.variants.ts +44 -0
  35. package/src/components/button/components/base-button.tsx +86 -0
  36. package/src/components/button/components/loader-overlay.tsx +21 -0
  37. package/src/components/button/components/loading-icon.tsx +47 -0
  38. package/src/components/button/index.ts +3 -0
  39. package/src/components/calendar/calendar.model.ts +86 -0
  40. package/src/components/calendar/calendar.stories.tsx +155 -0
  41. package/src/components/calendar/calendar.test.tsx +12 -0
  42. package/src/components/calendar/calendar.tsx +185 -0
  43. package/src/components/calendar/components/calendar-navigation.tsx +141 -0
  44. package/src/components/calendar/components/day.tsx +61 -0
  45. package/src/components/calendar/components/decade-view.tsx +45 -0
  46. package/src/components/calendar/components/index.ts +6 -0
  47. package/src/components/calendar/components/month-view.tsx +58 -0
  48. package/src/components/calendar/components/week-days.tsx +27 -0
  49. package/src/components/calendar/components/year-view.tsx +29 -0
  50. package/src/components/calendar/hooks/index.ts +4 -0
  51. package/src/components/calendar/hooks/use-calendar-navigation.ts +79 -0
  52. package/src/components/calendar/hooks/use-calendar.ts +90 -0
  53. package/src/components/calendar/hooks/use-multiple-calendar.ts +34 -0
  54. package/src/components/calendar/hooks/use-range-calendar.ts +91 -0
  55. package/src/components/calendar/hooks/use-single-calendar.ts +18 -0
  56. package/src/components/calendar/index.ts +1 -0
  57. package/src/components/calendar/utils/typeguards.ts +7 -0
  58. package/src/components/card/card.stories.tsx +116 -0
  59. package/src/components/card/card.tsx +74 -0
  60. package/src/components/card/index.ts +1 -0
  61. package/src/components/center/center.stories.tsx +81 -0
  62. package/src/components/center/center.tsx +24 -0
  63. package/src/components/center/index.ts +1 -0
  64. package/src/components/checkbox/checkbox.stories.tsx +307 -0
  65. package/src/components/checkbox/checkbox.tsx +273 -0
  66. package/src/components/checkbox/index.ts +1 -0
  67. package/src/components/checkbox-group/checkbox-group.stories.tsx +104 -0
  68. package/src/components/checkbox-group/checkbox-group.tsx +16 -0
  69. package/src/components/checkbox-group/index.ts +1 -0
  70. package/src/components/combobox/combobox.stories.tsx +339 -0
  71. package/src/components/combobox/combobox.tsx +892 -0
  72. package/src/components/combobox/index.ts +1 -0
  73. package/src/components/date-picker/date-input.stories.tsx +158 -0
  74. package/src/components/date-picker/date-input.tsx +163 -0
  75. package/src/components/date-picker/date-picker.model.ts +90 -0
  76. package/src/components/date-picker/date-picker.stories.tsx +200 -0
  77. package/src/components/date-picker/date-picker.test.tsx +23 -0
  78. package/src/components/date-picker/date-picker.tsx +298 -0
  79. package/src/components/date-picker/date-picker.utils.ts +260 -0
  80. package/src/components/date-picker/index.ts +3 -0
  81. package/src/components/date-picker/use-date-input-popover.ts +48 -0
  82. package/src/components/date-picker/use-date-input.ts +125 -0
  83. package/src/components/dialog/dialog.stories.tsx +171 -0
  84. package/src/components/dialog/dialog.test.tsx +68 -0
  85. package/src/components/dialog/dialog.tsx +277 -0
  86. package/src/components/dialog/index.ts +1 -0
  87. package/src/components/divider/divider.stories.tsx +70 -0
  88. package/src/components/divider/divider.test.tsx +22 -0
  89. package/src/components/divider/divider.tsx +23 -0
  90. package/src/components/divider/index.ts +1 -0
  91. package/src/components/dropzone/dropzone.stories.tsx +210 -0
  92. package/src/components/dropzone/dropzone.tsx +154 -0
  93. package/src/components/dropzone/file-types.ts +64 -0
  94. package/src/components/dropzone/index.ts +3 -0
  95. package/src/components/dropzone/upload-primitives.tsx +310 -0
  96. package/src/components/dropzone/use-dropzone.ts +122 -0
  97. package/src/components/empty-state/empty-state.stories.tsx +56 -0
  98. package/src/components/empty-state/empty-state.tsx +39 -0
  99. package/src/components/empty-state/index.ts +1 -0
  100. package/src/components/field/field.stories.tsx +223 -0
  101. package/src/components/field/field.tsx +229 -0
  102. package/src/components/field/index.ts +1 -0
  103. package/src/components/form/form.stories.tsx +594 -0
  104. package/src/components/form/form.tsx +30 -0
  105. package/src/components/form/index.ts +1 -0
  106. package/src/components/heading/heading.stories.tsx +74 -0
  107. package/src/components/heading/heading.tsx +28 -0
  108. package/src/components/heading/heading.variants.ts +27 -0
  109. package/src/components/heading/index.ts +1 -0
  110. package/src/components/index.ts +46 -0
  111. package/src/components/input/index.ts +1 -0
  112. package/src/components/input/input.stories.tsx +104 -0
  113. package/src/components/input/input.tsx +75 -0
  114. package/src/components/kbd/index.ts +1 -0
  115. package/src/components/kbd/kbd.stories.tsx +40 -0
  116. package/src/components/kbd/kbd.tsx +31 -0
  117. package/src/components/kbd/kbd.variants.ts +26 -0
  118. package/src/components/label/index.ts +1 -0
  119. package/src/components/label/label.stories.tsx +68 -0
  120. package/src/components/label/label.test.tsx +61 -0
  121. package/src/components/label/label.tsx +62 -0
  122. package/src/components/loader/index.ts +1 -0
  123. package/src/components/loader/loader.stories.tsx +60 -0
  124. package/src/components/loader/loader.test.tsx +26 -0
  125. package/src/components/loader/loader.tsx +60 -0
  126. package/src/components/menu/index.ts +2 -0
  127. package/src/components/menu/menu-primitives.tsx +248 -0
  128. package/src/components/menu/menu.stories.tsx +203 -0
  129. package/src/components/menu/menu.tsx +100 -0
  130. package/src/components/menu/util/render-menu-item.tsx +54 -0
  131. package/src/components/multi-select/hooks/use-multi-select.ts +66 -0
  132. package/src/components/multi-select/index.ts +1 -0
  133. package/src/components/multi-select/multi-select.stories.tsx +294 -0
  134. package/src/components/multi-select/multi-select.tsx +300 -0
  135. package/src/components/multi-select/multi-select.variants.ts +22 -0
  136. package/src/components/number-input/index.ts +1 -0
  137. package/src/components/number-input/number-input.stories.tsx +209 -0
  138. package/src/components/number-input/number-input.test.tsx +87 -0
  139. package/src/components/number-input/number-input.tsx +230 -0
  140. package/src/components/pagination/components/pagination-option.tsx +27 -0
  141. package/src/components/pagination/index.ts +1 -0
  142. package/src/components/pagination/pagination.stories.tsx +80 -0
  143. package/src/components/pagination/pagination.test.tsx +76 -0
  144. package/src/components/pagination/pagination.tsx +102 -0
  145. package/src/components/password/index.ts +1 -0
  146. package/src/components/password/password.stories.tsx +104 -0
  147. package/src/components/password/password.tsx +71 -0
  148. package/src/components/popover/index.ts +1 -0
  149. package/src/components/popover/popover.stories.tsx +213 -0
  150. package/src/components/popover/popover.tsx +203 -0
  151. package/src/components/progress/index.ts +1 -0
  152. package/src/components/progress/progress.stories.tsx +124 -0
  153. package/src/components/progress/progress.test.tsx +25 -0
  154. package/src/components/progress/progress.tsx +124 -0
  155. package/src/components/scroll-area/index.ts +1 -0
  156. package/src/components/scroll-area/scroll-area.stories.tsx +166 -0
  157. package/src/components/scroll-area/scroll-area.tsx +64 -0
  158. package/src/components/select/index.ts +1 -0
  159. package/src/components/select/select.stories.tsx +253 -0
  160. package/src/components/select/select.tsx +430 -0
  161. package/src/components/show/index.ts +1 -0
  162. package/src/components/show/show.stories.tsx +197 -0
  163. package/src/components/show/show.test.tsx +41 -0
  164. package/src/components/show/show.tsx +16 -0
  165. package/src/components/skeleton/index.ts +1 -0
  166. package/src/components/skeleton/skeleton.stories.tsx +36 -0
  167. package/src/components/skeleton/skeleton.test.tsx +14 -0
  168. package/src/components/skeleton/skeleton.tsx +15 -0
  169. package/src/components/stack/index.ts +1 -0
  170. package/src/components/stack/stack.stories.tsx +194 -0
  171. package/src/components/stack/stack.tsx +52 -0
  172. package/src/components/stepper/Stepper.tsx +190 -0
  173. package/src/components/stepper/context/stepper-context.tsx +11 -0
  174. package/src/components/stepper/index.ts +1 -0
  175. package/src/components/stepper/stepper.stories.tsx +130 -0
  176. package/src/components/stepper/stepper.test.tsx +91 -0
  177. package/src/components/switch/index.ts +1 -0
  178. package/src/components/switch/switch.stories.tsx +122 -0
  179. package/src/components/switch/switch.test.tsx +30 -0
  180. package/src/components/switch/switch.tsx +86 -0
  181. package/src/components/table/index.ts +3 -0
  182. package/src/components/table/table-primitives.tsx +122 -0
  183. package/src/components/table/table.model.ts +20 -0
  184. package/src/components/table/table.stories.tsx +169 -0
  185. package/src/components/table/table.test.tsx +91 -0
  186. package/src/components/table/table.tsx +109 -0
  187. package/src/components/table-pagination/index.ts +2 -0
  188. package/src/components/table-pagination/table-pagination.model.ts +2 -0
  189. package/src/components/table-pagination/table-pagination.stories.tsx +23 -0
  190. package/src/components/table-pagination/table-pagination.test.tsx +32 -0
  191. package/src/components/table-pagination/table-pagination.tsx +108 -0
  192. package/src/components/tabs/context/tabs-context.tsx +14 -0
  193. package/src/components/tabs/index.ts +1 -0
  194. package/src/components/tabs/tabs.stories.tsx +182 -0
  195. package/src/components/tabs/tabs.test.tsx +61 -0
  196. package/src/components/tabs/tabs.tsx +175 -0
  197. package/src/components/tag/index.ts +2 -0
  198. package/src/components/tag/tag.stories.tsx +170 -0
  199. package/src/components/tag/tag.test.tsx +18 -0
  200. package/src/components/tag/tag.tsx +99 -0
  201. package/src/components/tag/tag.variants.ts +31 -0
  202. package/src/components/textarea/index.ts +1 -0
  203. package/src/components/textarea/textarea.stories.tsx +73 -0
  204. package/src/components/textarea/textarea.tsx +105 -0
  205. package/src/components/timeline/index.ts +1 -0
  206. package/src/components/timeline/timeline-status.ts +5 -0
  207. package/src/components/timeline/timeline.stories.tsx +84 -0
  208. package/src/components/timeline/timeline.tsx +147 -0
  209. package/src/components/toast/index.ts +1 -0
  210. package/src/components/toast/toast.stories.tsx +392 -0
  211. package/src/components/toast/toast.test.tsx +50 -0
  212. package/src/components/toast/toast.tsx +411 -0
  213. package/src/components/tooltip/index.ts +1 -0
  214. package/src/components/tooltip/tooltip.stories.tsx +226 -0
  215. package/src/components/tooltip/tooltip.test.tsx +46 -0
  216. package/src/components/tooltip/tooltip.tsx +171 -0
  217. package/src/components/tree/hooks/use-controllable-tree-state.ts +80 -0
  218. package/src/components/tree/index.ts +2 -0
  219. package/src/components/tree/tree-primitives.tsx +126 -0
  220. package/src/components/tree/tree.stories.tsx +468 -0
  221. package/src/components/tree/tree.tsx +42 -0
  222. package/src/hooks/index.ts +26 -0
  223. package/src/hooks/useArray/__doc__/useArray.stories.tsx +100 -0
  224. package/src/hooks/useArray/__test__/useArray.test.tsx +88 -0
  225. package/src/hooks/useArray/index.ts +1 -0
  226. package/src/hooks/useArray/useArray.ts +76 -0
  227. package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +149 -0
  228. package/src/hooks/useAsync/__test__/useAsync.test.tsx +68 -0
  229. package/src/hooks/useAsync/index.ts +1 -0
  230. package/src/hooks/useAsync/useAsync.ts +58 -0
  231. package/src/hooks/useClickOutside/__doc__/useClickOutside.stories.tsx +40 -0
  232. package/src/hooks/useClickOutside/__test__/useClickOutside.test.tsx +33 -0
  233. package/src/hooks/useClickOutside/index.ts +1 -0
  234. package/src/hooks/useClickOutside/useClickOutside.ts +26 -0
  235. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +45 -0
  236. package/src/hooks/useClipboard/__test__/useClipboard.test.tsx +19 -0
  237. package/src/hooks/useClipboard/index.ts +1 -0
  238. package/src/hooks/useClipboard/useClipboard.tsx +28 -0
  239. package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +84 -0
  240. package/src/hooks/useDebounceCallback/index.ts +1 -0
  241. package/src/hooks/useDebounceCallback/useDebouncedCallback.ts +23 -0
  242. package/src/hooks/useDebounceValue/__doc__/useDebouncedValue.stories.tsx +75 -0
  243. package/src/hooks/useDebounceValue/index.ts +1 -0
  244. package/src/hooks/useDebounceValue/useDebouncedValue.ts +17 -0
  245. package/src/hooks/useDisclosure/__doc__/useDisclosure.stories.tsx +39 -0
  246. package/src/hooks/useDisclosure/__test__/useDisclosure.test.ts +43 -0
  247. package/src/hooks/useDisclosure/index.ts +1 -0
  248. package/src/hooks/useDisclosure/useDisclosure.ts +37 -0
  249. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +26 -0
  250. package/src/hooks/useDocumentTitle/index.ts +1 -0
  251. package/src/hooks/useDocumentTitle/useDocumentTitle.tsx +11 -0
  252. package/src/hooks/useEventListener/__doc__/useEventListener.stories.tsx +28 -0
  253. package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +26 -0
  254. package/src/hooks/useEventListener/index.ts +1 -0
  255. package/src/hooks/useEventListener/useEventListener.ts +25 -0
  256. package/src/hooks/useFocusTrap/__doc__/useFocusTrap.stories.tsx +37 -0
  257. package/src/hooks/useFocusTrap/index.ts +1 -0
  258. package/src/hooks/useFocusTrap/scopeTab.ts +38 -0
  259. package/src/hooks/useFocusTrap/tabbable.ts +70 -0
  260. package/src/hooks/useFocusTrap/useFocusTrap.ts +78 -0
  261. package/src/hooks/useHotkey/__docs__/useHotkey.stories.tsx +116 -0
  262. package/src/hooks/useHotkey/__test__/useHotkey.test.tsx +105 -0
  263. package/src/hooks/useHotkey/__utils__/create-hotkey-listener.ts +25 -0
  264. package/src/hooks/useHotkey/__utils__/index.ts +3 -0
  265. package/src/hooks/useHotkey/__utils__/is-input-field.ts +14 -0
  266. package/src/hooks/useHotkey/__utils__/match-key-modifiers.ts +25 -0
  267. package/src/hooks/useHotkey/index.ts +1 -0
  268. package/src/hooks/useHotkey/useHotkey.ts +34 -0
  269. package/src/hooks/useHover/__doc__/useHover.stories.tsx +41 -0
  270. package/src/hooks/useHover/__test__/useHover.test.tsx +45 -0
  271. package/src/hooks/useHover/index.ts +1 -0
  272. package/src/hooks/useHover/useHover.tsx +40 -0
  273. package/src/hooks/useIsVisible/__doc__/useIsVisible.stories.tsx +60 -0
  274. package/src/hooks/useIsVisible/index.ts +1 -0
  275. package/src/hooks/useIsVisible/useIsVisible.tsx +50 -0
  276. package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +86 -0
  277. package/src/hooks/useLocalStorage/__test__/useLocalStorage.test.ts +85 -0
  278. package/src/hooks/useLocalStorage/index.ts +1 -0
  279. package/src/hooks/useLocalStorage/useLocalStorage.ts +57 -0
  280. package/src/hooks/useMediaQuery/__doc__/useMediaQuery.stories.tsx +39 -0
  281. package/src/hooks/useMediaQuery/index.ts +1 -0
  282. package/src/hooks/useMediaQuery/useMediaQuery.ts +22 -0
  283. package/src/hooks/useMemoizedFn/index.ts +1 -0
  284. package/src/hooks/useMemoizedFn/useMemoizedFn.ts +32 -0
  285. package/src/hooks/useMutation/__doc__/useMutation.stories.tsx +111 -0
  286. package/src/hooks/useMutation/__test__/useMutation.test.tsx +83 -0
  287. package/src/hooks/useMutation/index.ts +1 -0
  288. package/src/hooks/useMutation/useMutation.tsx +60 -0
  289. package/src/hooks/useObject/__doc__/useObject.stories.tsx +119 -0
  290. package/src/hooks/useObject/__test__/useObject.test.tsx +87 -0
  291. package/src/hooks/useObject/index.ts +1 -0
  292. package/src/hooks/useObject/useObject.tsx +48 -0
  293. package/src/hooks/usePagination/__doc__/usePagination.stories.tsx +72 -0
  294. package/src/hooks/usePagination/__test__/usePagination.test.tsx +98 -0
  295. package/src/hooks/usePagination/index.ts +2 -0
  296. package/src/hooks/usePagination/usePagination.tsx +74 -0
  297. package/src/hooks/usePortal/__doc__/usePortal.stories.tsx +19 -0
  298. package/src/hooks/usePortal/__test__/usePortal.test.tsx +20 -0
  299. package/src/hooks/usePortal/index.ts +1 -0
  300. package/src/hooks/usePortal/usePortal.ts +40 -0
  301. package/src/hooks/usePreventCloseWindow/__doc__/usePreventCloseWindow.stories.tsx +32 -0
  302. package/src/hooks/usePreventCloseWindow/index.ts +1 -0
  303. package/src/hooks/usePreventCloseWindow/usePreventCloseWindow.ts +33 -0
  304. package/src/hooks/useRangePagination/__test__/useRangePagination.test.tsx +63 -0
  305. package/src/hooks/useRangePagination/index.ts +2 -0
  306. package/src/hooks/useRangePagination/useRangePagination.tsx +72 -0
  307. package/src/hooks/useSelection/__doc__/useSelection.stories.tsx +140 -0
  308. package/src/hooks/useSelection/__test__/useSelection.test.tsx +57 -0
  309. package/src/hooks/useSelection/index.ts +1 -0
  310. package/src/hooks/useSelection/useSelection.ts +121 -0
  311. package/src/hooks/useStep/__doc__/useStep.stories.tsx +98 -0
  312. package/src/hooks/useStep/__test__/useStep.test.ts +51 -0
  313. package/src/hooks/useStep/index.ts +1 -0
  314. package/src/hooks/useStep/useStep.ts +57 -0
  315. package/src/hooks/useToggle/__doc__/useToggle.stories.tsx +25 -0
  316. package/src/hooks/useToggle/__test__/useToggle.test.tsx +43 -0
  317. package/src/hooks/useToggle/index.ts +1 -0
  318. package/src/hooks/useToggle/useToggle.ts +16 -0
  319. package/src/index.ts +6 -0
  320. package/src/lib/cn.ts +8 -0
  321. package/src/lib/index.ts +1 -0
  322. package/src/models/Generic.model.ts +67 -0
  323. package/src/models/index.ts +1 -0
  324. package/src/providers/index.ts +2 -0
  325. package/src/providers/library-provider.tsx +44 -0
  326. package/src/providers/theme/ThemeProvider.tsx +25 -0
  327. package/src/providers/theme/index.ts +3 -0
  328. package/src/providers/theme/types.ts +11 -0
  329. package/src/providers/theme/useThemeProps.ts +25 -0
  330. package/src/stores/theme.store.ts +31 -0
  331. package/src/styles/components.css +4 -0
  332. package/src/styles/index.css +2 -0
  333. package/src/styles/library.css +2 -0
  334. package/src/styles/theme.css +232 -0
  335. package/src/utils/dates/parseDateRange.utility.ts +39 -0
  336. package/src/utils/form.tsx +91 -0
  337. package/src/utils/functions/createSafeContext.ts +17 -0
  338. package/src/utils/functions/ensureReactElement.tsx +30 -0
  339. package/src/utils/functions/getFormData.ts +19 -0
  340. package/src/utils/functions/index.ts +4 -0
  341. package/src/utils/functions/mergeRefs.ts +18 -0
  342. package/src/utils/index.ts +3 -0
  343. package/src/utils/strings/extractInitials.utility.ts +10 -0
  344. package/src/utils/strings/index.ts +1 -0
  345. package/src/utils/tests/click.ts +3 -0
  346. package/src/utils/tests/index.ts +2 -0
  347. package/src/utils/tests/keyboard.ts +21 -0
  348. package/src/utils/tests/type.ts +6 -0
  349. package/dist/components.css +0 -2
@@ -0,0 +1,594 @@
1
+ import { zodResolver } from "@hookform/resolvers/zod";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import type { ReactNode } from "react";
4
+ import { useState } from "react";
5
+ import {
6
+ type FieldValues,
7
+ type SubmitHandler,
8
+ type UseFormProps,
9
+ type UseFormReturn,
10
+ useController,
11
+ useForm,
12
+ } from "react-hook-form";
13
+ import { z } from "zod";
14
+ import { Button } from "../button/button";
15
+ import { Checkbox } from "../checkbox/checkbox";
16
+ import { Combobox } from "../combobox";
17
+ import { DateInput } from "../date-picker";
18
+ import { Field, FieldError, FieldLabel, FieldRoot } from "../field/field";
19
+ import { Input } from "../input/input";
20
+ import { NumberInput } from "../number-input";
21
+ import { PasswordRoot } from "../password/password";
22
+ import { Select } from "../select";
23
+ import { Textarea } from "../textarea/textarea";
24
+ import { toast } from "../toast";
25
+ import { Form, FormPrimitive } from "./form";
26
+
27
+ /**
28
+ * Thin wrapper around Base UI Form. `onSubmit` receives `(data, event)` —
29
+ * `data` is a `Record<string, unknown>` parsed from `FormData`, `event` is
30
+ * the native submit event. `preventDefault` is called automatically.
31
+ * `errors` propagates server-side messages to fields by `name`.
32
+ * `validationMode` lives on `FieldRoot`, not on Form — set it per-field.
33
+ *
34
+ * Reference: [Forms – Base UI](https://base-ui.com/react/handbook/forms)
35
+ */
36
+ const meta: Meta<typeof Form> = {
37
+ title: "Components/Form",
38
+ component: Form,
39
+ parameters: { layout: "centered" },
40
+ argTypes: {
41
+ children: { control: false },
42
+ onSubmit: { control: false },
43
+ actionsRef: { control: false },
44
+ errors: { control: false },
45
+ },
46
+ };
47
+
48
+ export default meta;
49
+ type Story = StoryObj<typeof Form>;
50
+
51
+ const NOTIFICATION_CHANNELS = [
52
+ { label: "Email", value: "email" },
53
+ { label: "SMS", value: "sms" },
54
+ { label: "Push", value: "push" },
55
+ { label: "Slack", value: "slack" },
56
+ ];
57
+
58
+ const ROLES = [
59
+ { label: "Developer", value: "developer" },
60
+ { label: "Designer", value: "designer" },
61
+ { label: "Manager", value: "manager" },
62
+ { label: "QA Engineer", value: "qa" },
63
+ ];
64
+ /**
65
+ * Native HTML constraint validation. Each `FieldRoot` carries a `name` so Form
66
+ * can route server errors automatically. Errors only surface after the first
67
+ * submit attempt — no premature invalid state.
68
+ */
69
+ export const Default: Story = {
70
+ render: () => {
71
+ const [done, setDone] = useState(false);
72
+
73
+ return (
74
+ <Form className="flex w-80 flex-col gap-4" onSubmit={() => setDone(true)}>
75
+ <Field
76
+ name="email"
77
+ label="Email"
78
+ tooltip="We will never share your email."
79
+ error={[
80
+ { message: "Email is required.", match: "valueMissing" },
81
+ { message: "Enter a valid email address.", match: "typeMismatch" },
82
+ ]}
83
+ >
84
+ <Input type="email" required placeholder="you@example.com" />
85
+ </Field>
86
+
87
+ <Field
88
+ name="username"
89
+ label="Username"
90
+ error={[
91
+ { message: "Username is required.", match: "valueMissing" },
92
+ { message: "Username must be 3–15 characters.", match: "tooShort" },
93
+ {
94
+ message: "Only letters, numbers, and underscores.",
95
+ match: "patternMismatch",
96
+ },
97
+ ]}
98
+ >
99
+ <Input
100
+ required
101
+ pattern="[a-zA-Z0-9_]+"
102
+ minLength={3}
103
+ maxLength={15}
104
+ placeholder="e.g. jane_doe"
105
+ />
106
+ </Field>
107
+
108
+ <Field
109
+ name="role"
110
+ label="Role"
111
+ error={[{ message: "Role is required.", match: "valueMissing" }]}
112
+ >
113
+ <Select items={ROLES} placeholder="Select a role…" required />
114
+ </Field>
115
+
116
+ <Field
117
+ name="experience"
118
+ label="Years of experience"
119
+ description="Must be between 0 and 50."
120
+ error={[
121
+ { message: "This field is required.", match: "valueMissing" },
122
+ { message: "Value must be 0 or more.", match: "rangeUnderflow" },
123
+ { message: "Value cannot exceed 50.", match: "rangeOverflow" },
124
+ ]}
125
+ >
126
+ <NumberInput required min={0} max={50} />
127
+ </Field>
128
+
129
+ <Field
130
+ name="startDate"
131
+ label="Start date"
132
+ error={{ message: "Start date is required.", match: "valueMissing" }}
133
+ >
134
+ <DateInput required />
135
+ </Field>
136
+
137
+ <Field
138
+ name="notifications"
139
+ label="Notification channels"
140
+ required
141
+ error={[
142
+ { message: "Select at least one channel.", match: "valueMissing" },
143
+ ]}
144
+ >
145
+ <Combobox
146
+ items={NOTIFICATION_CHANNELS}
147
+ defaultValue={[NOTIFICATION_CHANNELS[0]]}
148
+ placeholder="Select channels…"
149
+ multiple
150
+ required
151
+ />
152
+ </Field>
153
+
154
+ <Field
155
+ name="password"
156
+ label="Password"
157
+ error={{ message: "Password is required.", match: "valueMissing" }}
158
+ >
159
+ <PasswordRoot required placeholder="Enter your password" />
160
+ </Field>
161
+
162
+ <Field
163
+ name="bio"
164
+ label="Bio"
165
+ tooltip="Between 20 and 300 characters."
166
+ error={[
167
+ { message: "Bio is required.", match: "valueMissing" },
168
+ {
169
+ message: "Bio must be at least 20 characters.",
170
+ match: "tooShort",
171
+ },
172
+ {
173
+ message: "Bio must be 300 characters or less.",
174
+ match: "tooLong",
175
+ },
176
+ ]}
177
+ >
178
+ <Textarea
179
+ required
180
+ minLength={10}
181
+ maxLength={300}
182
+ placeholder="Tell us a bit about yourself…"
183
+ />
184
+ </Field>
185
+
186
+ <Field
187
+ name="terms"
188
+ inline
189
+ label="I accept the terms and conditions"
190
+ error={{
191
+ message: "You must accept the terms.",
192
+ match: "valueMissing",
193
+ }}
194
+ >
195
+ <Checkbox required />
196
+ </Field>
197
+
198
+ {done && <p className="text-sm text-green-600">Account created!</p>}
199
+
200
+ <Button type="submit">Create account</Button>
201
+ </Form>
202
+ );
203
+ },
204
+ };
205
+
206
+ /**
207
+ * `onSubmit` receives parsed `FormData` and is the right place for cross-field
208
+ * and custom-rule validation. Errors are stored in state and forwarded via
209
+ * `Form.errors`, which routes each message to the matching field by `name` and
210
+ * clears it automatically as soon as the user modifies that field.
211
+ *
212
+ * Native browser validation (email format, required) runs first — `onSubmit`
213
+ * only fires after all native constraints pass.
214
+ */
215
+ export const OnSubmitValidation: Story = {
216
+ render: () => {
217
+ const [errors, setErrors] = useState<Record<string, string>>({});
218
+
219
+ return (
220
+ <Form
221
+ className="flex w-80 flex-col gap-4"
222
+ errors={errors}
223
+ onSubmit={(data) => {
224
+ const next: Record<string, string> = {};
225
+ const pwd = data.password as string;
226
+
227
+ if (!/[A-Z]/.test(pwd))
228
+ next.password = "Must contain at least 1 uppercase letter.";
229
+ else if (!/[a-z]/.test(pwd))
230
+ next.password = "Must contain at least 1 lowercase letter.";
231
+ else if (!/\d/.test(pwd))
232
+ next.password = "Must contain at least 1 number.";
233
+
234
+ if (data.password !== data.confirmPassword)
235
+ next.confirmPassword = "Passwords must match.";
236
+
237
+ if (Object.keys(next).length > 0) {
238
+ setErrors(next);
239
+ return;
240
+ }
241
+
242
+ setErrors({});
243
+ }}
244
+ >
245
+ <Field
246
+ name="email"
247
+ label="Email"
248
+ error={[
249
+ { message: "Email is required.", match: "valueMissing" },
250
+ { message: "Enter a valid email address.", match: "typeMismatch" },
251
+ ]}
252
+ >
253
+ <Input type="email" required placeholder="you@example.com" />
254
+ </Field>
255
+
256
+ <Field
257
+ name="password"
258
+ label="Password"
259
+ error={{ message: "Password is required.", match: "valueMissing" }}
260
+ >
261
+ <PasswordRoot required placeholder="Enter your password" />
262
+ </Field>
263
+
264
+ <Field
265
+ name="confirmPassword"
266
+ label="Repeat password"
267
+ error={{
268
+ message: "Repeat password is required.",
269
+ match: "valueMissing",
270
+ }}
271
+ >
272
+ <PasswordRoot required placeholder="Repeat your password" />
273
+ </Field>
274
+
275
+ <Button type="submit">Create account</Button>
276
+ </Form>
277
+ );
278
+ },
279
+ };
280
+
281
+ /**
282
+ * `validationMode` is a `FieldRoot` prop, not a Form prop — set it per-field.
283
+ * `"onChange"` validates on every keystroke. `"onBlur"` validates when focus leaves.
284
+ * Default is `"onSubmit"`.
285
+ */
286
+ export const ValidateOnChange: Story = {
287
+ render: () => (
288
+ <Form className="flex w-80 flex-col gap-4">
289
+ <Field
290
+ name="email"
291
+ label="Email"
292
+ required
293
+ validationMode="onChange"
294
+ error={[
295
+ { message: "Email is required.", match: "valueMissing" },
296
+ { message: "Enter a valid email address.", match: "typeMismatch" },
297
+ ]}
298
+ >
299
+ <Input type="email" required placeholder="you@example.com" />
300
+ </Field>
301
+ <Button type="submit">Submit</Button>
302
+ </Form>
303
+ ),
304
+ };
305
+
306
+ /**
307
+ * `onSubmit(data, event)` — `data` is parsed from `FormData`, `event` is the
308
+ * native submit event. `preventDefault` is already called by the Form wrapper.
309
+ */
310
+ export const OnFormSubmit: Story = {
311
+ render: () => {
312
+ const [values, setValues] = useState<Record<string, unknown> | null>(null);
313
+
314
+ return (
315
+ <Form
316
+ className="flex w-80 flex-col gap-4"
317
+ onSubmit={(data) => setValues(data)}
318
+ >
319
+ <Field name="email" label="Email">
320
+ <Input type="email" required placeholder="you@example.com" />
321
+ </Field>
322
+ <Field name="username" label="Username">
323
+ <Input required placeholder="e.g. jane_doe" />
324
+ </Field>
325
+ <Button type="submit">Submit</Button>
326
+ {values && (
327
+ <pre className="rounded-md bg-muted p-3 text-xs">
328
+ {JSON.stringify(values, null, 2)}
329
+ </pre>
330
+ )}
331
+ </Form>
332
+ );
333
+ },
334
+ };
335
+
336
+ /**
337
+ * `FormPrimitive` + `FieldRoot` primitives for full structural control.
338
+ * Use when you need custom ordering or elements between parts.
339
+ *
340
+ * ```tsx
341
+ * <FormPrimitive>
342
+ * <FieldRoot name="email">
343
+ * <FieldLabel>Email</FieldLabel>
344
+ * <Input type="email" required />
345
+ * <FieldError match="valueMissing">Required.</FieldError>
346
+ * </FieldRoot>
347
+ * </FormPrimitive>
348
+ * ```
349
+ */
350
+ export const Primitive: Story = {
351
+ render: () => (
352
+ <FormPrimitive
353
+ className="flex w-80 flex-col gap-4"
354
+ onSubmit={(e) => e.preventDefault()}
355
+ >
356
+ <FieldRoot name="email">
357
+ <FieldLabel>Email</FieldLabel>
358
+ <Input type="email" required placeholder="you@example.com" />
359
+ <FieldError match="valueMissing">Email is required.</FieldError>
360
+ <FieldError match="typeMismatch">
361
+ Enter a valid email address.
362
+ </FieldError>
363
+ </FieldRoot>
364
+ <Button type="submit">Submit</Button>
365
+ </FormPrimitive>
366
+ ),
367
+ };
368
+
369
+ const CATEGORIES = [
370
+ { label: "Frontend", value: "frontend" },
371
+ { label: "Backend", value: "backend" },
372
+ { label: "DevOps", value: "devops" },
373
+ { label: "Design", value: "design" },
374
+ ];
375
+
376
+ const schema = z.object({
377
+ age: z.coerce
378
+ .number({ message: "Please enter a number." })
379
+ .positive({ message: "Number must be positive." }),
380
+ name: z.string().min(1, { message: "Please enter a name." }),
381
+ emoji: z.emoji({ message: "Please enter an emoji." }),
382
+ category: z.string().min(1, { message: "Select a category." }),
383
+ terms: z.literal(true, { message: "You must accept the terms." }),
384
+ });
385
+
386
+ type FormDataType = z.infer<typeof schema>;
387
+ type Errors = Partial<Record<keyof FormDataType, string[]>>;
388
+
389
+ export const UsingZod: Story = {
390
+ render: () => {
391
+ const [loading, setLoading] = useState(false);
392
+ const [errors, setErrors] = useState<Errors>({});
393
+
394
+ const onSubmit = async (data: any) => {
395
+ setLoading(true);
396
+
397
+ try {
398
+ const result = schema.safeParse(data);
399
+
400
+ if (!result.success) {
401
+ const { fieldErrors } = z.flattenError(result.error);
402
+ setErrors(fieldErrors as Errors);
403
+ return;
404
+ }
405
+
406
+ setErrors({});
407
+ await new Promise((r) => setTimeout(r, 800));
408
+
409
+ toast({
410
+ title: "Success",
411
+ description: (
412
+ <pre className="rounded-md bg-muted p-3 text-sm font-mono">
413
+ {JSON.stringify(result.data, null, 2)}
414
+ </pre>
415
+ ),
416
+ });
417
+ } finally {
418
+ setLoading(false);
419
+ }
420
+ };
421
+
422
+ return (
423
+ <Form
424
+ className="flex w-full max-w-64 flex-col gap-4"
425
+ errors={errors}
426
+ onSubmit={onSubmit}
427
+ >
428
+ <Field name="name" label="Name" required>
429
+ <Input placeholder="Enter name" />
430
+ </Field>
431
+
432
+ <Field name="age" label="Age" description="Must be positive." required>
433
+ <Input placeholder="Enter age" />
434
+ </Field>
435
+
436
+ <Field
437
+ name="emoji"
438
+ label="Emoji"
439
+ required
440
+ tooltip="What's your favorite emoji?"
441
+ >
442
+ <Input />
443
+ </Field>
444
+
445
+ <Field name="category" label="Category" required>
446
+ <Combobox items={CATEGORIES} placeholder="Select a category…" />
447
+ </Field>
448
+
449
+ <Field
450
+ name="terms"
451
+ inline
452
+ label="I accept the terms and conditions"
453
+ required
454
+ >
455
+ <Checkbox />
456
+ </Field>
457
+
458
+ <Button loading={loading} type="submit">
459
+ Submit
460
+ </Button>
461
+ </Form>
462
+ );
463
+ },
464
+ };
465
+
466
+ // ── React Hook Form ───────────────────────────────────────────────────────────
467
+
468
+ /**
469
+ * Integration with React Hook Form + Zod. The `RHFField` helper wraps `Field`
470
+ * and feeds `invalid`/`error` from `fieldState` — copy it into your project.
471
+ *
472
+ * ```tsx
473
+ * function RHFField({ name, label, description, rules, render }) {
474
+ * const { field, fieldState } = useController({ name, rules });
475
+ * return (
476
+ * <Field
477
+ * name={name}
478
+ * label={label}
479
+ * description={description}
480
+ * invalid={fieldState.invalid}
481
+ * error={fieldState.error?.message}
482
+ * >
483
+ * {render(field)}
484
+ * </Field>
485
+ * );
486
+ * }
487
+ * ```
488
+ */
489
+ export const WithReactHookForm: Story = {
490
+ render: () => {
491
+ const form = useRHFForm(FormSchema, {
492
+ defaultValues: { email: "", password: "" },
493
+ });
494
+
495
+ return (
496
+ <RHFForm
497
+ form={form}
498
+ onSubmit={() => {}}
499
+ className="flex w-80 flex-col gap-4"
500
+ >
501
+ <RHFField
502
+ name="email"
503
+ label="Email"
504
+ description="We'll never share your email."
505
+ render={(field) => (
506
+ <Input {...field} type="email" placeholder="you@example.com" />
507
+ )}
508
+ />
509
+
510
+ <RHFField
511
+ name="password"
512
+ label="Password"
513
+ render={(field) => (
514
+ <PasswordRoot {...field} placeholder="Enter your password" />
515
+ )}
516
+ />
517
+
518
+ {form.formState.isSubmitSuccessful && (
519
+ <p className="text-sm text-green-600">Account created!</p>
520
+ )}
521
+
522
+ <Button type="submit">Create account</Button>
523
+ </RHFForm>
524
+ );
525
+ },
526
+ };
527
+
528
+ // ── RHF helpers (not exported by the library — copy into your project) ────────
529
+
530
+ const FormSchema = z.object({
531
+ email: z
532
+ .string()
533
+ .min(1, "Email is required.")
534
+ .email("Enter a valid email address."),
535
+ password: z.string().min(1, "Password is required."),
536
+ });
537
+
538
+ function useRHFForm<T extends z.ZodType<FieldValues, FieldValues>>(
539
+ schema: T,
540
+ options?: Omit<UseFormProps<z.infer<T>>, "resolver">,
541
+ ) {
542
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
543
+ return useForm<z.infer<T>>({
544
+ resolver: zodResolver(schema as any),
545
+ ...options,
546
+ });
547
+ }
548
+
549
+ type RHFFormProps<T extends FieldValues> = {
550
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
551
+ form: UseFormReturn<T, any, any>;
552
+ onSubmit: SubmitHandler<T>;
553
+ children: ReactNode;
554
+ className?: string;
555
+ };
556
+
557
+ function RHFForm<T extends FieldValues>({
558
+ form,
559
+ onSubmit,
560
+ children,
561
+ ...props
562
+ }: RHFFormProps<T>) {
563
+ return (
564
+ <Form
565
+ // onSubmit={(data, event) => form.handleSubmit(onSubmit)(data, event)}
566
+ {...props}
567
+ >
568
+ {children}
569
+ </Form>
570
+ );
571
+ }
572
+
573
+ interface RHFFieldProps {
574
+ name: string;
575
+ label?: ReactNode;
576
+ description?: ReactNode;
577
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
578
+ render: (field: any) => ReactNode;
579
+ }
580
+
581
+ function RHFField({ name, label, description, render }: RHFFieldProps) {
582
+ const { field, fieldState } = useController({ name });
583
+ return (
584
+ <Field
585
+ name={name}
586
+ label={label}
587
+ description={description}
588
+ invalid={fieldState.invalid}
589
+ error={fieldState.error?.message}
590
+ >
591
+ {render(field)}
592
+ </Field>
593
+ );
594
+ }
@@ -0,0 +1,30 @@
1
+ import { Form as FormPrimitive } from "@base-ui/react/form";
2
+ import type React from "react";
3
+
4
+ type BaseProps = FormPrimitive.Props;
5
+
6
+ export interface FormProps
7
+ extends Omit<BaseProps, "onSubmit" | "onFormSubmit"> {
8
+ onSubmit?: BaseProps["onFormSubmit"];
9
+ onFormSubmit?: BaseProps["onFormSubmit"];
10
+ }
11
+
12
+ export function Form({
13
+ onSubmit,
14
+ onFormSubmit,
15
+ className,
16
+ ...props
17
+ }: FormProps): React.ReactElement {
18
+ const handleSubmit = onSubmit ?? onFormSubmit;
19
+
20
+ return (
21
+ <FormPrimitive
22
+ className={className}
23
+ onFormSubmit={handleSubmit}
24
+ data-slot="form"
25
+ {...props}
26
+ />
27
+ );
28
+ }
29
+
30
+ export { FormPrimitive };
@@ -0,0 +1 @@
1
+ export * from "./form";
@@ -0,0 +1,74 @@
1
+ import { faker } from "@faker-js/faker";
2
+ import { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { Heading } from "../../components";
4
+
5
+ const meta: Meta<typeof Heading> = {
6
+ title: "typography/Heading",
7
+ component: Heading,
8
+ args: {
9
+ children: faker.lorem.sentence(),
10
+ },
11
+ argTypes: {
12
+ children: {
13
+ control: false,
14
+ },
15
+ size: {
16
+ options: ["sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl"],
17
+ control: { type: "select" },
18
+ defaultValue: "xl",
19
+ description: "The font size of the heading. xl is the default.",
20
+ },
21
+ weight: {
22
+ options: ["normal", "medium", "semibold", "bold"],
23
+ control: { type: "select" },
24
+ defaultValue: "semibold",
25
+ description: "The font weight of the heading. semibold is the default.",
26
+ },
27
+ },
28
+ };
29
+
30
+ export default meta;
31
+
32
+ type Story = StoryObj<typeof Heading>;
33
+
34
+ export const Default: Story = {};
35
+
36
+ export const Sizes: Story = {
37
+ render: () => (
38
+ <>
39
+ <Heading size="sm">Heading sm (14px)</Heading>
40
+ <Heading size="md">Heading md (16px)</Heading>
41
+ <Heading size="lg">Heading lg (18px)</Heading>
42
+ <Heading size="xl">Heading xl (20px)</Heading>
43
+ <Heading size="2xl">Heading 2xl (24px)</Heading>
44
+ <Heading size="3xl">Heading 3xl (28px)</Heading>
45
+ <Heading size="4xl">Heading 4xl (32px)</Heading>
46
+ <Heading size="5xl">Heading 5xl (36px)</Heading>
47
+ <Heading size="6xl">Heading 6xl (40px)</Heading>
48
+ </>
49
+ ),
50
+ };
51
+
52
+ export const Weights: Story = {
53
+ render: () => (
54
+ <>
55
+ <Heading weight="normal">Heading normal</Heading>
56
+ <Heading weight="medium">Heading medium</Heading>
57
+ <Heading weight="semibold">Heading semibold</Heading>
58
+ <Heading weight="bold">Heading bold</Heading>
59
+ </>
60
+ ),
61
+ };
62
+
63
+ export const As: Story = {
64
+ render: () => (
65
+ <>
66
+ <Heading as="h1">Heading as h1</Heading>
67
+ <Heading as="h2">Heading as h2</Heading>
68
+ <Heading as="h3">Heading as h3</Heading>
69
+ <Heading as="h4">Heading as h4</Heading>
70
+ <Heading as="h5">Heading as h5</Heading>
71
+ <Heading as="h6">Heading as h6</Heading>
72
+ </>
73
+ ),
74
+ };