@arolariu/components 1.0.0 → 2.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 (451) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/EXAMPLES.md +2510 -0
  3. package/dist/components/ui/accordion.js +3 -3
  4. package/dist/components/ui/accordion.js.map +1 -1
  5. package/dist/components/ui/accordion_module.css.map +1 -1
  6. package/dist/components/ui/alert-dialog.d.ts +4 -16
  7. package/dist/components/ui/alert-dialog.d.ts.map +1 -1
  8. package/dist/components/ui/alert-dialog.js +21 -17
  9. package/dist/components/ui/alert-dialog.js.map +1 -1
  10. package/dist/components/ui/alert-dialog_module.css +1 -1
  11. package/dist/components/ui/alert-dialog_module.css.map +1 -1
  12. package/dist/components/ui/alert.js +4 -4
  13. package/dist/components/ui/alert.js.map +1 -1
  14. package/dist/components/ui/alert_module.css.map +1 -1
  15. package/dist/components/ui/aspect-ratio.js +2 -2
  16. package/dist/components/ui/aspect-ratio.js.map +1 -1
  17. package/dist/components/ui/aspect-ratio_module.css.map +1 -1
  18. package/dist/components/ui/async-boundary.js +2 -2
  19. package/dist/components/ui/async-boundary.js.map +1 -1
  20. package/dist/components/ui/avatar.d.ts +3 -12
  21. package/dist/components/ui/avatar.d.ts.map +1 -1
  22. package/dist/components/ui/avatar.js +18 -15
  23. package/dist/components/ui/avatar.js.map +1 -1
  24. package/dist/components/ui/avatar_module.css.map +1 -1
  25. package/dist/components/ui/background-beams.js +3 -3
  26. package/dist/components/ui/background-beams.js.map +1 -1
  27. package/dist/components/ui/background-beams_module.css.map +1 -1
  28. package/dist/components/ui/badge.js +2 -2
  29. package/dist/components/ui/badge.js.map +1 -1
  30. package/dist/components/ui/badge_module.css.map +1 -1
  31. package/dist/components/ui/breadcrumb.js +10 -10
  32. package/dist/components/ui/breadcrumb.js.map +1 -1
  33. package/dist/components/ui/breadcrumb_module.css.map +1 -1
  34. package/dist/components/ui/bubble-background.js +5 -5
  35. package/dist/components/ui/bubble-background.js.map +1 -1
  36. package/dist/components/ui/bubble-background_module.css.map +1 -1
  37. package/dist/components/ui/button-group.d.ts +1 -1
  38. package/dist/components/ui/button-group.d.ts.map +1 -1
  39. package/dist/components/ui/button-group.js +6 -6
  40. package/dist/components/ui/button-group.js.map +1 -1
  41. package/dist/components/ui/button-group_module.css.map +1 -1
  42. package/dist/components/ui/button.js +4 -4
  43. package/dist/components/ui/button.js.map +1 -1
  44. package/dist/components/ui/button_module.css.map +1 -1
  45. package/dist/components/ui/calendar.d.ts +3 -5
  46. package/dist/components/ui/calendar.d.ts.map +1 -1
  47. package/dist/components/ui/calendar.js +9 -9
  48. package/dist/components/ui/calendar.js.map +1 -1
  49. package/dist/components/ui/calendar_module.css.map +1 -1
  50. package/dist/components/ui/card-skeleton.js +2 -2
  51. package/dist/components/ui/card-skeleton.js.map +1 -1
  52. package/dist/components/ui/card-skeleton_module.css.map +1 -1
  53. package/dist/components/ui/card.js +8 -8
  54. package/dist/components/ui/card.js.map +1 -1
  55. package/dist/components/ui/card_module.css.map +1 -1
  56. package/dist/components/ui/carousel.d.ts.map +1 -1
  57. package/dist/components/ui/carousel.js +16 -16
  58. package/dist/components/ui/carousel.js.map +1 -1
  59. package/dist/components/ui/carousel_module.css +1 -1
  60. package/dist/components/ui/carousel_module.css.map +1 -1
  61. package/dist/components/ui/chart.d.ts +6 -3
  62. package/dist/components/ui/chart.d.ts.map +1 -1
  63. package/dist/components/ui/chart.js +70 -70
  64. package/dist/components/ui/chart.js.map +1 -1
  65. package/dist/components/ui/chart_module.css.map +1 -1
  66. package/dist/components/ui/checkbox-group.d.ts +2 -6
  67. package/dist/components/ui/checkbox-group.d.ts.map +1 -1
  68. package/dist/components/ui/checkbox-group.js +8 -7
  69. package/dist/components/ui/checkbox-group.js.map +1 -1
  70. package/dist/components/ui/checkbox-group_module.css.map +1 -1
  71. package/dist/components/ui/checkbox.d.ts +3 -1
  72. package/dist/components/ui/checkbox.d.ts.map +1 -1
  73. package/dist/components/ui/checkbox.js +6 -3
  74. package/dist/components/ui/checkbox.js.map +1 -1
  75. package/dist/components/ui/checkbox_module.css.map +1 -1
  76. package/dist/components/ui/collapsible.d.ts.map +1 -1
  77. package/dist/components/ui/collapsible.js +4 -4
  78. package/dist/components/ui/collapsible.js.map +1 -1
  79. package/dist/components/ui/collapsible_module.css.map +1 -1
  80. package/dist/components/ui/combobox.d.ts +335 -0
  81. package/dist/components/ui/combobox.d.ts.map +1 -0
  82. package/dist/components/ui/combobox.js +206 -0
  83. package/dist/components/ui/combobox.js.map +1 -0
  84. package/dist/components/ui/combobox.module.js +23 -0
  85. package/dist/components/ui/combobox.module.js.map +1 -0
  86. package/dist/components/ui/combobox_module.css +142 -0
  87. package/dist/components/ui/combobox_module.css.map +1 -0
  88. package/dist/components/ui/command.d.ts.map +1 -1
  89. package/dist/components/ui/command.js +62 -53
  90. package/dist/components/ui/command.js.map +1 -1
  91. package/dist/components/ui/command_module.css +1 -1
  92. package/dist/components/ui/command_module.css.map +1 -1
  93. package/dist/components/ui/context-menu.d.ts.map +1 -1
  94. package/dist/components/ui/context-menu.js +6 -6
  95. package/dist/components/ui/context-menu.js.map +1 -1
  96. package/dist/components/ui/context-menu_module.css.map +1 -1
  97. package/dist/components/ui/copy-button.js +6 -6
  98. package/dist/components/ui/copy-button.js.map +1 -1
  99. package/dist/components/ui/copy-button_module.css.map +1 -1
  100. package/dist/components/ui/counting-number.js +6 -6
  101. package/dist/components/ui/counting-number.js.map +1 -1
  102. package/dist/components/ui/counting-number_module.css.map +1 -1
  103. package/dist/components/ui/dialog.js +6 -6
  104. package/dist/components/ui/dialog.js.map +1 -1
  105. package/dist/components/ui/dialog_module.css +1 -1
  106. package/dist/components/ui/dialog_module.css.map +1 -1
  107. package/dist/components/ui/dot-background_module.css.map +1 -1
  108. package/dist/components/ui/drawer.d.ts.map +1 -1
  109. package/dist/components/ui/drawer.js +5 -5
  110. package/dist/components/ui/drawer.js.map +1 -1
  111. package/dist/components/ui/drawer_module.css.map +1 -1
  112. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  113. package/dist/components/ui/dropdown-menu.js +6 -6
  114. package/dist/components/ui/dropdown-menu.js.map +1 -1
  115. package/dist/components/ui/dropdown-menu_module.css.map +1 -1
  116. package/dist/components/ui/dropdrawer.d.ts +10 -16
  117. package/dist/components/ui/dropdrawer.d.ts.map +1 -1
  118. package/dist/components/ui/dropdrawer.js +73 -65
  119. package/dist/components/ui/dropdrawer.js.map +1 -1
  120. package/dist/components/ui/dropdrawer_module.css.map +1 -1
  121. package/dist/components/ui/empty.js +7 -7
  122. package/dist/components/ui/empty.js.map +1 -1
  123. package/dist/components/ui/empty_module.css.map +1 -1
  124. package/dist/components/ui/error-boundary.js +2 -2
  125. package/dist/components/ui/error-boundary.js.map +1 -1
  126. package/dist/components/ui/error-boundary_module.css.map +1 -1
  127. package/dist/components/ui/field.js +12 -12
  128. package/dist/components/ui/field.js.map +1 -1
  129. package/dist/components/ui/field_module.css.map +1 -1
  130. package/dist/components/ui/fireworks-background.js +6 -6
  131. package/dist/components/ui/fireworks-background.js.map +1 -1
  132. package/dist/components/ui/fireworks-background_module.css.map +1 -1
  133. package/dist/components/ui/flip-button.js +5 -5
  134. package/dist/components/ui/flip-button.js.map +1 -1
  135. package/dist/components/ui/flip-button_module.css.map +1 -1
  136. package/dist/components/ui/focus-scope.js +6 -6
  137. package/dist/components/ui/focus-scope.js.map +1 -1
  138. package/dist/components/ui/focus-scope_module.css.map +1 -1
  139. package/dist/components/ui/form-skeleton.js +2 -2
  140. package/dist/components/ui/form-skeleton.js.map +1 -1
  141. package/dist/components/ui/form-skeleton_module.css.map +1 -1
  142. package/dist/components/ui/form.d.ts +3 -3
  143. package/dist/components/ui/form.d.ts.map +1 -1
  144. package/dist/components/ui/form.js +13 -13
  145. package/dist/components/ui/form.js.map +1 -1
  146. package/dist/components/ui/form_module.css.map +1 -1
  147. package/dist/components/ui/gradient-background.js +2 -2
  148. package/dist/components/ui/gradient-background.js.map +1 -1
  149. package/dist/components/ui/gradient-background_module.css.map +1 -1
  150. package/dist/components/ui/gradient-text.js +2 -2
  151. package/dist/components/ui/gradient-text.js.map +1 -1
  152. package/dist/components/ui/gradient-text_module.css.map +1 -1
  153. package/dist/components/ui/highlight-text.js +4 -4
  154. package/dist/components/ui/highlight-text.js.map +1 -1
  155. package/dist/components/ui/highlight-text_module.css.map +1 -1
  156. package/dist/components/ui/hole-background.js +21 -21
  157. package/dist/components/ui/hole-background.js.map +1 -1
  158. package/dist/components/ui/hole-background_module.css.map +1 -1
  159. package/dist/components/ui/hover-card.js +3 -3
  160. package/dist/components/ui/hover-card.js.map +1 -1
  161. package/dist/components/ui/hover-card_module.css.map +1 -1
  162. package/dist/components/ui/input-group.js +7 -7
  163. package/dist/components/ui/input-group.js.map +1 -1
  164. package/dist/components/ui/input-group_module.css.map +1 -1
  165. package/dist/components/ui/input-otp.d.ts +3 -3
  166. package/dist/components/ui/input-otp.d.ts.map +1 -1
  167. package/dist/components/ui/input-otp.js +6 -6
  168. package/dist/components/ui/input-otp.js.map +1 -1
  169. package/dist/components/ui/input-otp_module.css.map +1 -1
  170. package/dist/components/ui/input.js +2 -2
  171. package/dist/components/ui/input.js.map +1 -1
  172. package/dist/components/ui/input_module.css.map +1 -1
  173. package/dist/components/ui/item.d.ts +1 -1
  174. package/dist/components/ui/item.d.ts.map +1 -1
  175. package/dist/components/ui/item.js +13 -13
  176. package/dist/components/ui/item.js.map +1 -1
  177. package/dist/components/ui/item_module.css.map +1 -1
  178. package/dist/components/ui/kbd.js +3 -3
  179. package/dist/components/ui/kbd.js.map +1 -1
  180. package/dist/components/ui/kbd_module.css.map +1 -1
  181. package/dist/components/ui/label.js +2 -2
  182. package/dist/components/ui/label.js.map +1 -1
  183. package/dist/components/ui/label_module.css.map +1 -1
  184. package/dist/components/ui/list-skeleton.js +2 -2
  185. package/dist/components/ui/list-skeleton.js.map +1 -1
  186. package/dist/components/ui/list-skeleton_module.css.map +1 -1
  187. package/dist/components/ui/loading-overlay.js +2 -2
  188. package/dist/components/ui/loading-overlay.js.map +1 -1
  189. package/dist/components/ui/loading-overlay_module.css.map +1 -1
  190. package/dist/components/ui/menubar.d.ts +11 -13
  191. package/dist/components/ui/menubar.d.ts.map +1 -1
  192. package/dist/components/ui/menubar.js +4 -4
  193. package/dist/components/ui/menubar.js.map +1 -1
  194. package/dist/components/ui/menubar_module.css.map +1 -1
  195. package/dist/components/ui/meter.d.ts +8 -24
  196. package/dist/components/ui/meter.d.ts.map +1 -1
  197. package/dist/components/ui/meter.js +23 -19
  198. package/dist/components/ui/meter.js.map +1 -1
  199. package/dist/components/ui/meter_module.css.map +1 -1
  200. package/dist/components/ui/navigation-menu.d.ts +3 -12
  201. package/dist/components/ui/navigation-menu.d.ts.map +1 -1
  202. package/dist/components/ui/navigation-menu.js +15 -12
  203. package/dist/components/ui/navigation-menu.js.map +1 -1
  204. package/dist/components/ui/navigation-menu_module.css +1 -1
  205. package/dist/components/ui/navigation-menu_module.css.map +1 -1
  206. package/dist/components/ui/number-field.d.ts +6 -12
  207. package/dist/components/ui/number-field.d.ts.map +1 -1
  208. package/dist/components/ui/number-field.js +3 -3
  209. package/dist/components/ui/number-field.js.map +1 -1
  210. package/dist/components/ui/number-field_module.css.map +1 -1
  211. package/dist/components/ui/pagination.js +8 -8
  212. package/dist/components/ui/pagination.js.map +1 -1
  213. package/dist/components/ui/pagination_module.css.map +1 -1
  214. package/dist/components/ui/popover.js +5 -5
  215. package/dist/components/ui/popover.js.map +1 -1
  216. package/dist/components/ui/popover_module.css.map +1 -1
  217. package/dist/components/ui/progress.d.ts +1 -4
  218. package/dist/components/ui/progress.d.ts.map +1 -1
  219. package/dist/components/ui/progress.js +10 -9
  220. package/dist/components/ui/progress.js.map +1 -1
  221. package/dist/components/ui/progress_module.css.map +1 -1
  222. package/dist/components/ui/radio-group.d.ts +2 -4
  223. package/dist/components/ui/radio-group.d.ts.map +1 -1
  224. package/dist/components/ui/radio-group.js +3 -3
  225. package/dist/components/ui/radio-group.js.map +1 -1
  226. package/dist/components/ui/radio-group_module.css.map +1 -1
  227. package/dist/components/ui/resizable.d.ts +13 -29
  228. package/dist/components/ui/resizable.d.ts.map +1 -1
  229. package/dist/components/ui/resizable.js +8 -7
  230. package/dist/components/ui/resizable.js.map +1 -1
  231. package/dist/components/ui/resizable_module.css.map +1 -1
  232. package/dist/components/ui/ripple-button.js +9 -9
  233. package/dist/components/ui/ripple-button.js.map +1 -1
  234. package/dist/components/ui/ripple-button_module.css.map +1 -1
  235. package/dist/components/ui/scratcher.d.ts +1 -1
  236. package/dist/components/ui/scratcher.d.ts.map +1 -1
  237. package/dist/components/ui/scratcher.js +5 -4
  238. package/dist/components/ui/scratcher.js.map +1 -1
  239. package/dist/components/ui/scratcher_module.css.map +1 -1
  240. package/dist/components/ui/scroll-area.d.ts +2 -4
  241. package/dist/components/ui/scroll-area.d.ts.map +1 -1
  242. package/dist/components/ui/scroll-area.js +2 -2
  243. package/dist/components/ui/scroll-area.js.map +1 -1
  244. package/dist/components/ui/scroll-area_module.css.map +1 -1
  245. package/dist/components/ui/select.js +4 -4
  246. package/dist/components/ui/select.js.map +1 -1
  247. package/dist/components/ui/select_module.css.map +1 -1
  248. package/dist/components/ui/separator.d.ts +1 -4
  249. package/dist/components/ui/separator.d.ts.map +1 -1
  250. package/dist/components/ui/separator.js +9 -8
  251. package/dist/components/ui/separator.js.map +1 -1
  252. package/dist/components/ui/separator_module.css.map +1 -1
  253. package/dist/components/ui/sheet.d.ts.map +1 -1
  254. package/dist/components/ui/sheet.js +6 -6
  255. package/dist/components/ui/sheet.js.map +1 -1
  256. package/dist/components/ui/sheet_module.css.map +1 -1
  257. package/dist/components/ui/sidebar.d.ts +1 -1
  258. package/dist/components/ui/sidebar.d.ts.map +1 -1
  259. package/dist/components/ui/sidebar.js +36 -36
  260. package/dist/components/ui/sidebar.js.map +1 -1
  261. package/dist/components/ui/sidebar_module.css.map +1 -1
  262. package/dist/components/ui/skeleton.js +2 -2
  263. package/dist/components/ui/skeleton.js.map +1 -1
  264. package/dist/components/ui/skeleton_module.css.map +1 -1
  265. package/dist/components/ui/slider.js +2 -2
  266. package/dist/components/ui/slider.js.map +1 -1
  267. package/dist/components/ui/slider_module.css.map +1 -1
  268. package/dist/components/ui/spinner.js +2 -2
  269. package/dist/components/ui/spinner.js.map +1 -1
  270. package/dist/components/ui/spinner_module.css.map +1 -1
  271. package/dist/components/ui/stepper.js +2 -2
  272. package/dist/components/ui/stepper.js.map +1 -1
  273. package/dist/components/ui/stepper_module.css.map +1 -1
  274. package/dist/components/ui/switch.js +2 -2
  275. package/dist/components/ui/switch.js.map +1 -1
  276. package/dist/components/ui/switch_module.css.map +1 -1
  277. package/dist/components/ui/table-skeleton.js +2 -2
  278. package/dist/components/ui/table-skeleton.js.map +1 -1
  279. package/dist/components/ui/table-skeleton_module.css.map +1 -1
  280. package/dist/components/ui/table.js +9 -9
  281. package/dist/components/ui/table.js.map +1 -1
  282. package/dist/components/ui/table_module.css.map +1 -1
  283. package/dist/components/ui/tabs.js +3 -3
  284. package/dist/components/ui/tabs.js.map +1 -1
  285. package/dist/components/ui/tabs_module.css.map +1 -1
  286. package/dist/components/ui/textarea.js +2 -2
  287. package/dist/components/ui/textarea.js.map +1 -1
  288. package/dist/components/ui/textarea_module.css.map +1 -1
  289. package/dist/components/ui/timeline.js +5 -5
  290. package/dist/components/ui/timeline.js.map +1 -1
  291. package/dist/components/ui/timeline_module.css.map +1 -1
  292. package/dist/components/ui/{sonner.d.ts → toast.d.ts} +15 -6
  293. package/dist/components/ui/toast.d.ts.map +1 -0
  294. package/dist/components/ui/{sonner.js → toast.js} +43 -42
  295. package/dist/components/ui/toast.js.map +1 -0
  296. package/dist/components/ui/toast.module.js +34 -0
  297. package/dist/components/ui/toast.module.js.map +1 -0
  298. package/dist/components/ui/{sonner_module.css → toast_module.css} +35 -35
  299. package/dist/components/ui/toast_module.css.map +1 -0
  300. package/dist/components/ui/toggle-group.d.ts +2 -8
  301. package/dist/components/ui/toggle-group.d.ts.map +1 -1
  302. package/dist/components/ui/toggle-group.js +14 -12
  303. package/dist/components/ui/toggle-group.js.map +1 -1
  304. package/dist/components/ui/toggle-group_module.css.map +1 -1
  305. package/dist/components/ui/toggle.js +2 -2
  306. package/dist/components/ui/toggle.js.map +1 -1
  307. package/dist/components/ui/toggle_module.css.map +1 -1
  308. package/dist/components/ui/toolbar.d.ts +10 -30
  309. package/dist/components/ui/toolbar.d.ts.map +1 -1
  310. package/dist/components/ui/toolbar.js +28 -23
  311. package/dist/components/ui/toolbar.js.map +1 -1
  312. package/dist/components/ui/toolbar_module.css.map +1 -1
  313. package/dist/components/ui/tooltip.js +4 -4
  314. package/dist/components/ui/tooltip.js.map +1 -1
  315. package/dist/components/ui/tooltip_module.css.map +1 -1
  316. package/dist/components/ui/typewriter.js +4 -4
  317. package/dist/components/ui/typewriter.js.map +1 -1
  318. package/dist/components/ui/typewriter_module.css.map +1 -1
  319. package/dist/components/ui/visually-hidden.js +2 -2
  320. package/dist/components/ui/visually-hidden.js.map +1 -1
  321. package/dist/components/ui/visually-hidden_module.css.map +1 -1
  322. package/dist/hooks/useAnnounce.js +5 -5
  323. package/dist/hooks/useAnnounce.js.map +1 -1
  324. package/dist/hooks/useClipboard.d.ts +77 -0
  325. package/dist/hooks/useClipboard.d.ts.map +1 -0
  326. package/dist/hooks/useClipboard.js +42 -0
  327. package/dist/hooks/useClipboard.js.map +1 -0
  328. package/dist/hooks/useControllableState.d.ts +54 -0
  329. package/dist/hooks/useControllableState.d.ts.map +1 -0
  330. package/dist/hooks/useControllableState.js +29 -0
  331. package/dist/hooks/useControllableState.js.map +1 -0
  332. package/dist/hooks/useDebounce.d.ts +33 -0
  333. package/dist/hooks/useDebounce.d.ts.map +1 -0
  334. package/dist/hooks/useDebounce.js +20 -0
  335. package/dist/hooks/useDebounce.js.map +1 -0
  336. package/dist/hooks/useEventCallback.d.ts +34 -0
  337. package/dist/hooks/useEventCallback.d.ts.map +1 -0
  338. package/dist/hooks/useEventCallback.js +12 -0
  339. package/dist/hooks/useEventCallback.js.map +1 -0
  340. package/dist/hooks/useFocusManager.js +6 -6
  341. package/dist/hooks/useFocusManager.js.map +1 -1
  342. package/dist/hooks/useFocusVisible.js +5 -5
  343. package/dist/hooks/useFocusVisible.js.map +1 -1
  344. package/dist/hooks/useId.d.ts +30 -0
  345. package/dist/hooks/useId.d.ts.map +1 -0
  346. package/dist/hooks/useId.js +9 -0
  347. package/dist/hooks/useId.js.map +1 -0
  348. package/dist/hooks/useIntersectionObserver.d.ts +51 -0
  349. package/dist/hooks/useIntersectionObserver.d.ts.map +1 -0
  350. package/dist/hooks/useIntersectionObserver.js +25 -0
  351. package/dist/hooks/useIntersectionObserver.js.map +1 -0
  352. package/dist/hooks/useInterval.d.ts +55 -0
  353. package/dist/hooks/useInterval.d.ts.map +1 -0
  354. package/dist/hooks/useInterval.js +24 -0
  355. package/dist/hooks/useInterval.js.map +1 -0
  356. package/dist/hooks/useLocalStorage.d.ts +43 -0
  357. package/dist/hooks/useLocalStorage.d.ts.map +1 -0
  358. package/dist/hooks/useLocalStorage.js +53 -0
  359. package/dist/hooks/useLocalStorage.js.map +1 -0
  360. package/dist/hooks/useMediaQuery.js +3 -3
  361. package/dist/hooks/useMediaQuery.js.map +1 -1
  362. package/dist/hooks/useMergedRefs.d.ts +27 -0
  363. package/dist/hooks/useMergedRefs.d.ts.map +1 -0
  364. package/dist/hooks/useMergedRefs.js +11 -0
  365. package/dist/hooks/useMergedRefs.js.map +1 -0
  366. package/dist/hooks/useOnClickOutside.d.ts +32 -0
  367. package/dist/hooks/useOnClickOutside.d.ts.map +1 -0
  368. package/dist/hooks/useOnClickOutside.js +23 -0
  369. package/dist/hooks/useOnClickOutside.js.map +1 -0
  370. package/dist/hooks/usePrevious.d.ts +33 -0
  371. package/dist/hooks/usePrevious.d.ts.map +1 -0
  372. package/dist/hooks/usePrevious.js +14 -0
  373. package/dist/hooks/usePrevious.js.map +1 -0
  374. package/dist/hooks/useThrottle.d.ts +37 -0
  375. package/dist/hooks/useThrottle.d.ts.map +1 -0
  376. package/dist/hooks/useThrottle.js +34 -0
  377. package/dist/hooks/useThrottle.js.map +1 -0
  378. package/dist/hooks/useTimeout.d.ts +28 -0
  379. package/dist/hooks/useTimeout.d.ts.map +1 -0
  380. package/dist/hooks/useTimeout.js +24 -0
  381. package/dist/hooks/useTimeout.js.map +1 -0
  382. package/dist/index.css.map +1 -1
  383. package/dist/index.d.ts +17 -3
  384. package/dist/index.d.ts.map +1 -1
  385. package/dist/index.js +15 -1
  386. package/dist/lib/utilities.d.ts +2 -3
  387. package/dist/lib/utilities.d.ts.map +1 -1
  388. package/dist/lib/utilities.js.map +1 -1
  389. package/dist/motion/Collapse.js +2 -2
  390. package/dist/motion/Collapse.js.map +1 -1
  391. package/dist/motion/Collapse_module.css.map +1 -1
  392. package/dist/motion/tokens.js +5 -5
  393. package/dist/motion/tokens.js.map +1 -1
  394. package/package.json +88 -10
  395. package/src/components/ui/alert-dialog.tsx +15 -8
  396. package/src/components/ui/avatar.tsx +9 -6
  397. package/src/components/ui/calendar.tsx +9 -14
  398. package/src/components/ui/carousel.tsx +2 -0
  399. package/src/components/ui/chart.tsx +65 -62
  400. package/src/components/ui/checkbox-group.tsx +4 -5
  401. package/src/components/ui/checkbox.tsx +10 -2
  402. package/src/components/ui/collapsible.tsx +1 -0
  403. package/src/components/ui/combobox.module.css +158 -0
  404. package/src/components/ui/combobox.tsx +569 -0
  405. package/src/components/ui/command.tsx +31 -15
  406. package/src/components/ui/context-menu.tsx +3 -0
  407. package/src/components/ui/drawer.tsx +2 -0
  408. package/src/components/ui/dropdown-menu.tsx +3 -0
  409. package/src/components/ui/dropdrawer.tsx +80 -62
  410. package/src/components/ui/form.tsx +28 -3
  411. package/src/components/ui/input-otp.tsx +3 -3
  412. package/src/components/ui/menubar.tsx +9 -10
  413. package/src/components/ui/meter.tsx +16 -17
  414. package/src/components/ui/navigation-menu.tsx +41 -33
  415. package/src/components/ui/number-field.tsx +6 -13
  416. package/src/components/ui/progress.tsx +3 -2
  417. package/src/components/ui/radio-group.tsx +2 -5
  418. package/src/components/ui/resizable.tsx +15 -18
  419. package/src/components/ui/scratcher.tsx +6 -10
  420. package/src/components/ui/scroll-area.tsx +2 -5
  421. package/src/components/ui/separator.tsx +4 -3
  422. package/src/components/ui/sheet.tsx +3 -0
  423. package/src/components/ui/sidebar.tsx +1 -0
  424. package/src/components/ui/{sonner.module.css → toast.module.css} +1 -1
  425. package/src/components/ui/{sonner.tsx → toast.tsx} +22 -14
  426. package/src/components/ui/toggle-group.tsx +6 -4
  427. package/src/components/ui/toolbar.tsx +20 -21
  428. package/src/hooks/useClipboard.tsx +137 -0
  429. package/src/hooks/useControllableState.tsx +81 -0
  430. package/src/hooks/useDebounce.tsx +50 -0
  431. package/src/hooks/useEventCallback.tsx +47 -0
  432. package/src/hooks/useId.tsx +36 -0
  433. package/src/hooks/useIntersectionObserver.tsx +81 -0
  434. package/src/hooks/useInterval.tsx +80 -0
  435. package/src/hooks/useLocalStorage.tsx +111 -0
  436. package/src/hooks/useMergedRefs.tsx +48 -0
  437. package/src/hooks/useOnClickOutside.tsx +55 -0
  438. package/src/hooks/usePrevious.tsx +44 -0
  439. package/src/hooks/useThrottle.tsx +78 -0
  440. package/src/hooks/useTimeout.tsx +51 -0
  441. package/src/index.ts +27 -4
  442. package/src/lib/utilities.ts +4 -4
  443. package/src/motion/tokens.ts +4 -4
  444. package/src/stories/DesignPrinciples.mdx +48 -0
  445. package/src/stories/GettingStarted.mdx +92 -0
  446. package/src/stories/Welcome.mdx +44 -0
  447. package/dist/components/ui/sonner.d.ts.map +0 -1
  448. package/dist/components/ui/sonner.js.map +0 -1
  449. package/dist/components/ui/sonner.module.js +0 -34
  450. package/dist/components/ui/sonner.module.js.map +0 -1
  451. package/dist/components/ui/sonner_module.css.map +0 -1
package/EXAMPLES.md CHANGED
@@ -1158,3 +1158,2513 @@ import { Button, Card } from "@arolariu/components";
1158
1158
  ```
1159
1159
 
1160
1160
  Ready to build something amazing? **[🚀 Start with our Quick Start Guide](./README.md#-quick-start)**
1161
+
1162
+ ---
1163
+
1164
+ ## 🎓 Pattern Recipes
1165
+
1166
+ > **Real-world patterns ready to copy, paste, and customize.** These recipes demonstrate common UI patterns using @arolariu/components with best practices for forms, data, modals, and error handling.
1167
+
1168
+ ### Recipe 1: Login Form with Validation
1169
+
1170
+ **Complete login form with zod validation, error handling, and loading states.**
1171
+
1172
+ ```tsx
1173
+ import {zodResolver} from "@hookform/resolvers/zod";
1174
+ import {useForm} from "react-hook-form";
1175
+ import * as z from "zod";
1176
+
1177
+ import {Alert, AlertDescription} from "@arolariu/components/alert";
1178
+ import {Button} from "@arolariu/components/button";
1179
+ import {
1180
+ Card,
1181
+ CardContent,
1182
+ CardDescription,
1183
+ CardFooter,
1184
+ CardHeader,
1185
+ CardTitle,
1186
+ } from "@arolariu/components/card";
1187
+ import {Checkbox} from "@arolariu/components/checkbox";
1188
+ import {
1189
+ Form,
1190
+ FormControl,
1191
+ FormField,
1192
+ FormItem,
1193
+ FormLabel,
1194
+ FormMessage,
1195
+ } from "@arolariu/components/form";
1196
+ import {Input} from "@arolariu/components/input";
1197
+ import {toast} from "@arolariu/components/sonner";
1198
+ import styles from "./login-form.module.css";
1199
+
1200
+ // Define validation schema
1201
+ const loginSchema = z.object({
1202
+ email: z.string().email("Please enter a valid email address"),
1203
+ password: z.string().min(8, "Password must be at least 8 characters"),
1204
+ rememberMe: z.boolean().default(false),
1205
+ });
1206
+
1207
+ type LoginFormValues = z.infer<typeof loginSchema>;
1208
+
1209
+ export function LoginForm() {
1210
+ const form = useForm<LoginFormValues>({
1211
+ resolver: zodResolver(loginSchema),
1212
+ defaultValues: {
1213
+ email: "",
1214
+ password: "",
1215
+ rememberMe: false,
1216
+ },
1217
+ });
1218
+
1219
+ async function onSubmit(values: LoginFormValues) {
1220
+ try {
1221
+ // Simulate API call
1222
+ await new Promise((resolve) => setTimeout(resolve, 1500));
1223
+
1224
+ // Check credentials (mock)
1225
+ if (values.email === "demo@example.com" && values.password === "password123") {
1226
+ toast.success("Login successful! Redirecting...");
1227
+ // Redirect to dashboard
1228
+ window.location.href = "/dashboard";
1229
+ } else {
1230
+ throw new Error("Invalid credentials");
1231
+ }
1232
+ } catch (error) {
1233
+ toast.error("Login failed. Please check your credentials and try again.");
1234
+ }
1235
+ }
1236
+
1237
+ return (
1238
+ <div className={styles.page}>
1239
+ <Card className={styles.card}>
1240
+ <CardHeader className={styles.header}>
1241
+ <CardTitle>Welcome Back</CardTitle>
1242
+ <CardDescription>
1243
+ Sign in to your account to continue
1244
+ </CardDescription>
1245
+ </CardHeader>
1246
+
1247
+ <Form {...form}>
1248
+ <form onSubmit={form.handleSubmit(onSubmit)}>
1249
+ <CardContent className={styles.content}>
1250
+ {form.formState.errors.root ? (
1251
+ <Alert variant="destructive">
1252
+ <AlertDescription>
1253
+ {form.formState.errors.root.message}
1254
+ </AlertDescription>
1255
+ </Alert>
1256
+ ) : null}
1257
+
1258
+ <FormField
1259
+ control={form.control}
1260
+ name="email"
1261
+ render={({field}) => (
1262
+ <FormItem>
1263
+ <FormLabel>Email</FormLabel>
1264
+ <FormControl>
1265
+ <Input
1266
+ type="email"
1267
+ placeholder="you@example.com"
1268
+ autoComplete="email"
1269
+ {...field}
1270
+ />
1271
+ </FormControl>
1272
+ <FormMessage />
1273
+ </FormItem>
1274
+ )}
1275
+ />
1276
+
1277
+ <FormField
1278
+ control={form.control}
1279
+ name="password"
1280
+ render={({field}) => (
1281
+ <FormItem>
1282
+ <FormLabel>Password</FormLabel>
1283
+ <FormControl>
1284
+ <Input
1285
+ type="password"
1286
+ placeholder="••••••••"
1287
+ autoComplete="current-password"
1288
+ {...field}
1289
+ />
1290
+ </FormControl>
1291
+ <FormMessage />
1292
+ </FormItem>
1293
+ )}
1294
+ />
1295
+
1296
+ <FormField
1297
+ control={form.control}
1298
+ name="rememberMe"
1299
+ render={({field}) => (
1300
+ <FormItem className={styles.checkboxItem}>
1301
+ <FormControl>
1302
+ <Checkbox
1303
+ checked={field.value}
1304
+ onCheckedChange={field.onChange}
1305
+ />
1306
+ </FormControl>
1307
+ <FormLabel className={styles.checkboxLabel}>
1308
+ Remember me for 30 days
1309
+ </FormLabel>
1310
+ </FormItem>
1311
+ )}
1312
+ />
1313
+ </CardContent>
1314
+
1315
+ <CardFooter className={styles.footer}>
1316
+ <Button
1317
+ type="submit"
1318
+ className={styles.submitButton}
1319
+ disabled={form.formState.isSubmitting}
1320
+ >
1321
+ {form.formState.isSubmitting ? "Signing in..." : "Sign In"}
1322
+ </Button>
1323
+
1324
+ <div className={styles.links}>
1325
+ <a href="/forgot-password" className={styles.link}>
1326
+ Forgot password?
1327
+ </a>
1328
+ <a href="/signup" className={styles.link}>
1329
+ Create account
1330
+ </a>
1331
+ </div>
1332
+ </CardFooter>
1333
+ </form>
1334
+ </Form>
1335
+ </Card>
1336
+ </div>
1337
+ );
1338
+ }
1339
+ ```
1340
+
1341
+ ```css
1342
+ /* login-form.module.css */
1343
+ .page {
1344
+ display: flex;
1345
+ align-items: center;
1346
+ justify-content: center;
1347
+ min-height: 100vh;
1348
+ padding: 1rem;
1349
+ background-color: var(--ac-muted);
1350
+ }
1351
+
1352
+ .card {
1353
+ width: min(28rem, 100%);
1354
+ }
1355
+
1356
+ .header {
1357
+ text-align: center;
1358
+ }
1359
+
1360
+ .content {
1361
+ display: grid;
1362
+ gap: 1rem;
1363
+ }
1364
+
1365
+ .checkboxItem {
1366
+ display: flex;
1367
+ flex-direction: row;
1368
+ align-items: center;
1369
+ gap: 0.5rem;
1370
+ }
1371
+
1372
+ .checkboxLabel {
1373
+ margin-top: 0;
1374
+ font-weight: 400;
1375
+ }
1376
+
1377
+ .footer {
1378
+ display: flex;
1379
+ flex-direction: column;
1380
+ gap: 1rem;
1381
+ }
1382
+
1383
+ .submitButton {
1384
+ width: 100%;
1385
+ }
1386
+
1387
+ .links {
1388
+ display: flex;
1389
+ justify-content: space-between;
1390
+ font-size: 0.875rem;
1391
+ }
1392
+
1393
+ .link {
1394
+ color: var(--ac-primary);
1395
+ text-decoration: none;
1396
+ }
1397
+
1398
+ .link:hover {
1399
+ text-decoration: underline;
1400
+ }
1401
+ ```
1402
+
1403
+ ---
1404
+
1405
+ ### Recipe 2: Data Table with Sorting (TanStack Table)
1406
+
1407
+ **Sortable, filterable data table with row actions and pagination.**
1408
+
1409
+ ```tsx
1410
+ import {
1411
+ createColumnHelper,
1412
+ flexRender,
1413
+ getCoreRowModel,
1414
+ getPaginationRowModel,
1415
+ getSortedRowModel,
1416
+ useReactTable,
1417
+ type SortingState,
1418
+ } from "@tanstack/react-table";
1419
+ import {ArrowUpDown, ChevronLeft, ChevronRight, MoreHorizontal} from "lucide-react";
1420
+ import {useState} from "react";
1421
+
1422
+ import {Badge} from "@arolariu/components/badge";
1423
+ import {Button} from "@arolariu/components/button";
1424
+ import {
1425
+ DropdownMenu,
1426
+ DropdownMenuContent,
1427
+ DropdownMenuItem,
1428
+ DropdownMenuLabel,
1429
+ DropdownMenuSeparator,
1430
+ DropdownMenuTrigger,
1431
+ } from "@arolariu/components/dropdown-menu";
1432
+ import {Input} from "@arolariu/components/input";
1433
+ import {
1434
+ Table,
1435
+ TableBody,
1436
+ TableCell,
1437
+ TableHead,
1438
+ TableHeader,
1439
+ TableRow,
1440
+ } from "@arolariu/components/table";
1441
+ import {toast} from "@arolariu/components/sonner";
1442
+ import styles from "./data-table.module.css";
1443
+
1444
+ interface User {
1445
+ id: string;
1446
+ name: string;
1447
+ email: string;
1448
+ role: "admin" | "user" | "guest";
1449
+ status: "active" | "inactive";
1450
+ createdAt: Date;
1451
+ }
1452
+
1453
+ const data: User[] = [
1454
+ {
1455
+ id: "1",
1456
+ name: "John Doe",
1457
+ email: "john@example.com",
1458
+ role: "admin",
1459
+ status: "active",
1460
+ createdAt: new Date("2024-01-15"),
1461
+ },
1462
+ {
1463
+ id: "2",
1464
+ name: "Jane Smith",
1465
+ email: "jane@example.com",
1466
+ role: "user",
1467
+ status: "active",
1468
+ createdAt: new Date("2024-02-20"),
1469
+ },
1470
+ {
1471
+ id: "3",
1472
+ name: "Bob Johnson",
1473
+ email: "bob@example.com",
1474
+ role: "guest",
1475
+ status: "inactive",
1476
+ createdAt: new Date("2024-03-10"),
1477
+ },
1478
+ ];
1479
+
1480
+ const columnHelper = createColumnHelper<User>();
1481
+
1482
+ export function DataTableWithSorting() {
1483
+ const [sorting, setSorting] = useState<SortingState>([]);
1484
+ const [globalFilter, setGlobalFilter] = useState("");
1485
+
1486
+ const columns = [
1487
+ columnHelper.accessor("name", {
1488
+ header: ({column}) => (
1489
+ <button
1490
+ type="button"
1491
+ className={styles.sortButton}
1492
+ onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
1493
+ >
1494
+ Name
1495
+ <ArrowUpDown className={styles.sortIcon} />
1496
+ </button>
1497
+ ),
1498
+ cell: (info) => <span className={styles.emphasisText}>{info.getValue()}</span>,
1499
+ }),
1500
+ columnHelper.accessor("email", {
1501
+ header: "Email",
1502
+ }),
1503
+ columnHelper.accessor("role", {
1504
+ header: "Role",
1505
+ cell: (info) => {
1506
+ const role = info.getValue();
1507
+ return (
1508
+ <Badge variant={role === "admin" ? "default" : "secondary"}>
1509
+ {role}
1510
+ </Badge>
1511
+ );
1512
+ },
1513
+ }),
1514
+ columnHelper.accessor("status", {
1515
+ header: "Status",
1516
+ cell: (info) => {
1517
+ const status = info.getValue();
1518
+ return (
1519
+ <Badge variant={status === "active" ? "default" : "outline"}>
1520
+ {status}
1521
+ </Badge>
1522
+ );
1523
+ },
1524
+ }),
1525
+ columnHelper.accessor("createdAt", {
1526
+ header: ({column}) => (
1527
+ <button
1528
+ type="button"
1529
+ className={styles.sortButton}
1530
+ onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
1531
+ >
1532
+ Created At
1533
+ <ArrowUpDown className={styles.sortIcon} />
1534
+ </button>
1535
+ ),
1536
+ cell: (info) => info.getValue().toLocaleDateString(),
1537
+ }),
1538
+ columnHelper.display({
1539
+ id: "actions",
1540
+ cell: ({row}) => (
1541
+ <DropdownMenu>
1542
+ <DropdownMenuTrigger render={<button type="button" className={styles.iconButton} />}>
1543
+ <MoreHorizontal />
1544
+ </DropdownMenuTrigger>
1545
+ <DropdownMenuContent align="end">
1546
+ <DropdownMenuLabel>Actions</DropdownMenuLabel>
1547
+ <DropdownMenuItem
1548
+ onClick={() => {
1549
+ navigator.clipboard.writeText(row.original.id);
1550
+ toast.success("User ID copied to clipboard");
1551
+ }}
1552
+ >
1553
+ Copy ID
1554
+ </DropdownMenuItem>
1555
+ <DropdownMenuSeparator />
1556
+ <DropdownMenuItem onClick={() => toast.info(`Viewing user: ${row.original.name}`)}>
1557
+ View details
1558
+ </DropdownMenuItem>
1559
+ <DropdownMenuItem onClick={() => toast.info(`Editing user: ${row.original.name}`)}>
1560
+ Edit user
1561
+ </DropdownMenuItem>
1562
+ </DropdownMenuContent>
1563
+ </DropdownMenu>
1564
+ ),
1565
+ }),
1566
+ ];
1567
+
1568
+ const table = useReactTable({
1569
+ data,
1570
+ columns,
1571
+ state: {
1572
+ sorting,
1573
+ globalFilter,
1574
+ },
1575
+ onSortingChange: setSorting,
1576
+ onGlobalFilterChange: setGlobalFilter,
1577
+ getCoreRowModel: getCoreRowModel(),
1578
+ getSortedRowModel: getSortedRowModel(),
1579
+ getPaginationRowModel: getPaginationRowModel(),
1580
+ initialState: {
1581
+ pagination: {
1582
+ pageSize: 5,
1583
+ },
1584
+ },
1585
+ });
1586
+
1587
+ return (
1588
+ <div className={styles.container}>
1589
+ <div className={styles.toolbar}>
1590
+ <Input
1591
+ placeholder="Search users..."
1592
+ value={globalFilter}
1593
+ onChange={(e) => setGlobalFilter(e.target.value)}
1594
+ className={styles.searchInput}
1595
+ />
1596
+ </div>
1597
+
1598
+ <div className={styles.tableWrapper}>
1599
+ <Table>
1600
+ <TableHeader>
1601
+ {table.getHeaderGroups().map((headerGroup) => (
1602
+ <TableRow key={headerGroup.id}>
1603
+ {headerGroup.headers.map((header) => (
1604
+ <TableHead key={header.id}>
1605
+ {header.isPlaceholder
1606
+ ? null
1607
+ : flexRender(header.column.columnDef.header, header.getContext())}
1608
+ </TableHead>
1609
+ ))}
1610
+ </TableRow>
1611
+ ))}
1612
+ </TableHeader>
1613
+ <TableBody>
1614
+ {table.getRowModel().rows.length > 0 ? (
1615
+ table.getRowModel().rows.map((row) => (
1616
+ <TableRow key={row.id}>
1617
+ {row.getVisibleCells().map((cell) => (
1618
+ <TableCell key={cell.id}>
1619
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
1620
+ </TableCell>
1621
+ ))}
1622
+ </TableRow>
1623
+ ))
1624
+ ) : (
1625
+ <TableRow>
1626
+ <TableCell colSpan={columns.length} className={styles.emptyCell}>
1627
+ No results found.
1628
+ </TableCell>
1629
+ </TableRow>
1630
+ )}
1631
+ </TableBody>
1632
+ </Table>
1633
+ </div>
1634
+
1635
+ <div className={styles.pagination}>
1636
+ <div className={styles.paginationInfo}>
1637
+ Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
1638
+ </div>
1639
+ <div className={styles.paginationButtons}>
1640
+ <Button
1641
+ variant="outline"
1642
+ size="sm"
1643
+ onClick={() => table.previousPage()}
1644
+ disabled={!table.getCanPreviousPage()}
1645
+ >
1646
+ <ChevronLeft />
1647
+ Previous
1648
+ </Button>
1649
+ <Button
1650
+ variant="outline"
1651
+ size="sm"
1652
+ onClick={() => table.nextPage()}
1653
+ disabled={!table.getCanNextPage()}
1654
+ >
1655
+ Next
1656
+ <ChevronRight />
1657
+ </Button>
1658
+ </div>
1659
+ </div>
1660
+ </div>
1661
+ );
1662
+ }
1663
+ ```
1664
+
1665
+ ```css
1666
+ /* data-table.module.css */
1667
+ .container {
1668
+ display: grid;
1669
+ gap: 1rem;
1670
+ }
1671
+
1672
+ .toolbar {
1673
+ display: flex;
1674
+ gap: 0.5rem;
1675
+ }
1676
+
1677
+ .searchInput {
1678
+ max-width: 20rem;
1679
+ }
1680
+
1681
+ .tableWrapper {
1682
+ border: 1px solid var(--ac-border);
1683
+ border-radius: var(--ac-radius-md);
1684
+ overflow: hidden;
1685
+ }
1686
+
1687
+ .sortButton {
1688
+ display: inline-flex;
1689
+ align-items: center;
1690
+ gap: 0.5rem;
1691
+ font-weight: 500;
1692
+ background: none;
1693
+ border: none;
1694
+ cursor: pointer;
1695
+ }
1696
+
1697
+ .sortIcon {
1698
+ width: 1rem;
1699
+ height: 1rem;
1700
+ opacity: 0.5;
1701
+ }
1702
+
1703
+ .emphasisText {
1704
+ font-weight: 500;
1705
+ }
1706
+
1707
+ .iconButton {
1708
+ display: inline-flex;
1709
+ align-items: center;
1710
+ justify-content: center;
1711
+ width: 2rem;
1712
+ height: 2rem;
1713
+ border: none;
1714
+ background: none;
1715
+ border-radius: var(--ac-radius-sm);
1716
+ cursor: pointer;
1717
+ }
1718
+
1719
+ .iconButton:hover {
1720
+ background-color: var(--ac-accent);
1721
+ }
1722
+
1723
+ .emptyCell {
1724
+ text-align: center;
1725
+ padding: 2rem;
1726
+ color: var(--ac-muted-foreground);
1727
+ }
1728
+
1729
+ .pagination {
1730
+ display: flex;
1731
+ align-items: center;
1732
+ justify-content: space-between;
1733
+ }
1734
+
1735
+ .paginationInfo {
1736
+ font-size: 0.875rem;
1737
+ color: var(--ac-muted-foreground);
1738
+ }
1739
+
1740
+ .paginationButtons {
1741
+ display: flex;
1742
+ gap: 0.5rem;
1743
+ }
1744
+ ```
1745
+
1746
+ ---
1747
+
1748
+ ### Recipe 3: Modal Form (Dialog + Form + Validation)
1749
+
1750
+ **Dialog with form validation and async submission.**
1751
+
1752
+ ```tsx
1753
+ import {zodResolver} from "@hookform/resolvers/zod";
1754
+ import {Plus} from "lucide-react";
1755
+ import {useState} from "react";
1756
+ import {useForm} from "react-hook-form";
1757
+ import * as z from "zod";
1758
+
1759
+ import {Button} from "@arolariu/components/button";
1760
+ import {
1761
+ Dialog,
1762
+ DialogContent,
1763
+ DialogDescription,
1764
+ DialogFooter,
1765
+ DialogHeader,
1766
+ DialogTitle,
1767
+ DialogTrigger,
1768
+ } from "@arolariu/components/dialog";
1769
+ import {
1770
+ Form,
1771
+ FormControl,
1772
+ FormDescription,
1773
+ FormField,
1774
+ FormItem,
1775
+ FormLabel,
1776
+ FormMessage,
1777
+ } from "@arolariu/components/form";
1778
+ import {Input} from "@arolariu/components/input";
1779
+ import {
1780
+ Select,
1781
+ SelectContent,
1782
+ SelectItem,
1783
+ SelectTrigger,
1784
+ SelectValue,
1785
+ } from "@arolariu/components/select";
1786
+ import {Textarea} from "@arolariu/components/textarea";
1787
+ import {toast} from "@arolariu/components/sonner";
1788
+ import styles from "./modal-form.module.css";
1789
+
1790
+ const projectSchema = z.object({
1791
+ name: z.string().min(3, "Project name must be at least 3 characters"),
1792
+ description: z.string().max(500, "Description must be less than 500 characters").optional(),
1793
+ category: z.enum(["web", "mobile", "desktop", "other"], {
1794
+ required_error: "Please select a category",
1795
+ }),
1796
+ budget: z.string().regex(/^\d+$/, "Budget must be a valid number"),
1797
+ });
1798
+
1799
+ type ProjectFormValues = z.infer<typeof projectSchema>;
1800
+
1801
+ export function CreateProjectModal() {
1802
+ const [open, setOpen] = useState(false);
1803
+
1804
+ const form = useForm<ProjectFormValues>({
1805
+ resolver: zodResolver(projectSchema),
1806
+ defaultValues: {
1807
+ name: "",
1808
+ description: "",
1809
+ category: undefined,
1810
+ budget: "",
1811
+ },
1812
+ });
1813
+
1814
+ async function onSubmit(values: ProjectFormValues) {
1815
+ try {
1816
+ // Simulate API call
1817
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1818
+
1819
+ console.log("Project created:", values);
1820
+ toast.success("Project created successfully!");
1821
+
1822
+ // Close modal and reset form
1823
+ setOpen(false);
1824
+ form.reset();
1825
+ } catch (error) {
1826
+ toast.error("Failed to create project. Please try again.");
1827
+ }
1828
+ }
1829
+
1830
+ return (
1831
+ <Dialog open={open} onOpenChange={setOpen}>
1832
+ <DialogTrigger render={<Button />}>
1833
+ <Plus />
1834
+ Create Project
1835
+ </DialogTrigger>
1836
+
1837
+ <DialogContent className={styles.content}>
1838
+ <DialogHeader>
1839
+ <DialogTitle>Create New Project</DialogTitle>
1840
+ <DialogDescription>
1841
+ Fill in the details below to create a new project. Click save when you're done.
1842
+ </DialogDescription>
1843
+ </DialogHeader>
1844
+
1845
+ <Form {...form}>
1846
+ <form onSubmit={form.handleSubmit(onSubmit)} className={styles.form}>
1847
+ <FormField
1848
+ control={form.control}
1849
+ name="name"
1850
+ render={({field}) => (
1851
+ <FormItem>
1852
+ <FormLabel>Project Name</FormLabel>
1853
+ <FormControl>
1854
+ <Input placeholder="My Awesome Project" {...field} />
1855
+ </FormControl>
1856
+ <FormDescription>
1857
+ Choose a unique name for your project.
1858
+ </FormDescription>
1859
+ <FormMessage />
1860
+ </FormItem>
1861
+ )}
1862
+ />
1863
+
1864
+ <FormField
1865
+ control={form.control}
1866
+ name="category"
1867
+ render={({field}) => (
1868
+ <FormItem>
1869
+ <FormLabel>Category</FormLabel>
1870
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
1871
+ <FormControl>
1872
+ <SelectTrigger>
1873
+ <SelectValue placeholder="Select a category" />
1874
+ </SelectTrigger>
1875
+ </FormControl>
1876
+ <SelectContent>
1877
+ <SelectItem value="web">Web Application</SelectItem>
1878
+ <SelectItem value="mobile">Mobile App</SelectItem>
1879
+ <SelectItem value="desktop">Desktop Application</SelectItem>
1880
+ <SelectItem value="other">Other</SelectItem>
1881
+ </SelectContent>
1882
+ </Select>
1883
+ <FormMessage />
1884
+ </FormItem>
1885
+ )}
1886
+ />
1887
+
1888
+ <FormField
1889
+ control={form.control}
1890
+ name="budget"
1891
+ render={({field}) => (
1892
+ <FormItem>
1893
+ <FormLabel>Budget (USD)</FormLabel>
1894
+ <FormControl>
1895
+ <Input type="text" placeholder="10000" {...field} />
1896
+ </FormControl>
1897
+ <FormMessage />
1898
+ </FormItem>
1899
+ )}
1900
+ />
1901
+
1902
+ <FormField
1903
+ control={form.control}
1904
+ name="description"
1905
+ render={({field}) => (
1906
+ <FormItem>
1907
+ <FormLabel>Description</FormLabel>
1908
+ <FormControl>
1909
+ <Textarea
1910
+ placeholder="Describe your project..."
1911
+ className={styles.textarea}
1912
+ {...field}
1913
+ />
1914
+ </FormControl>
1915
+ <FormMessage />
1916
+ </FormItem>
1917
+ )}
1918
+ />
1919
+
1920
+ <DialogFooter>
1921
+ <Button
1922
+ type="button"
1923
+ variant="outline"
1924
+ onClick={() => setOpen(false)}
1925
+ >
1926
+ Cancel
1927
+ </Button>
1928
+ <Button type="submit" disabled={form.formState.isSubmitting}>
1929
+ {form.formState.isSubmitting ? "Creating..." : "Create Project"}
1930
+ </Button>
1931
+ </DialogFooter>
1932
+ </form>
1933
+ </Form>
1934
+ </DialogContent>
1935
+ </Dialog>
1936
+ );
1937
+ }
1938
+ ```
1939
+
1940
+ ```css
1941
+ /* modal-form.module.css */
1942
+ .content {
1943
+ max-width: 32rem;
1944
+ }
1945
+
1946
+ .form {
1947
+ display: grid;
1948
+ gap: 1rem;
1949
+ padding-block: 1rem;
1950
+ }
1951
+
1952
+ .textarea {
1953
+ min-height: 6rem;
1954
+ resize: vertical;
1955
+ }
1956
+ ```
1957
+
1958
+ ---
1959
+
1960
+ ### Recipe 4: Toast Notifications (Sonner)
1961
+
1962
+ **Comprehensive toast notification patterns for all use cases.**
1963
+
1964
+ ```tsx
1965
+ import {CheckCircle2, Info, Loader2, XCircle} from "lucide-react";
1966
+
1967
+ import {Button} from "@arolariu/components/button";
1968
+ import {Card, CardContent, CardHeader, CardTitle} from "@arolariu/components/card";
1969
+ import {toast, Toaster} from "@arolariu/components/sonner";
1970
+ import styles from "./toast-demo.module.css";
1971
+
1972
+ export function ToastDemo() {
1973
+ // Basic toasts
1974
+ const showSuccess = () => {
1975
+ toast.success("Operation completed successfully!");
1976
+ };
1977
+
1978
+ const showError = () => {
1979
+ toast.error("Something went wrong. Please try again.");
1980
+ };
1981
+
1982
+ const showInfo = () => {
1983
+ toast.info("This is an informational message.");
1984
+ };
1985
+
1986
+ const showWarning = () => {
1987
+ toast.warning("Warning: This action cannot be undone!");
1988
+ };
1989
+
1990
+ // Toast with action
1991
+ const showWithAction = () => {
1992
+ toast.success("File uploaded successfully", {
1993
+ action: {
1994
+ label: "View",
1995
+ onClick: () => console.log("View clicked"),
1996
+ },
1997
+ });
1998
+ };
1999
+
2000
+ // Toast with description
2001
+ const showWithDescription = () => {
2002
+ toast.success("Project created", {
2003
+ description: "Your project has been created and is now live.",
2004
+ });
2005
+ };
2006
+
2007
+ // Promise toast (loading → success/error)
2008
+ const showPromiseToast = () => {
2009
+ const uploadPromise = new Promise((resolve, reject) => {
2010
+ setTimeout(() => {
2011
+ Math.random() > 0.5 ? resolve({name: "document.pdf"}) : reject(new Error("Upload failed"));
2012
+ }, 2000);
2013
+ });
2014
+
2015
+ toast.promise(uploadPromise, {
2016
+ loading: "Uploading file...",
2017
+ success: (data: {name: string}) => `${data.name} uploaded successfully!`,
2018
+ error: "Failed to upload file.",
2019
+ });
2020
+ };
2021
+
2022
+ // Custom styled toast
2023
+ const showCustomToast = () => {
2024
+ toast.custom(
2025
+ <div className={styles.customToast}>
2026
+ <CheckCircle2 className={styles.customIcon} />
2027
+ <div className={styles.customContent}>
2028
+ <div className={styles.customTitle}>Custom Toast</div>
2029
+ <div className={styles.customDescription}>
2030
+ This is a fully customized toast notification.
2031
+ </div>
2032
+ </div>
2033
+ </div>
2034
+ );
2035
+ };
2036
+
2037
+ // Loading toast (manual control)
2038
+ const showLoadingToast = () => {
2039
+ const toastId = toast.loading("Processing your request...");
2040
+
2041
+ setTimeout(() => {
2042
+ toast.success("Request processed!", {id: toastId});
2043
+ }, 3000);
2044
+ };
2045
+
2046
+ return (
2047
+ <>
2048
+ <Toaster position="top-right" richColors />
2049
+
2050
+ <div className={styles.container}>
2051
+ <Card>
2052
+ <CardHeader>
2053
+ <CardTitle>Toast Notification Examples</CardTitle>
2054
+ </CardHeader>
2055
+ <CardContent className={styles.grid}>
2056
+ <div className={styles.section}>
2057
+ <h3 className={styles.sectionTitle}>Basic Toasts</h3>
2058
+ <div className={styles.buttonGroup}>
2059
+ <Button onClick={showSuccess} variant="default">
2060
+ <CheckCircle2 />
2061
+ Success Toast
2062
+ </Button>
2063
+ <Button onClick={showError} variant="destructive">
2064
+ <XCircle />
2065
+ Error Toast
2066
+ </Button>
2067
+ <Button onClick={showInfo} variant="outline">
2068
+ <Info />
2069
+ Info Toast
2070
+ </Button>
2071
+ <Button onClick={showWarning} variant="outline">
2072
+ Warning Toast
2073
+ </Button>
2074
+ </div>
2075
+ </div>
2076
+
2077
+ <div className={styles.section}>
2078
+ <h3 className={styles.sectionTitle}>Advanced Toasts</h3>
2079
+ <div className={styles.buttonGroup}>
2080
+ <Button onClick={showWithAction} variant="secondary">
2081
+ Toast with Action
2082
+ </Button>
2083
+ <Button onClick={showWithDescription} variant="secondary">
2084
+ Toast with Description
2085
+ </Button>
2086
+ <Button onClick={showPromiseToast} variant="secondary">
2087
+ <Loader2 />
2088
+ Promise Toast
2089
+ </Button>
2090
+ <Button onClick={showLoadingToast} variant="secondary">
2091
+ Loading Toast
2092
+ </Button>
2093
+ </div>
2094
+ </div>
2095
+
2096
+ <div className={styles.section}>
2097
+ <h3 className={styles.sectionTitle}>Custom Toast</h3>
2098
+ <Button onClick={showCustomToast} variant="outline">
2099
+ Show Custom Toast
2100
+ </Button>
2101
+ </div>
2102
+ </CardContent>
2103
+ </Card>
2104
+ </div>
2105
+ </>
2106
+ );
2107
+ }
2108
+ ```
2109
+
2110
+ ```css
2111
+ /* toast-demo.module.css */
2112
+ .container {
2113
+ display: flex;
2114
+ align-items: center;
2115
+ justify-content: center;
2116
+ min-height: 100vh;
2117
+ padding: 1rem;
2118
+ }
2119
+
2120
+ .grid {
2121
+ display: grid;
2122
+ gap: 2rem;
2123
+ }
2124
+
2125
+ .section {
2126
+ display: grid;
2127
+ gap: 1rem;
2128
+ }
2129
+
2130
+ .sectionTitle {
2131
+ font-size: 1rem;
2132
+ font-weight: 600;
2133
+ }
2134
+
2135
+ .buttonGroup {
2136
+ display: grid;
2137
+ gap: 0.5rem;
2138
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
2139
+ }
2140
+
2141
+ .customToast {
2142
+ display: flex;
2143
+ align-items: flex-start;
2144
+ gap: 0.75rem;
2145
+ padding: 1rem;
2146
+ background-color: var(--ac-card);
2147
+ border: 1px solid var(--ac-border);
2148
+ border-radius: var(--ac-radius-md);
2149
+ box-shadow: var(--ac-shadow-lg);
2150
+ }
2151
+
2152
+ .customIcon {
2153
+ flex-shrink: 0;
2154
+ width: 1.25rem;
2155
+ height: 1.25rem;
2156
+ color: var(--ac-primary);
2157
+ }
2158
+
2159
+ .customContent {
2160
+ display: grid;
2161
+ gap: 0.25rem;
2162
+ }
2163
+
2164
+ .customTitle {
2165
+ font-weight: 600;
2166
+ }
2167
+
2168
+ .customDescription {
2169
+ font-size: 0.875rem;
2170
+ color: var(--ac-muted-foreground);
2171
+ }
2172
+ ```
2173
+
2174
+ ---
2175
+
2176
+ ### Recipe 5: Sidebar Navigation (with Keyboard Support)
2177
+
2178
+ **Responsive sidebar with keyboard navigation and active states.**
2179
+
2180
+ ```tsx
2181
+ import {
2182
+ ChevronDown,
2183
+ FileText,
2184
+ Home,
2185
+ Settings,
2186
+ Users,
2187
+ } from "lucide-react";
2188
+ import {useState} from "react";
2189
+
2190
+ import {
2191
+ Collapsible,
2192
+ CollapsibleContent,
2193
+ CollapsibleTrigger,
2194
+ } from "@arolariu/components/collapsible";
2195
+ import {
2196
+ Sidebar,
2197
+ SidebarContent,
2198
+ SidebarGroup,
2199
+ SidebarGroupContent,
2200
+ SidebarGroupLabel,
2201
+ SidebarMenu,
2202
+ SidebarMenuButton,
2203
+ SidebarMenuItem,
2204
+ SidebarMenuSub,
2205
+ SidebarMenuSubButton,
2206
+ SidebarMenuSubItem,
2207
+ SidebarProvider,
2208
+ SidebarTrigger,
2209
+ } from "@arolariu/components/sidebar";
2210
+ import styles from "./app-sidebar.module.css";
2211
+
2212
+ const menuItems = [
2213
+ {
2214
+ title: "Dashboard",
2215
+ icon: Home,
2216
+ url: "/dashboard",
2217
+ },
2218
+ {
2219
+ title: "Team",
2220
+ icon: Users,
2221
+ url: "/team",
2222
+ submenu: [
2223
+ {title: "Members", url: "/team/members"},
2224
+ {title: "Roles", url: "/team/roles"},
2225
+ {title: "Invitations", url: "/team/invitations"},
2226
+ ],
2227
+ },
2228
+ {
2229
+ title: "Projects",
2230
+ icon: FileText,
2231
+ url: "/projects",
2232
+ submenu: [
2233
+ {title: "Active", url: "/projects/active"},
2234
+ {title: "Archived", url: "/projects/archived"},
2235
+ {title: "Templates", url: "/projects/templates"},
2236
+ ],
2237
+ },
2238
+ {
2239
+ title: "Settings",
2240
+ icon: Settings,
2241
+ url: "/settings",
2242
+ },
2243
+ ];
2244
+
2245
+ export function AppSidebar() {
2246
+ const [activeItem, setActiveItem] = useState("/dashboard");
2247
+
2248
+ return (
2249
+ <SidebarProvider>
2250
+ <div className={styles.layout}>
2251
+ <Sidebar>
2252
+ <SidebarContent>
2253
+ <SidebarGroup>
2254
+ <SidebarGroupLabel>Application</SidebarGroupLabel>
2255
+ <SidebarGroupContent>
2256
+ <SidebarMenu>
2257
+ {menuItems.map((item) => {
2258
+ const isActive = activeItem === item.url || activeItem.startsWith(item.url + "/");
2259
+
2260
+ if (item.submenu) {
2261
+ return (
2262
+ <Collapsible key={item.title} defaultOpen={isActive}>
2263
+ <SidebarMenuItem>
2264
+ <CollapsibleTrigger asChild>
2265
+ <SidebarMenuButton isActive={isActive}>
2266
+ <item.icon />
2267
+ <span>{item.title}</span>
2268
+ <ChevronDown className={styles.chevron} />
2269
+ </SidebarMenuButton>
2270
+ </CollapsibleTrigger>
2271
+ <CollapsibleContent>
2272
+ <SidebarMenuSub>
2273
+ {item.submenu.map((subitem) => (
2274
+ <SidebarMenuSubItem key={subitem.title}>
2275
+ <SidebarMenuSubButton
2276
+ isActive={activeItem === subitem.url}
2277
+ onClick={() => setActiveItem(subitem.url)}
2278
+ >
2279
+ {subitem.title}
2280
+ </SidebarMenuSubButton>
2281
+ </SidebarMenuSubItem>
2282
+ ))}
2283
+ </SidebarMenuSub>
2284
+ </CollapsibleContent>
2285
+ </SidebarMenuItem>
2286
+ </Collapsible>
2287
+ );
2288
+ }
2289
+
2290
+ return (
2291
+ <SidebarMenuItem key={item.title}>
2292
+ <SidebarMenuButton
2293
+ isActive={isActive}
2294
+ onClick={() => setActiveItem(item.url)}
2295
+ >
2296
+ <item.icon />
2297
+ <span>{item.title}</span>
2298
+ </SidebarMenuButton>
2299
+ </SidebarMenuItem>
2300
+ );
2301
+ })}
2302
+ </SidebarMenu>
2303
+ </SidebarGroupContent>
2304
+ </SidebarGroup>
2305
+ </SidebarContent>
2306
+ </Sidebar>
2307
+
2308
+ <main className={styles.main}>
2309
+ <div className={styles.header}>
2310
+ <SidebarTrigger />
2311
+ <h1 className={styles.title}>Welcome to Dashboard</h1>
2312
+ </div>
2313
+
2314
+ <div className={styles.content}>
2315
+ <p>Current route: {activeItem}</p>
2316
+ </div>
2317
+ </main>
2318
+ </div>
2319
+ </SidebarProvider>
2320
+ );
2321
+ }
2322
+ ```
2323
+
2324
+ ```css
2325
+ /* app-sidebar.module.css */
2326
+ .layout {
2327
+ display: flex;
2328
+ min-height: 100vh;
2329
+ }
2330
+
2331
+ .main {
2332
+ flex: 1;
2333
+ display: grid;
2334
+ grid-template-rows: auto 1fr;
2335
+ }
2336
+
2337
+ .header {
2338
+ display: flex;
2339
+ align-items: center;
2340
+ gap: 1rem;
2341
+ padding: 1rem;
2342
+ border-bottom: 1px solid var(--ac-border);
2343
+ }
2344
+
2345
+ .title {
2346
+ font-size: 1.5rem;
2347
+ font-weight: 600;
2348
+ }
2349
+
2350
+ .content {
2351
+ padding: 2rem;
2352
+ }
2353
+
2354
+ .chevron {
2355
+ margin-left: auto;
2356
+ transition: transform 150ms;
2357
+ }
2358
+
2359
+ :global([data-state="open"]) .chevron {
2360
+ transform: rotate(180deg);
2361
+ }
2362
+ ```
2363
+
2364
+ ---
2365
+
2366
+ ### Recipe 6: Accessible Dropdown Menu (with Keyboard Nav)
2367
+
2368
+ **Fully accessible dropdown menu with keyboard shortcuts.**
2369
+
2370
+ ```tsx
2371
+ import {
2372
+ Copy,
2373
+ Download,
2374
+ Edit,
2375
+ LogOut,
2376
+ MoreVertical,
2377
+ Share2,
2378
+ Trash2,
2379
+ User,
2380
+ } from "lucide-react";
2381
+
2382
+ import {Button} from "@arolariu/components/button";
2383
+ import {
2384
+ DropdownMenu,
2385
+ DropdownMenuContent,
2386
+ DropdownMenuGroup,
2387
+ DropdownMenuItem,
2388
+ DropdownMenuLabel,
2389
+ DropdownMenuSeparator,
2390
+ DropdownMenuShortcut,
2391
+ DropdownMenuTrigger,
2392
+ } from "@arolariu/components/dropdown-menu";
2393
+ import {toast} from "@arolariu/components/sonner";
2394
+ import styles from "./dropdown-demo.module.css";
2395
+
2396
+ export function AccessibleDropdownMenu() {
2397
+ const handleAction = (action: string) => {
2398
+ toast.info(`Action: ${action}`);
2399
+ };
2400
+
2401
+ return (
2402
+ <div className={styles.container}>
2403
+ <DropdownMenu>
2404
+ <DropdownMenuTrigger render={<Button variant="outline" />}>
2405
+ <MoreVertical />
2406
+ Actions
2407
+ </DropdownMenuTrigger>
2408
+
2409
+ <DropdownMenuContent align="end" className={styles.content}>
2410
+ <DropdownMenuLabel>My Account</DropdownMenuLabel>
2411
+
2412
+ <DropdownMenuSeparator />
2413
+
2414
+ <DropdownMenuGroup>
2415
+ <DropdownMenuItem onClick={() => handleAction("Profile")}>
2416
+ <User />
2417
+ <span>Profile</span>
2418
+ <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
2419
+ </DropdownMenuItem>
2420
+
2421
+ <DropdownMenuItem onClick={() => handleAction("Edit")}>
2422
+ <Edit />
2423
+ <span>Edit</span>
2424
+ <DropdownMenuShortcut>⌘E</DropdownMenuShortcut>
2425
+ </DropdownMenuItem>
2426
+
2427
+ <DropdownMenuItem onClick={() => handleAction("Copy")}>
2428
+ <Copy />
2429
+ <span>Copy Link</span>
2430
+ <DropdownMenuShortcut>⌘C</DropdownMenuShortcut>
2431
+ </DropdownMenuItem>
2432
+
2433
+ <DropdownMenuItem onClick={() => handleAction("Share")}>
2434
+ <Share2 />
2435
+ <span>Share</span>
2436
+ <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
2437
+ </DropdownMenuItem>
2438
+ </DropdownMenuGroup>
2439
+
2440
+ <DropdownMenuSeparator />
2441
+
2442
+ <DropdownMenuGroup>
2443
+ <DropdownMenuItem onClick={() => handleAction("Download")}>
2444
+ <Download />
2445
+ <span>Download</span>
2446
+ <DropdownMenuShortcut>⌘D</DropdownMenuShortcut>
2447
+ </DropdownMenuItem>
2448
+ </DropdownMenuGroup>
2449
+
2450
+ <DropdownMenuSeparator />
2451
+
2452
+ <DropdownMenuItem
2453
+ onClick={() => handleAction("Delete")}
2454
+ className={styles.dangerItem}
2455
+ >
2456
+ <Trash2 />
2457
+ <span>Delete</span>
2458
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
2459
+ </DropdownMenuItem>
2460
+
2461
+ <DropdownMenuSeparator />
2462
+
2463
+ <DropdownMenuItem onClick={() => handleAction("Logout")}>
2464
+ <LogOut />
2465
+ <span>Log out</span>
2466
+ <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
2467
+ </DropdownMenuItem>
2468
+ </DropdownMenuContent>
2469
+ </DropdownMenu>
2470
+
2471
+ <div className={styles.instructions}>
2472
+ <p className={styles.instructionTitle}>Keyboard Navigation:</p>
2473
+ <ul className={styles.instructionList}>
2474
+ <li><kbd>Enter</kbd> or <kbd>Space</kbd> - Open menu</li>
2475
+ <li><kbd>↑</kbd> <kbd>↓</kbd> - Navigate items</li>
2476
+ <li><kbd>Enter</kbd> - Select item</li>
2477
+ <li><kbd>Esc</kbd> - Close menu</li>
2478
+ </ul>
2479
+ </div>
2480
+ </div>
2481
+ );
2482
+ }
2483
+ ```
2484
+
2485
+ ```css
2486
+ /* dropdown-demo.module.css */
2487
+ .container {
2488
+ display: flex;
2489
+ flex-direction: column;
2490
+ align-items: center;
2491
+ gap: 2rem;
2492
+ padding: 4rem 1rem;
2493
+ }
2494
+
2495
+ .content {
2496
+ min-width: 14rem;
2497
+ }
2498
+
2499
+ .dangerItem {
2500
+ color: var(--ac-destructive);
2501
+ }
2502
+
2503
+ .instructions {
2504
+ display: grid;
2505
+ gap: 0.5rem;
2506
+ padding: 1rem;
2507
+ background-color: var(--ac-muted);
2508
+ border-radius: var(--ac-radius-md);
2509
+ }
2510
+
2511
+ .instructionTitle {
2512
+ font-weight: 600;
2513
+ }
2514
+
2515
+ .instructionList {
2516
+ display: grid;
2517
+ gap: 0.25rem;
2518
+ padding-left: 1.5rem;
2519
+ font-size: 0.875rem;
2520
+ color: var(--ac-muted-foreground);
2521
+ }
2522
+
2523
+ .instructionList kbd {
2524
+ display: inline-block;
2525
+ padding: 0.125rem 0.375rem;
2526
+ background-color: var(--ac-background);
2527
+ border: 1px solid var(--ac-border);
2528
+ border-radius: var(--ac-radius-sm);
2529
+ font-family: var(--ac-font-mono);
2530
+ font-size: 0.75rem;
2531
+ }
2532
+ ```
2533
+
2534
+ ---
2535
+
2536
+ ### Recipe 7: Date Picker (Calendar Integration)
2537
+
2538
+ **Calendar-based date picker with range selection.**
2539
+
2540
+ ```tsx
2541
+ import {format} from "date-fns";
2542
+ import {Calendar as CalendarIcon} from "lucide-react";
2543
+ import {useState} from "react";
2544
+
2545
+ import {Button} from "@arolariu/components/button";
2546
+ import {Calendar} from "@arolariu/components/calendar";
2547
+ import {
2548
+ Popover,
2549
+ PopoverContent,
2550
+ PopoverTrigger,
2551
+ } from "@arolariu/components/popover";
2552
+ import {cn} from "@arolariu/components/utilities";
2553
+ import styles from "./date-picker.module.css";
2554
+
2555
+ export function DatePicker() {
2556
+ const [date, setDate] = useState<Date | undefined>();
2557
+
2558
+ return (
2559
+ <div className={styles.container}>
2560
+ <div className={styles.field}>
2561
+ <label className={styles.label}>Select Date</label>
2562
+ <Popover>
2563
+ <PopoverTrigger render={<Button variant="outline" className={styles.trigger} />}>
2564
+ <CalendarIcon className={styles.icon} />
2565
+ {date ? format(date, "PPP") : <span>Pick a date</span>}
2566
+ </PopoverTrigger>
2567
+ <PopoverContent align="start" className={styles.popoverContent}>
2568
+ <Calendar
2569
+ mode="single"
2570
+ selected={date}
2571
+ onSelect={setDate}
2572
+ initialFocus
2573
+ />
2574
+ </PopoverContent>
2575
+ </Popover>
2576
+ </div>
2577
+ </div>
2578
+ );
2579
+ }
2580
+
2581
+ export function DateRangePicker() {
2582
+ const [dateRange, setDateRange] = useState<{from: Date | undefined; to: Date | undefined}>({
2583
+ from: undefined,
2584
+ to: undefined,
2585
+ });
2586
+
2587
+ return (
2588
+ <div className={styles.container}>
2589
+ <div className={styles.field}>
2590
+ <label className={styles.label}>Select Date Range</label>
2591
+ <Popover>
2592
+ <PopoverTrigger render={<Button variant="outline" className={styles.trigger} />}>
2593
+ <CalendarIcon className={styles.icon} />
2594
+ {dateRange.from ? (
2595
+ dateRange.to ? (
2596
+ <>
2597
+ {format(dateRange.from, "LLL dd, y")} - {format(dateRange.to, "LLL dd, y")}
2598
+ </>
2599
+ ) : (
2600
+ format(dateRange.from, "LLL dd, y")
2601
+ )
2602
+ ) : (
2603
+ <span>Pick a date range</span>
2604
+ )}
2605
+ </PopoverTrigger>
2606
+ <PopoverContent align="start" className={styles.popoverContent}>
2607
+ <Calendar
2608
+ mode="range"
2609
+ selected={dateRange}
2610
+ onSelect={(range) =>
2611
+ setDateRange({
2612
+ from: range?.from,
2613
+ to: range?.to,
2614
+ })
2615
+ }
2616
+ numberOfMonths={2}
2617
+ initialFocus
2618
+ />
2619
+ </PopoverContent>
2620
+ </Popover>
2621
+ </div>
2622
+ </div>
2623
+ );
2624
+ }
2625
+ ```
2626
+
2627
+ ```css
2628
+ /* date-picker.module.css */
2629
+ .container {
2630
+ display: flex;
2631
+ align-items: center;
2632
+ justify-content: center;
2633
+ min-height: 100vh;
2634
+ padding: 1rem;
2635
+ }
2636
+
2637
+ .field {
2638
+ display: grid;
2639
+ gap: 0.5rem;
2640
+ width: min(24rem, 100%);
2641
+ }
2642
+
2643
+ .label {
2644
+ font-weight: 500;
2645
+ }
2646
+
2647
+ .trigger {
2648
+ justify-content: flex-start;
2649
+ text-align: left;
2650
+ font-weight: 400;
2651
+ }
2652
+
2653
+ .icon {
2654
+ width: 1rem;
2655
+ height: 1rem;
2656
+ margin-right: 0.5rem;
2657
+ }
2658
+
2659
+ .popoverContent {
2660
+ width: auto;
2661
+ padding: 0;
2662
+ }
2663
+ ```
2664
+
2665
+ ---
2666
+
2667
+ ### Recipe 8: File Upload Area (with Progress)
2668
+
2669
+ **Drag-and-drop file upload with progress tracking.**
2670
+
2671
+ ```tsx
2672
+ import {Upload, X} from "lucide-react";
2673
+ import {useState} from "react";
2674
+
2675
+ import {Button} from "@arolariu/components/button";
2676
+ import {Card, CardContent, CardHeader, CardTitle} from "@arolariu/components/card";
2677
+ import {Progress} from "@arolariu/components/progress";
2678
+ import {toast} from "@arolariu/components/sonner";
2679
+ import styles from "./file-upload.module.css";
2680
+
2681
+ interface FileUpload {
2682
+ id: string;
2683
+ file: File;
2684
+ progress: number;
2685
+ status: "uploading" | "complete" | "error";
2686
+ }
2687
+
2688
+ export function FileUploadArea() {
2689
+ const [uploads, setUploads] = useState<FileUpload[]>([]);
2690
+ const [isDragging, setIsDragging] = useState(false);
2691
+
2692
+ const handleDragOver = (e: React.DragEvent) => {
2693
+ e.preventDefault();
2694
+ setIsDragging(true);
2695
+ };
2696
+
2697
+ const handleDragLeave = () => {
2698
+ setIsDragging(false);
2699
+ };
2700
+
2701
+ const handleDrop = (e: React.DragEvent) => {
2702
+ e.preventDefault();
2703
+ setIsDragging(false);
2704
+
2705
+ const files = Array.from(e.dataTransfer.files);
2706
+ handleFiles(files);
2707
+ };
2708
+
2709
+ const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
2710
+ const files = e.target.files ? Array.from(e.target.files) : [];
2711
+ handleFiles(files);
2712
+ };
2713
+
2714
+ const handleFiles = (files: File[]) => {
2715
+ const newUploads: FileUpload[] = files.map((file) => ({
2716
+ id: Math.random().toString(36).substring(7),
2717
+ file,
2718
+ progress: 0,
2719
+ status: "uploading" as const,
2720
+ }));
2721
+
2722
+ setUploads((prev) => [...prev, ...newUploads]);
2723
+
2724
+ // Simulate upload progress
2725
+ newUploads.forEach((upload) => {
2726
+ simulateUpload(upload.id);
2727
+ });
2728
+ };
2729
+
2730
+ const simulateUpload = (id: string) => {
2731
+ const interval = setInterval(() => {
2732
+ setUploads((prev) =>
2733
+ prev.map((upload) => {
2734
+ if (upload.id === id) {
2735
+ const newProgress = Math.min(upload.progress + 10, 100);
2736
+ const newStatus = newProgress === 100 ? "complete" : upload.status;
2737
+
2738
+ if (newStatus === "complete") {
2739
+ clearInterval(interval);
2740
+ toast.success(`${upload.file.name} uploaded successfully`);
2741
+ }
2742
+
2743
+ return {...upload, progress: newProgress, status: newStatus};
2744
+ }
2745
+ return upload;
2746
+ })
2747
+ );
2748
+ }, 300);
2749
+ };
2750
+
2751
+ const removeUpload = (id: string) => {
2752
+ setUploads((prev) => prev.filter((upload) => upload.id !== id));
2753
+ };
2754
+
2755
+ return (
2756
+ <div className={styles.container}>
2757
+ <Card className={styles.card}>
2758
+ <CardHeader>
2759
+ <CardTitle>Upload Files</CardTitle>
2760
+ </CardHeader>
2761
+ <CardContent className={styles.content}>
2762
+ <div
2763
+ className={`${styles.dropZone} ${isDragging ? styles.dropZoneActive : ""}`}
2764
+ onDragOver={handleDragOver}
2765
+ onDragLeave={handleDragLeave}
2766
+ onDrop={handleDrop}
2767
+ >
2768
+ <Upload className={styles.uploadIcon} />
2769
+ <div className={styles.dropZoneText}>
2770
+ <p className={styles.dropZoneTitle}>Drop files here or click to upload</p>
2771
+ <p className={styles.dropZoneSubtitle}>
2772
+ Supports: Images, PDFs, Documents (Max 10MB)
2773
+ </p>
2774
+ </div>
2775
+ <input
2776
+ type="file"
2777
+ multiple
2778
+ className={styles.fileInput}
2779
+ onChange={handleFileInput}
2780
+ />
2781
+ </div>
2782
+
2783
+ {uploads.length > 0 ? (
2784
+ <div className={styles.uploadList}>
2785
+ {uploads.map((upload) => (
2786
+ <div key={upload.id} className={styles.uploadItem}>
2787
+ <div className={styles.uploadInfo}>
2788
+ <div className={styles.uploadName}>{upload.file.name}</div>
2789
+ <div className={styles.uploadSize}>
2790
+ {(upload.file.size / 1024 / 1024).toFixed(2)} MB
2791
+ </div>
2792
+ </div>
2793
+
2794
+ <div className={styles.uploadProgress}>
2795
+ <Progress value={upload.progress} className={styles.progressBar} />
2796
+ <span className={styles.uploadPercent}>{upload.progress}%</span>
2797
+ </div>
2798
+
2799
+ {upload.status === "complete" ? (
2800
+ <Button
2801
+ variant="ghost"
2802
+ size="icon"
2803
+ onClick={() => removeUpload(upload.id)}
2804
+ className={styles.removeButton}
2805
+ >
2806
+ <X />
2807
+ </Button>
2808
+ ) : null}
2809
+ </div>
2810
+ ))}
2811
+ </div>
2812
+ ) : null}
2813
+ </CardContent>
2814
+ </Card>
2815
+ </div>
2816
+ );
2817
+ }
2818
+ ```
2819
+
2820
+ ```css
2821
+ /* file-upload.module.css */
2822
+ .container {
2823
+ display: flex;
2824
+ align-items: center;
2825
+ justify-content: center;
2826
+ min-height: 100vh;
2827
+ padding: 1rem;
2828
+ }
2829
+
2830
+ .card {
2831
+ width: min(40rem, 100%);
2832
+ }
2833
+
2834
+ .content {
2835
+ display: grid;
2836
+ gap: 1.5rem;
2837
+ }
2838
+
2839
+ .dropZone {
2840
+ position: relative;
2841
+ display: flex;
2842
+ flex-direction: column;
2843
+ align-items: center;
2844
+ gap: 1rem;
2845
+ padding: 3rem 2rem;
2846
+ border: 2px dashed var(--ac-border);
2847
+ border-radius: var(--ac-radius-md);
2848
+ background-color: var(--ac-muted);
2849
+ cursor: pointer;
2850
+ transition: all 150ms;
2851
+ }
2852
+
2853
+ .dropZone:hover {
2854
+ border-color: var(--ac-primary);
2855
+ background-color: color-mix(in oklch, var(--ac-primary) 5%, var(--ac-muted));
2856
+ }
2857
+
2858
+ .dropZoneActive {
2859
+ border-color: var(--ac-primary);
2860
+ background-color: color-mix(in oklch, var(--ac-primary) 10%, var(--ac-muted));
2861
+ }
2862
+
2863
+ .uploadIcon {
2864
+ width: 3rem;
2865
+ height: 3rem;
2866
+ color: var(--ac-muted-foreground);
2867
+ }
2868
+
2869
+ .dropZoneText {
2870
+ text-align: center;
2871
+ }
2872
+
2873
+ .dropZoneTitle {
2874
+ font-weight: 600;
2875
+ }
2876
+
2877
+ .dropZoneSubtitle {
2878
+ margin-top: 0.25rem;
2879
+ font-size: 0.875rem;
2880
+ color: var(--ac-muted-foreground);
2881
+ }
2882
+
2883
+ .fileInput {
2884
+ position: absolute;
2885
+ inset: 0;
2886
+ width: 100%;
2887
+ height: 100%;
2888
+ opacity: 0;
2889
+ cursor: pointer;
2890
+ }
2891
+
2892
+ .uploadList {
2893
+ display: grid;
2894
+ gap: 1rem;
2895
+ }
2896
+
2897
+ .uploadItem {
2898
+ display: grid;
2899
+ grid-template-columns: 1fr auto;
2900
+ gap: 1rem;
2901
+ padding: 1rem;
2902
+ border: 1px solid var(--ac-border);
2903
+ border-radius: var(--ac-radius-md);
2904
+ }
2905
+
2906
+ .uploadInfo {
2907
+ display: grid;
2908
+ gap: 0.25rem;
2909
+ }
2910
+
2911
+ .uploadName {
2912
+ font-weight: 500;
2913
+ word-break: break-all;
2914
+ }
2915
+
2916
+ .uploadSize {
2917
+ font-size: 0.875rem;
2918
+ color: var(--ac-muted-foreground);
2919
+ }
2920
+
2921
+ .uploadProgress {
2922
+ grid-column: 1 / -1;
2923
+ display: flex;
2924
+ align-items: center;
2925
+ gap: 0.75rem;
2926
+ }
2927
+
2928
+ .progressBar {
2929
+ flex: 1;
2930
+ }
2931
+
2932
+ .uploadPercent {
2933
+ font-size: 0.875rem;
2934
+ font-weight: 500;
2935
+ color: var(--ac-muted-foreground);
2936
+ }
2937
+
2938
+ .removeButton {
2939
+ align-self: flex-start;
2940
+ }
2941
+ ```
2942
+
2943
+ ---
2944
+
2945
+ ### Recipe 9: Settings Page (Tabs + Form + Switch)
2946
+
2947
+ **Complete settings page with tabs and form controls.**
2948
+
2949
+ ```tsx
2950
+ import {zodResolver} from "@hookform/resolvers/zod";
2951
+ import {useForm} from "react-hook-form";
2952
+ import * as z from "zod";
2953
+
2954
+ import {Button} from "@arolariu/components/button";
2955
+ import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@arolariu/components/card";
2956
+ import {
2957
+ Form,
2958
+ FormControl,
2959
+ FormDescription,
2960
+ FormField,
2961
+ FormItem,
2962
+ FormLabel,
2963
+ FormMessage,
2964
+ } from "@arolariu/components/form";
2965
+ import {Input} from "@arolariu/components/input";
2966
+ import {
2967
+ Select,
2968
+ SelectContent,
2969
+ SelectItem,
2970
+ SelectTrigger,
2971
+ SelectValue,
2972
+ } from "@arolariu/components/select";
2973
+ import {Switch} from "@arolariu/components/switch";
2974
+ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@arolariu/components/tabs";
2975
+ import {Textarea} from "@arolariu/components/textarea";
2976
+ import {toast} from "@arolariu/components/sonner";
2977
+ import styles from "./settings-page.module.css";
2978
+
2979
+ const profileSchema = z.object({
2980
+ username: z.string().min(3, "Username must be at least 3 characters"),
2981
+ email: z.string().email("Please enter a valid email address"),
2982
+ bio: z.string().max(500, "Bio must be less than 500 characters").optional(),
2983
+ });
2984
+
2985
+ const notificationSchema = z.object({
2986
+ emailNotifications: z.boolean(),
2987
+ pushNotifications: z.boolean(),
2988
+ weeklyDigest: z.boolean(),
2989
+ });
2990
+
2991
+ const appearanceSchema = z.object({
2992
+ theme: z.enum(["light", "dark", "system"]),
2993
+ language: z.string(),
2994
+ });
2995
+
2996
+ export function SettingsPage() {
2997
+ const profileForm = useForm<z.infer<typeof profileSchema>>({
2998
+ resolver: zodResolver(profileSchema),
2999
+ defaultValues: {
3000
+ username: "johndoe",
3001
+ email: "john@example.com",
3002
+ bio: "",
3003
+ },
3004
+ });
3005
+
3006
+ const notificationForm = useForm<z.infer<typeof notificationSchema>>({
3007
+ resolver: zodResolver(notificationSchema),
3008
+ defaultValues: {
3009
+ emailNotifications: true,
3010
+ pushNotifications: false,
3011
+ weeklyDigest: true,
3012
+ },
3013
+ });
3014
+
3015
+ const appearanceForm = useForm<z.infer<typeof appearanceSchema>>({
3016
+ resolver: zodResolver(appearanceSchema),
3017
+ defaultValues: {
3018
+ theme: "system",
3019
+ language: "en",
3020
+ },
3021
+ });
3022
+
3023
+ const onProfileSubmit = (values: z.infer<typeof profileSchema>) => {
3024
+ console.log("Profile updated:", values);
3025
+ toast.success("Profile updated successfully!");
3026
+ };
3027
+
3028
+ const onNotificationSubmit = (values: z.infer<typeof notificationSchema>) => {
3029
+ console.log("Notifications updated:", values);
3030
+ toast.success("Notification preferences updated!");
3031
+ };
3032
+
3033
+ const onAppearanceSubmit = (values: z.infer<typeof appearanceSchema>) => {
3034
+ console.log("Appearance updated:", values);
3035
+ toast.success("Appearance settings updated!");
3036
+ };
3037
+
3038
+ return (
3039
+ <div className={styles.container}>
3040
+ <div className={styles.header}>
3041
+ <h1 className={styles.title}>Settings</h1>
3042
+ <p className={styles.subtitle}>
3043
+ Manage your account settings and preferences.
3044
+ </p>
3045
+ </div>
3046
+
3047
+ <Tabs defaultValue="profile" className={styles.tabs}>
3048
+ <TabsList>
3049
+ <TabsTrigger value="profile">Profile</TabsTrigger>
3050
+ <TabsTrigger value="notifications">Notifications</TabsTrigger>
3051
+ <TabsTrigger value="appearance">Appearance</TabsTrigger>
3052
+ </TabsList>
3053
+
3054
+ <TabsContent value="profile" className={styles.tabContent}>
3055
+ <Card>
3056
+ <CardHeader>
3057
+ <CardTitle>Profile Information</CardTitle>
3058
+ <CardDescription>
3059
+ Update your profile details and public information.
3060
+ </CardDescription>
3061
+ </CardHeader>
3062
+ <CardContent>
3063
+ <Form {...profileForm}>
3064
+ <form
3065
+ onSubmit={profileForm.handleSubmit(onProfileSubmit)}
3066
+ className={styles.form}
3067
+ >
3068
+ <FormField
3069
+ control={profileForm.control}
3070
+ name="username"
3071
+ render={({field}) => (
3072
+ <FormItem>
3073
+ <FormLabel>Username</FormLabel>
3074
+ <FormControl>
3075
+ <Input placeholder="johndoe" {...field} />
3076
+ </FormControl>
3077
+ <FormDescription>
3078
+ This is your public display name.
3079
+ </FormDescription>
3080
+ <FormMessage />
3081
+ </FormItem>
3082
+ )}
3083
+ />
3084
+
3085
+ <FormField
3086
+ control={profileForm.control}
3087
+ name="email"
3088
+ render={({field}) => (
3089
+ <FormItem>
3090
+ <FormLabel>Email</FormLabel>
3091
+ <FormControl>
3092
+ <Input type="email" placeholder="john@example.com" {...field} />
3093
+ </FormControl>
3094
+ <FormDescription>
3095
+ Your email address for account notifications.
3096
+ </FormDescription>
3097
+ <FormMessage />
3098
+ </FormItem>
3099
+ )}
3100
+ />
3101
+
3102
+ <FormField
3103
+ control={profileForm.control}
3104
+ name="bio"
3105
+ render={({field}) => (
3106
+ <FormItem>
3107
+ <FormLabel>Bio</FormLabel>
3108
+ <FormControl>
3109
+ <Textarea
3110
+ placeholder="Tell us about yourself..."
3111
+ className={styles.textarea}
3112
+ {...field}
3113
+ />
3114
+ </FormControl>
3115
+ <FormDescription>
3116
+ Brief description for your profile. Max 500 characters.
3117
+ </FormDescription>
3118
+ <FormMessage />
3119
+ </FormItem>
3120
+ )}
3121
+ />
3122
+
3123
+ <Button type="submit">Save Changes</Button>
3124
+ </form>
3125
+ </Form>
3126
+ </CardContent>
3127
+ </Card>
3128
+ </TabsContent>
3129
+
3130
+ <TabsContent value="notifications" className={styles.tabContent}>
3131
+ <Card>
3132
+ <CardHeader>
3133
+ <CardTitle>Notification Preferences</CardTitle>
3134
+ <CardDescription>
3135
+ Choose how you want to receive notifications.
3136
+ </CardDescription>
3137
+ </CardHeader>
3138
+ <CardContent>
3139
+ <Form {...notificationForm}>
3140
+ <form
3141
+ onSubmit={notificationForm.handleSubmit(onNotificationSubmit)}
3142
+ className={styles.form}
3143
+ >
3144
+ <FormField
3145
+ control={notificationForm.control}
3146
+ name="emailNotifications"
3147
+ render={({field}) => (
3148
+ <FormItem className={styles.switchItem}>
3149
+ <div className={styles.switchContent}>
3150
+ <FormLabel>Email Notifications</FormLabel>
3151
+ <FormDescription>
3152
+ Receive email updates about your account activity.
3153
+ </FormDescription>
3154
+ </div>
3155
+ <FormControl>
3156
+ <Switch
3157
+ checked={field.value}
3158
+ onCheckedChange={field.onChange}
3159
+ />
3160
+ </FormControl>
3161
+ </FormItem>
3162
+ )}
3163
+ />
3164
+
3165
+ <FormField
3166
+ control={notificationForm.control}
3167
+ name="pushNotifications"
3168
+ render={({field}) => (
3169
+ <FormItem className={styles.switchItem}>
3170
+ <div className={styles.switchContent}>
3171
+ <FormLabel>Push Notifications</FormLabel>
3172
+ <FormDescription>
3173
+ Get push notifications on your devices.
3174
+ </FormDescription>
3175
+ </div>
3176
+ <FormControl>
3177
+ <Switch
3178
+ checked={field.value}
3179
+ onCheckedChange={field.onChange}
3180
+ />
3181
+ </FormControl>
3182
+ </FormItem>
3183
+ )}
3184
+ />
3185
+
3186
+ <FormField
3187
+ control={notificationForm.control}
3188
+ name="weeklyDigest"
3189
+ render={({field}) => (
3190
+ <FormItem className={styles.switchItem}>
3191
+ <div className={styles.switchContent}>
3192
+ <FormLabel>Weekly Digest</FormLabel>
3193
+ <FormDescription>
3194
+ Receive a weekly summary of your activity.
3195
+ </FormDescription>
3196
+ </div>
3197
+ <FormControl>
3198
+ <Switch
3199
+ checked={field.value}
3200
+ onCheckedChange={field.onChange}
3201
+ />
3202
+ </FormControl>
3203
+ </FormItem>
3204
+ )}
3205
+ />
3206
+
3207
+ <Button type="submit">Save Preferences</Button>
3208
+ </form>
3209
+ </Form>
3210
+ </CardContent>
3211
+ </Card>
3212
+ </TabsContent>
3213
+
3214
+ <TabsContent value="appearance" className={styles.tabContent}>
3215
+ <Card>
3216
+ <CardHeader>
3217
+ <CardTitle>Appearance Settings</CardTitle>
3218
+ <CardDescription>
3219
+ Customize how the application looks and feels.
3220
+ </CardDescription>
3221
+ </CardHeader>
3222
+ <CardContent>
3223
+ <Form {...appearanceForm}>
3224
+ <form
3225
+ onSubmit={appearanceForm.handleSubmit(onAppearanceSubmit)}
3226
+ className={styles.form}
3227
+ >
3228
+ <FormField
3229
+ control={appearanceForm.control}
3230
+ name="theme"
3231
+ render={({field}) => (
3232
+ <FormItem>
3233
+ <FormLabel>Theme</FormLabel>
3234
+ <Select
3235
+ onValueChange={field.onChange}
3236
+ defaultValue={field.value}
3237
+ >
3238
+ <FormControl>
3239
+ <SelectTrigger>
3240
+ <SelectValue placeholder="Select a theme" />
3241
+ </SelectTrigger>
3242
+ </FormControl>
3243
+ <SelectContent>
3244
+ <SelectItem value="light">Light</SelectItem>
3245
+ <SelectItem value="dark">Dark</SelectItem>
3246
+ <SelectItem value="system">System</SelectItem>
3247
+ </SelectContent>
3248
+ </Select>
3249
+ <FormDescription>
3250
+ Choose your preferred color scheme.
3251
+ </FormDescription>
3252
+ <FormMessage />
3253
+ </FormItem>
3254
+ )}
3255
+ />
3256
+
3257
+ <FormField
3258
+ control={appearanceForm.control}
3259
+ name="language"
3260
+ render={({field}) => (
3261
+ <FormItem>
3262
+ <FormLabel>Language</FormLabel>
3263
+ <Select
3264
+ onValueChange={field.onChange}
3265
+ defaultValue={field.value}
3266
+ >
3267
+ <FormControl>
3268
+ <SelectTrigger>
3269
+ <SelectValue placeholder="Select a language" />
3270
+ </SelectTrigger>
3271
+ </FormControl>
3272
+ <SelectContent>
3273
+ <SelectItem value="en">English</SelectItem>
3274
+ <SelectItem value="es">Español</SelectItem>
3275
+ <SelectItem value="fr">Français</SelectItem>
3276
+ <SelectItem value="de">Deutsch</SelectItem>
3277
+ </SelectContent>
3278
+ </Select>
3279
+ <FormDescription>
3280
+ Select your preferred language.
3281
+ </FormDescription>
3282
+ <FormMessage />
3283
+ </FormItem>
3284
+ )}
3285
+ />
3286
+
3287
+ <Button type="submit">Save Settings</Button>
3288
+ </form>
3289
+ </Form>
3290
+ </CardContent>
3291
+ </Card>
3292
+ </TabsContent>
3293
+ </Tabs>
3294
+ </div>
3295
+ );
3296
+ }
3297
+ ```
3298
+
3299
+ ```css
3300
+ /* settings-page.module.css */
3301
+ .container {
3302
+ max-width: 56rem;
3303
+ margin-inline: auto;
3304
+ padding: 2rem 1rem;
3305
+ }
3306
+
3307
+ .header {
3308
+ margin-bottom: 2rem;
3309
+ }
3310
+
3311
+ .title {
3312
+ font-size: 2rem;
3313
+ font-weight: 700;
3314
+ }
3315
+
3316
+ .subtitle {
3317
+ margin-top: 0.5rem;
3318
+ color: var(--ac-muted-foreground);
3319
+ }
3320
+
3321
+ .tabs {
3322
+ display: grid;
3323
+ gap: 1rem;
3324
+ }
3325
+
3326
+ .tabContent {
3327
+ margin-top: 1rem;
3328
+ }
3329
+
3330
+ .form {
3331
+ display: grid;
3332
+ gap: 1.5rem;
3333
+ }
3334
+
3335
+ .textarea {
3336
+ min-height: 6rem;
3337
+ resize: vertical;
3338
+ }
3339
+
3340
+ .switchItem {
3341
+ display: flex;
3342
+ flex-direction: row;
3343
+ align-items: center;
3344
+ justify-content: space-between;
3345
+ padding: 1rem;
3346
+ border: 1px solid var(--ac-border);
3347
+ border-radius: var(--ac-radius-md);
3348
+ }
3349
+
3350
+ .switchContent {
3351
+ display: grid;
3352
+ gap: 0.25rem;
3353
+ }
3354
+ ```
3355
+
3356
+ ---
3357
+
3358
+ ### Recipe 10: Error Handling (ErrorBoundary + AsyncBoundary + Toast)
3359
+
3360
+ **Comprehensive error handling pattern with boundaries and toast notifications.**
3361
+
3362
+ ```tsx
3363
+ import {AlertTriangle, RefreshCw} from "lucide-react";
3364
+ import {Component, Suspense, type ReactNode} from "react";
3365
+
3366
+ import {Alert, AlertDescription, AlertTitle} from "@arolariu/components/alert";
3367
+ import {AsyncBoundary} from "@arolariu/components/async-boundary";
3368
+ import {Button} from "@arolariu/components/button";
3369
+ import {Card, CardContent, CardHeader, CardTitle} from "@arolariu/components/card";
3370
+ import {ErrorBoundary} from "@arolariu/components/error-boundary";
3371
+ import {Skeleton} from "@arolariu/components/skeleton";
3372
+ import {toast} from "@arolariu/components/sonner";
3373
+ import styles from "./error-handling.module.css";
3374
+
3375
+ // Async data fetching component
3376
+ async function fetchUserData(userId: string) {
3377
+ await new Promise((resolve) => setTimeout(resolve, 1500));
3378
+
3379
+ // Simulate random error
3380
+ if (Math.random() > 0.7) {
3381
+ throw new Error("Failed to fetch user data");
3382
+ }
3383
+
3384
+ return {
3385
+ id: userId,
3386
+ name: "John Doe",
3387
+ email: "john@example.com",
3388
+ };
3389
+ }
3390
+
3391
+ function UserProfile({userId}: {userId: string}) {
3392
+ const user = use(fetchUserData(userId));
3393
+
3394
+ return (
3395
+ <Card>
3396
+ <CardHeader>
3397
+ <CardTitle>User Profile</CardTitle>
3398
+ </CardHeader>
3399
+ <CardContent className={styles.profile}>
3400
+ <div className={styles.profileField}>
3401
+ <span className={styles.profileLabel}>Name:</span>
3402
+ <span>{user.name}</span>
3403
+ </div>
3404
+ <div className={styles.profileField}>
3405
+ <span className={styles.profileLabel}>Email:</span>
3406
+ <span>{user.email}</span>
3407
+ </div>
3408
+ </CardContent>
3409
+ </Card>
3410
+ );
3411
+ }
3412
+
3413
+ // Loading fallback
3414
+ function ProfileSkeleton() {
3415
+ return (
3416
+ <Card>
3417
+ <CardHeader>
3418
+ <Skeleton className={styles.skeletonTitle} />
3419
+ </CardHeader>
3420
+ <CardContent className={styles.skeletonContent}>
3421
+ <Skeleton className={styles.skeletonField} />
3422
+ <Skeleton className={styles.skeletonField} />
3423
+ </CardContent>
3424
+ </Card>
3425
+ );
3426
+ }
3427
+
3428
+ // Error fallback
3429
+ function ProfileError({error, reset}: {error: Error; reset: () => void}) {
3430
+ return (
3431
+ <Alert variant="destructive">
3432
+ <AlertTriangle />
3433
+ <AlertTitle>Error Loading Profile</AlertTitle>
3434
+ <AlertDescription className={styles.errorDescription}>
3435
+ {error.message}
3436
+ <Button
3437
+ variant="outline"
3438
+ size="sm"
3439
+ onClick={reset}
3440
+ className={styles.retryButton}
3441
+ >
3442
+ <RefreshCw />
3443
+ Retry
3444
+ </Button>
3445
+ </AlertDescription>
3446
+ </Alert>
3447
+ );
3448
+ }
3449
+
3450
+ export function ErrorHandlingDemo() {
3451
+ const handleAPIError = async () => {
3452
+ try {
3453
+ // Simulate API call that fails
3454
+ await new Promise((_, reject) =>
3455
+ setTimeout(() => reject(new Error("API request failed")), 1000)
3456
+ );
3457
+ } catch (error) {
3458
+ toast.error("Failed to load data. Please try again.", {
3459
+ action: {
3460
+ label: "Retry",
3461
+ onClick: () => handleAPIError(),
3462
+ },
3463
+ });
3464
+ }
3465
+ };
3466
+
3467
+ const handleValidationError = () => {
3468
+ toast.error("Validation failed: Email is required");
3469
+ };
3470
+
3471
+ const handleNetworkError = () => {
3472
+ toast.error("Network error. Please check your connection.", {
3473
+ duration: 5000,
3474
+ });
3475
+ };
3476
+
3477
+ return (
3478
+ <div className={styles.container}>
3479
+ <div className={styles.header}>
3480
+ <h1 className={styles.title}>Error Handling Patterns</h1>
3481
+ <p className={styles.subtitle}>
3482
+ Comprehensive error handling with boundaries and toast notifications.
3483
+ </p>
3484
+ </div>
3485
+
3486
+ <div className={styles.grid}>
3487
+ {/* Pattern 1: AsyncBoundary (React 19 pattern) */}
3488
+ <div className={styles.section}>
3489
+ <h2 className={styles.sectionTitle}>1. AsyncBoundary (Suspense + ErrorBoundary)</h2>
3490
+ <AsyncBoundary
3491
+ suspenseFallback={<ProfileSkeleton />}
3492
+ errorFallback={({error, reset}) => <ProfileError error={error} reset={reset} />}
3493
+ >
3494
+ <UserProfile userId="123" />
3495
+ </AsyncBoundary>
3496
+ </div>
3497
+
3498
+ {/* Pattern 2: Toast for API errors */}
3499
+ <div className={styles.section}>
3500
+ <h2 className={styles.sectionTitle}>2. Toast Notifications for Errors</h2>
3501
+ <Card>
3502
+ <CardContent className={styles.buttonGroup}>
3503
+ <Button onClick={handleAPIError} variant="destructive">
3504
+ Trigger API Error
3505
+ </Button>
3506
+ <Button onClick={handleValidationError} variant="destructive">
3507
+ Trigger Validation Error
3508
+ </Button>
3509
+ <Button onClick={handleNetworkError} variant="destructive">
3510
+ Trigger Network Error
3511
+ </Button>
3512
+ </CardContent>
3513
+ </Card>
3514
+ </div>
3515
+
3516
+ {/* Pattern 3: Inline error display */}
3517
+ <div className={styles.section}>
3518
+ <h2 className={styles.sectionTitle}>3. Inline Error Display</h2>
3519
+ <Alert variant="destructive">
3520
+ <AlertTriangle />
3521
+ <AlertTitle>Authentication Error</AlertTitle>
3522
+ <AlertDescription>
3523
+ Your session has expired. Please log in again to continue.
3524
+ <Button
3525
+ variant="outline"
3526
+ size="sm"
3527
+ className={styles.loginButton}
3528
+ onClick={() => toast.info("Redirecting to login...")}
3529
+ >
3530
+ Log In
3531
+ </Button>
3532
+ </AlertDescription>
3533
+ </Alert>
3534
+ </div>
3535
+ </div>
3536
+ </div>
3537
+ );
3538
+ }
3539
+
3540
+ // React 19 use() hook polyfill (remove when React 19 is stable)
3541
+ function use<T>(promise: Promise<T>): T {
3542
+ if ((promise as any).status === "fulfilled") {
3543
+ return (promise as any).value;
3544
+ } else if ((promise as any).status === "rejected") {
3545
+ throw (promise as any).reason;
3546
+ } else {
3547
+ throw promise.then(
3548
+ (value) => {
3549
+ (promise as any).status = "fulfilled";
3550
+ (promise as any).value = value;
3551
+ },
3552
+ (reason) => {
3553
+ (promise as any).status = "rejected";
3554
+ (promise as any).reason = reason;
3555
+ }
3556
+ );
3557
+ }
3558
+ }
3559
+ ```
3560
+
3561
+ ```css
3562
+ /* error-handling.module.css */
3563
+ .container {
3564
+ max-width: 56rem;
3565
+ margin-inline: auto;
3566
+ padding: 2rem 1rem;
3567
+ }
3568
+
3569
+ .header {
3570
+ margin-bottom: 2rem;
3571
+ }
3572
+
3573
+ .title {
3574
+ font-size: 2rem;
3575
+ font-weight: 700;
3576
+ }
3577
+
3578
+ .subtitle {
3579
+ margin-top: 0.5rem;
3580
+ color: var(--ac-muted-foreground);
3581
+ }
3582
+
3583
+ .grid {
3584
+ display: grid;
3585
+ gap: 2rem;
3586
+ }
3587
+
3588
+ .section {
3589
+ display: grid;
3590
+ gap: 1rem;
3591
+ }
3592
+
3593
+ .sectionTitle {
3594
+ font-size: 1.25rem;
3595
+ font-weight: 600;
3596
+ }
3597
+
3598
+ .profile {
3599
+ display: grid;
3600
+ gap: 0.75rem;
3601
+ }
3602
+
3603
+ .profileField {
3604
+ display: flex;
3605
+ gap: 0.5rem;
3606
+ }
3607
+
3608
+ .profileLabel {
3609
+ font-weight: 600;
3610
+ }
3611
+
3612
+ .skeletonTitle {
3613
+ height: 1.5rem;
3614
+ width: 8rem;
3615
+ }
3616
+
3617
+ .skeletonContent {
3618
+ display: grid;
3619
+ gap: 0.75rem;
3620
+ }
3621
+
3622
+ .skeletonField {
3623
+ height: 1rem;
3624
+ }
3625
+
3626
+ .errorDescription {
3627
+ display: flex;
3628
+ align-items: center;
3629
+ gap: 1rem;
3630
+ margin-top: 0.5rem;
3631
+ }
3632
+
3633
+ .retryButton,
3634
+ .loginButton {
3635
+ margin-left: auto;
3636
+ }
3637
+
3638
+ .buttonGroup {
3639
+ display: grid;
3640
+ gap: 0.5rem;
3641
+ padding: 1.5rem;
3642
+ }
3643
+ ```
3644
+
3645
+ ---
3646
+
3647
+ ## 🎓 Key Takeaways from Pattern Recipes
3648
+
3649
+ ### ✅ Best Practices Demonstrated
3650
+
3651
+ 1. **Form Validation**: Always use `zod` + `react-hook-form` for type-safe validation
3652
+ 2. **Error Handling**: Combine boundaries, inline errors, and toast notifications
3653
+ 3. **Loading States**: Use `Skeleton` components and `AsyncBoundary` for async data
3654
+ 4. **Accessibility**: Keyboard navigation, ARIA attributes, and semantic HTML
3655
+ 5. **Responsive Design**: CSS Modules with container queries and media queries
3656
+ 6. **Type Safety**: Leverage namespace types (`Component.Props`, `Component.State`)
3657
+ 7. **Composition**: Use `render` prop for element composition (Base UI pattern)
3658
+ 8. **Toast Notifications**: Use `toast.promise()` for async operations
3659
+ 9. **State Management**: Keep form state close to components, lift when needed
3660
+ 10. **Progressive Enhancement**: Start with semantic HTML, enhance with JavaScript
3661
+
3662
+ ### 📚 Additional Resources
3663
+
3664
+ - [Base UI Documentation](https://base-ui.com/react/components)
3665
+ - [React Hook Form](https://react-hook-form.com/)
3666
+ - [Zod Validation](https://zod.dev/)
3667
+ - [TanStack Table](https://tanstack.com/table)
3668
+ - [Date-fns](https://date-fns.org/)
3669
+
3670
+ Ready to build something amazing? **[🚀 Start with our Quick Start Guide](./README.md#-quick-start)**