shadcn-rails 0.1.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 (315) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +40 -0
  3. data/CHANGELOG.md +54 -0
  4. data/CLAUDE.md +463 -0
  5. data/PROGRESS.md +485 -0
  6. data/README.md +1483 -0
  7. data/Rakefile +29 -0
  8. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +13 -0
  9. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +46 -0
  10. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +111 -0
  11. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +27 -0
  12. data/__tests__/controllers/accordion_controller.test.js +904 -0
  13. data/__tests__/controllers/calendar_controller.test.js +1370 -0
  14. data/__tests__/controllers/carousel_controller.test.js +912 -0
  15. data/__tests__/controllers/checkbox_controller.test.js +454 -0
  16. data/__tests__/controllers/collapsible_controller.test.js +407 -0
  17. data/__tests__/controllers/combobox_controller.test.js +966 -0
  18. data/__tests__/controllers/context_menu_controller.test.js +627 -0
  19. data/__tests__/controllers/date_picker_controller.test.js +636 -0
  20. data/__tests__/controllers/dialog_controller.test.js +878 -0
  21. data/__tests__/controllers/drawer_controller.test.js +995 -0
  22. data/__tests__/controllers/menubar_controller.test.js +736 -0
  23. data/__tests__/controllers/navigation_menu_controller.test.js +598 -0
  24. data/__tests__/controllers/popover_controller.test.js +1007 -0
  25. data/__tests__/controllers/radio_group_controller.test.js +640 -0
  26. data/__tests__/controllers/resizable_controller.test.js +680 -0
  27. data/__tests__/controllers/select_controller.test.js +674 -0
  28. data/__tests__/controllers/sheet_controller.test.js +986 -0
  29. data/__tests__/controllers/slider_controller.test.js +1036 -0
  30. data/__tests__/controllers/switch_controller.test.js +424 -0
  31. data/__tests__/controllers/tabs_controller.test.js +907 -0
  32. data/__tests__/controllers/toggle_group_controller.test.js +839 -0
  33. data/__tests__/controllers/tooltip_controller.test.js +808 -0
  34. data/__tests__/helpers/stimulus-test-helper.js +203 -0
  35. data/app/assets/config/manifest.js +1 -0
  36. data/app/assets/javascripts/shadcn/controllers/accordion_controller.d.ts +53 -0
  37. data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +140 -0
  38. data/app/assets/javascripts/shadcn/controllers/avatar_controller.d.ts +22 -0
  39. data/app/assets/javascripts/shadcn/controllers/avatar_controller.js +26 -0
  40. data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +592 -0
  41. data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +263 -0
  42. data/app/assets/javascripts/shadcn/controllers/checkbox_controller.d.ts +31 -0
  43. data/app/assets/javascripts/shadcn/controllers/checkbox_controller.js +48 -0
  44. data/app/assets/javascripts/shadcn/controllers/collapsible_controller.d.ts +43 -0
  45. data/app/assets/javascripts/shadcn/controllers/collapsible_controller.js +73 -0
  46. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +234 -0
  47. data/app/assets/javascripts/shadcn/controllers/command_controller.js +141 -0
  48. data/app/assets/javascripts/shadcn/controllers/command_dialog_controller.js +162 -0
  49. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +202 -0
  50. data/app/assets/javascripts/shadcn/controllers/date_picker_controller.js +282 -0
  51. data/app/assets/javascripts/shadcn/controllers/dialog_controller.d.ts +67 -0
  52. data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +187 -0
  53. data/app/assets/javascripts/shadcn/controllers/drawer_controller.d.ts +58 -0
  54. data/app/assets/javascripts/shadcn/controllers/drawer_controller.js +112 -0
  55. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.d.ts +83 -0
  56. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +225 -0
  57. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.d.ts +59 -0
  58. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +143 -0
  59. data/app/assets/javascripts/shadcn/controllers/input_otp_controller.d.ts +44 -0
  60. data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +206 -0
  61. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +323 -0
  62. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +251 -0
  63. data/app/assets/javascripts/shadcn/controllers/popover_controller.d.ts +56 -0
  64. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +141 -0
  65. data/app/assets/javascripts/shadcn/controllers/radio_group_controller.d.ts +47 -0
  66. data/app/assets/javascripts/shadcn/controllers/radio_group_controller.js +108 -0
  67. data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +272 -0
  68. data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.d.ts +44 -0
  69. data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.js +74 -0
  70. data/app/assets/javascripts/shadcn/controllers/select_controller.d.ts +84 -0
  71. data/app/assets/javascripts/shadcn/controllers/select_controller.js +222 -0
  72. data/app/assets/javascripts/shadcn/controllers/sheet_controller.d.ts +60 -0
  73. data/app/assets/javascripts/shadcn/controllers/sheet_controller.js +151 -0
  74. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +148 -0
  75. data/app/assets/javascripts/shadcn/controllers/slider_controller.d.ts +102 -0
  76. data/app/assets/javascripts/shadcn/controllers/slider_controller.js +364 -0
  77. data/app/assets/javascripts/shadcn/controllers/switch_controller.d.ts +46 -0
  78. data/app/assets/javascripts/shadcn/controllers/switch_controller.js +78 -0
  79. data/app/assets/javascripts/shadcn/controllers/tabs_controller.d.ts +51 -0
  80. data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +126 -0
  81. data/app/assets/javascripts/shadcn/controllers/toast_controller.d.ts +37 -0
  82. data/app/assets/javascripts/shadcn/controllers/toast_controller.js +58 -0
  83. data/app/assets/javascripts/shadcn/controllers/toggle_controller.d.ts +27 -0
  84. data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +42 -0
  85. data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.d.ts +44 -0
  86. data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +68 -0
  87. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.d.ts +56 -0
  88. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +117 -0
  89. data/app/assets/javascripts/shadcn/index.d.ts +74 -0
  90. data/app/assets/javascripts/shadcn/index.js +133 -0
  91. data/app/assets/stylesheets/.keep +0 -0
  92. data/app/assets/stylesheets/shadcn/base.css +445 -0
  93. data/app/assets/stylesheets/shadcn/components.css +513 -0
  94. data/app/assets/stylesheets/shadcn/index.css +18 -0
  95. data/app/assets/stylesheets/shadcn/themes/gray.css +68 -0
  96. data/app/assets/stylesheets/shadcn/themes/slate.css +68 -0
  97. data/app/assets/stylesheets/shadcn/themes/stone.css +68 -0
  98. data/app/assets/stylesheets/shadcn/themes/zinc.css +68 -0
  99. data/app/components/shadcn/accordion_component.rb +63 -0
  100. data/app/components/shadcn/accordion_content_component.rb +29 -0
  101. data/app/components/shadcn/accordion_item_component.rb +40 -0
  102. data/app/components/shadcn/accordion_trigger_component.rb +49 -0
  103. data/app/components/shadcn/alert_component.rb +75 -0
  104. data/app/components/shadcn/alert_description_component.rb +12 -0
  105. data/app/components/shadcn/alert_dialog_action_component.rb +24 -0
  106. data/app/components/shadcn/alert_dialog_cancel_component.rb +24 -0
  107. data/app/components/shadcn/alert_dialog_component.rb +71 -0
  108. data/app/components/shadcn/alert_dialog_content_component.rb +57 -0
  109. data/app/components/shadcn/alert_dialog_description_component.rb +12 -0
  110. data/app/components/shadcn/alert_dialog_footer_component.rb +19 -0
  111. data/app/components/shadcn/alert_dialog_header_component.rb +19 -0
  112. data/app/components/shadcn/alert_dialog_title_component.rb +12 -0
  113. data/app/components/shadcn/alert_title_component.rb +12 -0
  114. data/app/components/shadcn/aspect_ratio_component.rb +49 -0
  115. data/app/components/shadcn/avatar_component.rb +107 -0
  116. data/app/components/shadcn/avatar_fallback_component.rb +17 -0
  117. data/app/components/shadcn/badge_component.rb +49 -0
  118. data/app/components/shadcn/base_component.rb +100 -0
  119. data/app/components/shadcn/breadcrumb_component.rb +70 -0
  120. data/app/components/shadcn/breadcrumb_item_component.rb +50 -0
  121. data/app/components/shadcn/button_component.rb +141 -0
  122. data/app/components/shadcn/button_group_component.rb +69 -0
  123. data/app/components/shadcn/calendar_component.rb +337 -0
  124. data/app/components/shadcn/card_action_component.rb +10 -0
  125. data/app/components/shadcn/card_component.rb +63 -0
  126. data/app/components/shadcn/card_content_component.rb +19 -0
  127. data/app/components/shadcn/card_description_component.rb +12 -0
  128. data/app/components/shadcn/card_footer_component.rb +12 -0
  129. data/app/components/shadcn/card_header_component.rb +24 -0
  130. data/app/components/shadcn/card_title_component.rb +18 -0
  131. data/app/components/shadcn/carousel_component.rb +275 -0
  132. data/app/components/shadcn/checkbox_component.rb +103 -0
  133. data/app/components/shadcn/collapsible_component.rb +66 -0
  134. data/app/components/shadcn/collapsible_content_component.rb +28 -0
  135. data/app/components/shadcn/combobox_component.rb +322 -0
  136. data/app/components/shadcn/command_component.rb +52 -0
  137. data/app/components/shadcn/command_dialog_component.rb +76 -0
  138. data/app/components/shadcn/command_empty_component.rb +12 -0
  139. data/app/components/shadcn/command_group_component.rb +34 -0
  140. data/app/components/shadcn/command_input_component.rb +59 -0
  141. data/app/components/shadcn/command_item_component.rb +48 -0
  142. data/app/components/shadcn/command_list_component.rb +38 -0
  143. data/app/components/shadcn/command_separator_component.rb +12 -0
  144. data/app/components/shadcn/command_shortcut_component.rb +12 -0
  145. data/app/components/shadcn/context_menu_component.rb +64 -0
  146. data/app/components/shadcn/context_menu_content_component.rb +44 -0
  147. data/app/components/shadcn/context_menu_item_component.rb +63 -0
  148. data/app/components/shadcn/context_menu_label_component.rb +18 -0
  149. data/app/components/shadcn/context_menu_separator_component.rb +12 -0
  150. data/app/components/shadcn/context_menu_shortcut_component.rb +12 -0
  151. data/app/components/shadcn/date_picker_component.rb +368 -0
  152. data/app/components/shadcn/dialog_component.rb +77 -0
  153. data/app/components/shadcn/dialog_content_component.rb +91 -0
  154. data/app/components/shadcn/dialog_description_component.rb +12 -0
  155. data/app/components/shadcn/dialog_footer_component.rb +12 -0
  156. data/app/components/shadcn/dialog_header_component.rb +19 -0
  157. data/app/components/shadcn/dialog_title_component.rb +12 -0
  158. data/app/components/shadcn/drawer_component.rb +72 -0
  159. data/app/components/shadcn/drawer_content_component.rb +76 -0
  160. data/app/components/shadcn/drawer_description_component.rb +12 -0
  161. data/app/components/shadcn/drawer_footer_component.rb +12 -0
  162. data/app/components/shadcn/drawer_header_component.rb +19 -0
  163. data/app/components/shadcn/drawer_title_component.rb +12 -0
  164. data/app/components/shadcn/dropdown_menu_component.rb +75 -0
  165. data/app/components/shadcn/dropdown_menu_content_component.rb +49 -0
  166. data/app/components/shadcn/dropdown_menu_group_component.rb +10 -0
  167. data/app/components/shadcn/dropdown_menu_item_component.rb +63 -0
  168. data/app/components/shadcn/dropdown_menu_label_component.rb +18 -0
  169. data/app/components/shadcn/dropdown_menu_separator_component.rb +12 -0
  170. data/app/components/shadcn/dropdown_menu_shortcut_component.rb +12 -0
  171. data/app/components/shadcn/empty_component.rb +48 -0
  172. data/app/components/shadcn/empty_content_component.rb +12 -0
  173. data/app/components/shadcn/empty_description_component.rb +12 -0
  174. data/app/components/shadcn/empty_header_component.rb +29 -0
  175. data/app/components/shadcn/empty_media_component.rb +21 -0
  176. data/app/components/shadcn/empty_title_component.rb +12 -0
  177. data/app/components/shadcn/field_component.rb +113 -0
  178. data/app/components/shadcn/hover_card_component.rb +64 -0
  179. data/app/components/shadcn/hover_card_content_component.rb +36 -0
  180. data/app/components/shadcn/input_component.rb +108 -0
  181. data/app/components/shadcn/input_group_component.rb +70 -0
  182. data/app/components/shadcn/input_otp_component.rb +183 -0
  183. data/app/components/shadcn/item_actions_component.rb +12 -0
  184. data/app/components/shadcn/item_component.rb +98 -0
  185. data/app/components/shadcn/item_content_component.rb +24 -0
  186. data/app/components/shadcn/item_description_component.rb +12 -0
  187. data/app/components/shadcn/item_footer_component.rb +12 -0
  188. data/app/components/shadcn/item_group_component.rb +24 -0
  189. data/app/components/shadcn/item_header_component.rb +12 -0
  190. data/app/components/shadcn/item_media_component.rb +22 -0
  191. data/app/components/shadcn/item_separator_component.rb +12 -0
  192. data/app/components/shadcn/item_title_component.rb +12 -0
  193. data/app/components/shadcn/kbd_component.rb +36 -0
  194. data/app/components/shadcn/label_component.rb +49 -0
  195. data/app/components/shadcn/menubar_checkbox_item_component.rb +76 -0
  196. data/app/components/shadcn/menubar_component.rb +56 -0
  197. data/app/components/shadcn/menubar_content_component.rb +64 -0
  198. data/app/components/shadcn/menubar_item_component.rb +65 -0
  199. data/app/components/shadcn/menubar_label_component.rb +27 -0
  200. data/app/components/shadcn/menubar_menu_component.rb +34 -0
  201. data/app/components/shadcn/menubar_radio_group_component.rb +42 -0
  202. data/app/components/shadcn/menubar_radio_item_component.rb +76 -0
  203. data/app/components/shadcn/menubar_separator_component.rb +22 -0
  204. data/app/components/shadcn/menubar_shortcut_component.rb +21 -0
  205. data/app/components/shadcn/menubar_sub_component.rb +38 -0
  206. data/app/components/shadcn/menubar_sub_content_component.rb +45 -0
  207. data/app/components/shadcn/menubar_sub_trigger_component.rb +59 -0
  208. data/app/components/shadcn/menubar_trigger_component.rb +31 -0
  209. data/app/components/shadcn/native_select_component.rb +150 -0
  210. data/app/components/shadcn/navigation_menu_component.rb +76 -0
  211. data/app/components/shadcn/navigation_menu_content_component.rb +30 -0
  212. data/app/components/shadcn/navigation_menu_item_component.rb +39 -0
  213. data/app/components/shadcn/navigation_menu_link_component.rb +38 -0
  214. data/app/components/shadcn/navigation_menu_list_component.rb +29 -0
  215. data/app/components/shadcn/navigation_menu_trigger_component.rb +59 -0
  216. data/app/components/shadcn/pagination_component.rb +195 -0
  217. data/app/components/shadcn/pagination_content_component.rb +47 -0
  218. data/app/components/shadcn/pagination_ellipsis_component.rb +30 -0
  219. data/app/components/shadcn/pagination_item_component.rb +53 -0
  220. data/app/components/shadcn/pagination_next_component.rb +48 -0
  221. data/app/components/shadcn/pagination_previous_component.rb +48 -0
  222. data/app/components/shadcn/popover_component.rb +76 -0
  223. data/app/components/shadcn/popover_content_component.rb +25 -0
  224. data/app/components/shadcn/progress_component.rb +77 -0
  225. data/app/components/shadcn/radio_group_component.rb +129 -0
  226. data/app/components/shadcn/radio_group_item_component.rb +109 -0
  227. data/app/components/shadcn/resizable_handle_component.rb +98 -0
  228. data/app/components/shadcn/resizable_panel_component.rb +56 -0
  229. data/app/components/shadcn/resizable_panel_group_component.rb +94 -0
  230. data/app/components/shadcn/scroll_area_component.rb +110 -0
  231. data/app/components/shadcn/select_component.rb +151 -0
  232. data/app/components/shadcn/select_group_component.rb +32 -0
  233. data/app/components/shadcn/select_item_component.rb +59 -0
  234. data/app/components/shadcn/select_separator_component.rb +12 -0
  235. data/app/components/shadcn/separator_component.rb +54 -0
  236. data/app/components/shadcn/sheet_component.rb +82 -0
  237. data/app/components/shadcn/sheet_content_component.rb +95 -0
  238. data/app/components/shadcn/sheet_description_component.rb +12 -0
  239. data/app/components/shadcn/sheet_footer_component.rb +12 -0
  240. data/app/components/shadcn/sheet_header_component.rb +19 -0
  241. data/app/components/shadcn/sheet_title_component.rb +12 -0
  242. data/app/components/shadcn/sidebar_component.rb +180 -0
  243. data/app/components/shadcn/sidebar_content_component.rb +32 -0
  244. data/app/components/shadcn/sidebar_footer_component.rb +24 -0
  245. data/app/components/shadcn/sidebar_group_action_component.rb +26 -0
  246. data/app/components/shadcn/sidebar_group_component.rb +38 -0
  247. data/app/components/shadcn/sidebar_group_content_component.rb +32 -0
  248. data/app/components/shadcn/sidebar_group_label_component.rb +25 -0
  249. data/app/components/shadcn/sidebar_header_component.rb +24 -0
  250. data/app/components/shadcn/sidebar_inset_component.rb +25 -0
  251. data/app/components/shadcn/sidebar_menu_action_component.rb +37 -0
  252. data/app/components/shadcn/sidebar_menu_badge_component.rb +25 -0
  253. data/app/components/shadcn/sidebar_menu_button_component.rb +52 -0
  254. data/app/components/shadcn/sidebar_menu_component.rb +32 -0
  255. data/app/components/shadcn/sidebar_menu_item_component.rb +41 -0
  256. data/app/components/shadcn/sidebar_menu_skeleton_component.rb +46 -0
  257. data/app/components/shadcn/sidebar_menu_sub_button_component.rb +43 -0
  258. data/app/components/shadcn/sidebar_menu_sub_component.rb +33 -0
  259. data/app/components/shadcn/sidebar_menu_sub_item_component.rb +30 -0
  260. data/app/components/shadcn/sidebar_provider_component.rb +57 -0
  261. data/app/components/shadcn/sidebar_rail_component.rb +30 -0
  262. data/app/components/shadcn/sidebar_separator_component.rb +24 -0
  263. data/app/components/shadcn/sidebar_trigger_component.rb +51 -0
  264. data/app/components/shadcn/skeleton_component.rb +29 -0
  265. data/app/components/shadcn/slider_component.rb +76 -0
  266. data/app/components/shadcn/spinner_component.rb +67 -0
  267. data/app/components/shadcn/switch_component.rb +147 -0
  268. data/app/components/shadcn/table_body_component.rb +16 -0
  269. data/app/components/shadcn/table_caption_component.rb +12 -0
  270. data/app/components/shadcn/table_cell_component.rb +12 -0
  271. data/app/components/shadcn/table_component.rb +57 -0
  272. data/app/components/shadcn/table_footer_component.rb +16 -0
  273. data/app/components/shadcn/table_head_component.rb +12 -0
  274. data/app/components/shadcn/table_header_component.rb +16 -0
  275. data/app/components/shadcn/table_row_component.rb +40 -0
  276. data/app/components/shadcn/tabs_component.rb +78 -0
  277. data/app/components/shadcn/tabs_content_component.rb +32 -0
  278. data/app/components/shadcn/tabs_list_component.rb +30 -0
  279. data/app/components/shadcn/tabs_trigger_component.rb +37 -0
  280. data/app/components/shadcn/textarea_component.rb +84 -0
  281. data/app/components/shadcn/toast_action_component.rb +18 -0
  282. data/app/components/shadcn/toast_component.rb +114 -0
  283. data/app/components/shadcn/toast_description_component.rb +12 -0
  284. data/app/components/shadcn/toast_title_component.rb +12 -0
  285. data/app/components/shadcn/toast_viewport_component.rb +12 -0
  286. data/app/components/shadcn/toggle_component.rb +77 -0
  287. data/app/components/shadcn/toggle_group_component.rb +96 -0
  288. data/app/components/shadcn/toggle_group_item_component.rb +62 -0
  289. data/app/components/shadcn/tooltip_component.rb +89 -0
  290. data/app/components/shadcn/typography_component.rb +112 -0
  291. data/babel.config.cjs +5 -0
  292. data/bin/console +11 -0
  293. data/bin/setup +8 -0
  294. data/config/importmap.rb +5 -0
  295. data/fly.toml +26 -0
  296. data/jest.config.js +19 -0
  297. data/jest.setup.js +8 -0
  298. data/lib/generators/shadcn/component/component_generator.rb +188 -0
  299. data/lib/generators/shadcn/install/install_generator.rb +140 -0
  300. data/lib/generators/shadcn/install/templates/initializer.rb.tt +35 -0
  301. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +35 -0
  302. data/lib/generators/shadcn/theme/theme_generator.rb +128 -0
  303. data/lib/shadcn/rails/class_merger.rb +228 -0
  304. data/lib/shadcn/rails/configuration.rb +341 -0
  305. data/lib/shadcn/rails/engine.rb +59 -0
  306. data/lib/shadcn/rails/helpers/class_name_helper.rb +35 -0
  307. data/lib/shadcn/rails/helpers/component_helper.rb +60 -0
  308. data/lib/shadcn/rails/helpers/pagination_helper.rb +187 -0
  309. data/lib/shadcn/rails/version.rb +7 -0
  310. data/lib/shadcn/rails.rb +179 -0
  311. data/package-lock.json +7415 -0
  312. data/package.json +68 -0
  313. data/rollup.config.js +29 -0
  314. data/sig/shadcn/rails.rbs +6 -0
  315. metadata +526 -0
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Table component for displaying tabular data
5
+ # Matches shadcn/ui Table component
6
+ #
7
+ # @example Basic table
8
+ # <%= render Shadcn::TableComponent.new do |table| %>
9
+ # <% table.with_header do |header| %>
10
+ # <% header.with_row do |row| %>
11
+ # <% row.with_head { "Name" } %>
12
+ # <% row.with_head { "Email" } %>
13
+ # <% row.with_head { "Role" } %>
14
+ # <% end %>
15
+ # <% end %>
16
+ # <% table.with_body do |body| %>
17
+ # <% @users.each do |user| %>
18
+ # <% body.with_row do |row| %>
19
+ # <% row.with_cell { user.name } %>
20
+ # <% row.with_cell { user.email } %>
21
+ # <% row.with_cell { user.role } %>
22
+ # <% end %>
23
+ # <% end %>
24
+ # <% end %>
25
+ # <% end %>
26
+ #
27
+ class TableComponent < BaseComponent
28
+ BASE_CLASSES = "w-full caption-bottom text-sm"
29
+
30
+ renders_one :caption, lambda { |**options|
31
+ TableCaptionComponent.new(**options)
32
+ }
33
+ renders_one :header, lambda { |**options|
34
+ TableHeaderComponent.new(**options)
35
+ }
36
+ renders_one :body, lambda { |**options|
37
+ TableBodyComponent.new(**options)
38
+ }
39
+ renders_one :footer, lambda { |**options|
40
+ TableFooterComponent.new(**options)
41
+ }
42
+
43
+ def call
44
+ content_tag(:div, table_element, class: "relative w-full overflow-auto")
45
+ end
46
+
47
+ private
48
+
49
+ def table_element
50
+ content_tag(:table, table_content, class: merge_classes(BASE_CLASSES))
51
+ end
52
+
53
+ def table_content
54
+ safe_join([caption, header, body, footer, content].compact)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Table Footer component
5
+ class TableFooterComponent < BaseComponent
6
+ BASE_CLASSES = "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0"
7
+
8
+ renders_many :rows, lambda { |**options, &block|
9
+ TableRowComponent.new(**options, &block)
10
+ }
11
+
12
+ def call
13
+ content_tag(:tfoot, safe_join([rows, content].compact.flatten), class: merge_classes(BASE_CLASSES))
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Table Head component
5
+ class TableHeadComponent < BaseComponent
6
+ BASE_CLASSES = "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"
7
+
8
+ def call
9
+ content_tag(:th, content, class: merge_classes(BASE_CLASSES), **html_options)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Table Header component
5
+ class TableHeaderComponent < BaseComponent
6
+ BASE_CLASSES = "[&_tr]:border-b"
7
+
8
+ renders_many :rows, lambda { |**options, &block|
9
+ TableRowComponent.new(**options, &block)
10
+ }
11
+
12
+ def call
13
+ content_tag(:thead, safe_join([rows, content].compact.flatten), class: merge_classes(BASE_CLASSES))
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Table Row component
5
+ class TableRowComponent < BaseComponent
6
+ BASE_CLASSES = "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
7
+
8
+ renders_many :heads, lambda { |**options, &block|
9
+ TableHeadComponent.new(**options, &block)
10
+ }
11
+ renders_many :cells, lambda { |**options, &block|
12
+ TableCellComponent.new(**options, &block)
13
+ }
14
+
15
+ # @param selected [Boolean] Whether row is selected
16
+ def initialize(selected: false, **options, &block)
17
+ super(**options, &block)
18
+ @selected = selected
19
+ end
20
+
21
+ def call
22
+ content_tag(:tr, row_content, row_attributes)
23
+ end
24
+
25
+ private
26
+
27
+ def row_content
28
+ safe_join([heads, cells, content].compact.flatten)
29
+ end
30
+
31
+ def row_attributes
32
+ attrs = {
33
+ class: merge_classes(BASE_CLASSES),
34
+ "data-state": @selected ? "selected" : nil
35
+ }
36
+ attrs.merge!(html_options)
37
+ attrs.compact
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Tabs component for tabbed interfaces
5
+ # Matches shadcn/ui Tabs component
6
+ # Uses Stimulus for interactivity
7
+ #
8
+ # @example Basic tabs
9
+ # <%= render Shadcn::TabsComponent.new(default_value: "account") do |tabs| %>
10
+ # <% tabs.with_list do |list| %>
11
+ # <% list.with_trigger(value: "account") { "Account" } %>
12
+ # <% list.with_trigger(value: "password") { "Password" } %>
13
+ # <% end %>
14
+ # <% tabs.with_panel(value: "account") do %>
15
+ # Account settings content
16
+ # <% end %>
17
+ # <% tabs.with_panel(value: "password") do %>
18
+ # Password settings content
19
+ # <% end %>
20
+ # <% end %>
21
+ #
22
+ # @example Tabs with URL sync
23
+ # <%= render Shadcn::TabsComponent.new(default_value: "account", url_param: "tab") do |tabs| %>
24
+ # <% tabs.with_list do |list| %>
25
+ # <% list.with_trigger(value: "account") { "Account" } %>
26
+ # <% list.with_trigger(value: "password") { "Password" } %>
27
+ # <% end %>
28
+ # <% tabs.with_panel(value: "account") do %>
29
+ # Account settings content
30
+ # <% end %>
31
+ # <% tabs.with_panel(value: "password") do %>
32
+ # Password settings content
33
+ # <% end %>
34
+ # <% end %>
35
+ # # URL will update to ?tab=account or ?tab=password when tabs are clicked
36
+ #
37
+ class TabsComponent < BaseComponent
38
+ renders_one :list, lambda { |**options|
39
+ TabsListComponent.new(**options)
40
+ }
41
+ renders_many :panels, lambda { |value:, **options, &block|
42
+ TabsContentComponent.new(value: value, **options, &block)
43
+ }
44
+
45
+ # @param default_value [String] The value of the initially active tab
46
+ # @param orientation [Symbol] Orientation (:horizontal, :vertical)
47
+ # @param url_param [String, nil] Query parameter name to sync active tab with URL (e.g., "tab")
48
+ def initialize(default_value: nil, orientation: :horizontal, url_param: nil, **options)
49
+ super(**options)
50
+ @default_value = default_value
51
+ @orientation = orientation
52
+ @url_param = url_param
53
+ end
54
+
55
+ def call
56
+ content_tag(:div, tabs_content, tabs_attributes)
57
+ end
58
+
59
+ private
60
+
61
+ def tabs_content
62
+ safe_join([list, panels, content].compact.flatten)
63
+ end
64
+
65
+ def tabs_attributes
66
+ attrs = {
67
+ class: class_name,
68
+ "data-controller": "shadcn--tabs",
69
+ "data-shadcn--tabs-default-value": @default_value,
70
+ "data-shadcn--tabs-url-param-value": @url_param,
71
+ "data-orientation": @orientation.to_s
72
+ }
73
+ attrs.merge!(html_options)
74
+ attrs.merge!(build_data)
75
+ attrs.compact
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Tabs Content component
5
+ class TabsContentComponent < BaseComponent
6
+ BASE_CLASSES = "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
7
+
8
+ # @param value [String] The value that identifies this tab panel
9
+ def initialize(value:, **options)
10
+ super(**options)
11
+ @value = value
12
+ end
13
+
14
+ def call
15
+ content_tag(:div, content, content_attributes)
16
+ end
17
+
18
+ private
19
+
20
+ def content_attributes
21
+ {
22
+ class: merge_classes(BASE_CLASSES),
23
+ role: "tabpanel",
24
+ "data-shadcn--tabs-target": "content",
25
+ "data-value": @value,
26
+ "data-state": "inactive",
27
+ hidden: true,
28
+ tabindex: "0"
29
+ }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Tabs List component
5
+ class TabsListComponent < BaseComponent
6
+ BASE_CLASSES = "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground"
7
+
8
+ renders_many :triggers, lambda { |value:, **options, &block|
9
+ TabsTriggerComponent.new(value: value, **options, &block)
10
+ }
11
+
12
+ def call
13
+ content_tag(:div, list_content, list_attributes)
14
+ end
15
+
16
+ private
17
+
18
+ def list_content
19
+ safe_join([triggers, content].compact.flatten)
20
+ end
21
+
22
+ def list_attributes
23
+ {
24
+ class: merge_classes(BASE_CLASSES),
25
+ role: "tablist",
26
+ "data-shadcn--tabs-target": "list"
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Tabs Trigger component
5
+ class TabsTriggerComponent < BaseComponent
6
+ BASE_CLASSES = "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow"
7
+
8
+ # @param value [String] The value that identifies this tab
9
+ # @param disabled [Boolean] Whether the tab is disabled
10
+ def initialize(value:, disabled: false, **options)
11
+ super(**options)
12
+ @value = value
13
+ @disabled = disabled
14
+ end
15
+
16
+ def call
17
+ content_tag(:button, content, trigger_attributes)
18
+ end
19
+
20
+ private
21
+
22
+ def trigger_attributes
23
+ {
24
+ type: "button",
25
+ role: "tab",
26
+ class: merge_classes(BASE_CLASSES),
27
+ disabled: @disabled || nil,
28
+ "data-shadcn--tabs-target": "trigger",
29
+ "data-value": @value,
30
+ "data-state": "inactive",
31
+ "data-action": "click->shadcn--tabs#selectTab",
32
+ "aria-selected": "false",
33
+ tabindex: "-1"
34
+ }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Textarea component for multi-line text input
5
+ # Matches shadcn/ui Textarea component
6
+ #
7
+ # @example Basic textarea
8
+ # <%= render Shadcn::TextareaComponent.new(name: "bio", placeholder: "Tell us about yourself") %>
9
+ #
10
+ # @example With rows
11
+ # <%= render Shadcn::TextareaComponent.new(name: "message", rows: 6) %>
12
+ #
13
+ class TextareaComponent < BaseComponent
14
+ BASE_CLASSES = "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
15
+
16
+ # @param name [String, nil] Textarea name attribute
17
+ # @param id [String, nil] Textarea id attribute
18
+ # @param value [String, nil] Initial value
19
+ # @param placeholder [String, nil] Placeholder text
20
+ # @param rows [Integer] Number of visible rows
21
+ # @param cols [Integer, nil] Number of visible columns
22
+ # @param disabled [Boolean] Whether textarea is disabled
23
+ # @param required [Boolean] Whether textarea is required
24
+ # @param readonly [Boolean] Whether textarea is readonly
25
+ # @param autofocus [Boolean] Whether to autofocus
26
+ # @param minlength [Integer, nil] Minimum length
27
+ # @param maxlength [Integer, nil] Maximum length
28
+ def initialize(
29
+ name: nil,
30
+ id: nil,
31
+ value: nil,
32
+ placeholder: nil,
33
+ rows: 3,
34
+ cols: nil,
35
+ disabled: false,
36
+ required: false,
37
+ readonly: false,
38
+ autofocus: false,
39
+ minlength: nil,
40
+ maxlength: nil,
41
+ **options
42
+ )
43
+ super(**options)
44
+ @name = name
45
+ @id = id
46
+ @value = value
47
+ @placeholder = placeholder
48
+ @rows = rows
49
+ @cols = cols
50
+ @disabled = disabled
51
+ @required = required
52
+ @readonly = readonly
53
+ @autofocus = autofocus
54
+ @minlength = minlength
55
+ @maxlength = maxlength
56
+ end
57
+
58
+ def call
59
+ content_tag(:textarea, @value || content, textarea_attributes)
60
+ end
61
+
62
+ private
63
+
64
+ def textarea_attributes
65
+ attrs = {
66
+ name: @name,
67
+ id: @id,
68
+ placeholder: @placeholder,
69
+ rows: @rows,
70
+ cols: @cols,
71
+ disabled: @disabled || nil,
72
+ required: @required || nil,
73
+ readonly: @readonly || nil,
74
+ autofocus: @autofocus || nil,
75
+ minlength: @minlength,
76
+ maxlength: @maxlength,
77
+ class: merge_classes(BASE_CLASSES)
78
+ }
79
+ attrs.merge!(html_options)
80
+ attrs.merge!(build_data)
81
+ attrs.compact
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Toast Action component
5
+ class ToastActionComponent < BaseComponent
6
+ BASE_CLASSES = "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive"
7
+
8
+ # @param alt_text [String] Alternative text for accessibility
9
+ def initialize(alt_text:, **options, &block)
10
+ super(**options, &block)
11
+ @alt_text = alt_text
12
+ end
13
+
14
+ def call
15
+ content_tag(:div, content, class: merge_classes(BASE_CLASSES), "aria-label": @alt_text)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Toast component for notifications
5
+ # Matches shadcn/ui Toast component
6
+ # Uses Stimulus for interactivity
7
+ #
8
+ # @example Basic toast (typically rendered via JavaScript/Turbo)
9
+ # <%= render Shadcn::ToastComponent.new(variant: :default) do |toast| %>
10
+ # <% toast.with_title { "Scheduled" } %>
11
+ # <% toast.with_description { "Your message has been scheduled." } %>
12
+ # <% end %>
13
+ #
14
+ # @example Destructive toast
15
+ # <%= render Shadcn::ToastComponent.new(variant: :destructive) do |toast| %>
16
+ # <% toast.with_title { "Error" } %>
17
+ # <% toast.with_description { "Something went wrong." } %>
18
+ # <% toast.with_action(alt_text: "Try again") do %>
19
+ # <%= render Shadcn::ButtonComponent.new(variant: :outline, size: :sm) { "Try again" } %>
20
+ # <% end %>
21
+ # <% end %>
22
+ #
23
+ class ToastComponent < BaseComponent
24
+ VARIANTS = {
25
+ default: "border bg-background text-foreground",
26
+ destructive: "destructive group border-destructive bg-destructive text-destructive-foreground"
27
+ }.freeze
28
+
29
+ BASE_CLASSES = "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full"
30
+
31
+ renders_one :title, lambda { |**options|
32
+ ToastTitleComponent.new(**options)
33
+ }
34
+ renders_one :description, lambda { |**options|
35
+ ToastDescriptionComponent.new(**options)
36
+ }
37
+ renders_one :action, lambda { |alt_text:, **options, &block|
38
+ ToastActionComponent.new(alt_text: alt_text, **options, &block)
39
+ }
40
+ renders_one :close
41
+
42
+ # @param variant [Symbol] Toast variant (:default, :destructive)
43
+ # @param duration [Integer] Auto-dismiss duration in ms (0 for no auto-dismiss)
44
+ # @param open [Boolean] Whether toast is visible
45
+ def initialize(variant: :default, duration: 5000, open: true, **options)
46
+ super(**options)
47
+ @variant = variant.to_sym
48
+ @duration = duration
49
+ @open = open
50
+ end
51
+
52
+ def call
53
+ content_tag(:li, toast_content, toast_attributes)
54
+ end
55
+
56
+ private
57
+
58
+ def toast_content
59
+ safe_join([
60
+ content_wrapper,
61
+ action,
62
+ close_button
63
+ ].compact)
64
+ end
65
+
66
+ def content_wrapper
67
+ content_tag(:div, safe_join([title, description, content].compact), class: "grid gap-1")
68
+ end
69
+
70
+ def close_button
71
+ close || default_close_button
72
+ end
73
+
74
+ def default_close_button
75
+ content_tag(:button, close_icon, {
76
+ type: "button",
77
+ class: "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
78
+ "data-action": "click->shadcn--toast#close",
79
+ "aria-label": "Close"
80
+ })
81
+ end
82
+
83
+ def close_icon
84
+ content_tag(:svg,
85
+ content_tag(:path, nil, d: "M18 6 6 18M6 6l12 12", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round"),
86
+ xmlns: "http://www.w3.org/2000/svg",
87
+ width: "16",
88
+ height: "16",
89
+ viewBox: "0 0 24 24",
90
+ fill: "none",
91
+ class: "h-4 w-4"
92
+ )
93
+ end
94
+
95
+ def toast_classes
96
+ cn(BASE_CLASSES, VARIANTS[@variant], class_name)
97
+ end
98
+
99
+ def toast_attributes
100
+ attrs = {
101
+ class: toast_classes,
102
+ role: "status",
103
+ "aria-live": "polite",
104
+ "data-controller": "shadcn--toast",
105
+ "data-shadcn--toast-duration-value": @duration,
106
+ "data-shadcn--toast-open-value": @open.to_s,
107
+ "data-state": @open ? "open" : "closed"
108
+ }
109
+ attrs.merge!(html_options)
110
+ attrs.merge!(build_data)
111
+ attrs.compact
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Toast Description component
5
+ class ToastDescriptionComponent < BaseComponent
6
+ BASE_CLASSES = "text-sm opacity-90"
7
+
8
+ def call
9
+ content_tag(:div, content, class: merge_classes(BASE_CLASSES))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Toast Title component
5
+ class ToastTitleComponent < BaseComponent
6
+ BASE_CLASSES = "text-sm font-semibold [&+div]:text-xs"
7
+
8
+ def call
9
+ content_tag(:div, content, class: merge_classes(BASE_CLASSES))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Toast Viewport (container for all toasts)
5
+ class ToastViewportComponent < BaseComponent
6
+ BASE_CLASSES = "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]"
7
+
8
+ def call
9
+ content_tag(:ol, content, class: merge_classes(BASE_CLASSES), tabindex: "-1", "data-shadcn--toaster-target": "viewport")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Toggle component for a two-state button
5
+ # Matches shadcn/ui Toggle component
6
+ #
7
+ # @example Basic usage
8
+ # <%= render Shadcn::ToggleComponent.new(aria_label: "Toggle bold") do %>
9
+ # <svg>...</svg>
10
+ # <% end %>
11
+ #
12
+ # @example Outline variant
13
+ # <%= render Shadcn::ToggleComponent.new(variant: :outline, pressed: true) do %>
14
+ # Italic
15
+ # <% end %>
16
+ #
17
+ class ToggleComponent < BaseComponent
18
+ VARIANTS = {
19
+ default: "bg-transparent",
20
+ outline: "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground"
21
+ }.freeze
22
+
23
+ SIZES = {
24
+ sm: "h-8 px-2",
25
+ default: "h-9 px-3",
26
+ lg: "h-10 px-3"
27
+ }.freeze
28
+
29
+ BASE_CLASSES = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground"
30
+
31
+ # @param variant [Symbol] :default or :outline
32
+ # @param size [Symbol] :sm, :default, or :lg
33
+ # @param pressed [Boolean] Initial pressed state
34
+ # @param disabled [Boolean] Whether toggle is disabled
35
+ # @param aria_label [String] Accessibility label
36
+ def initialize(
37
+ variant: :default,
38
+ size: :default,
39
+ pressed: false,
40
+ disabled: false,
41
+ aria_label: nil,
42
+ **options
43
+ )
44
+ super(**options)
45
+ @variant = variant
46
+ @size = size
47
+ @pressed = pressed
48
+ @disabled = disabled
49
+ @aria_label = aria_label
50
+ end
51
+
52
+ def call
53
+ content_tag(:button, toggle_attributes) do
54
+ content
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def toggle_attributes
61
+ attrs = {
62
+ type: "button",
63
+ class: cn(BASE_CLASSES, VARIANTS[@variant], SIZES[@size], class_name),
64
+ disabled: @disabled || nil,
65
+ "aria-pressed": @pressed.to_s,
66
+ "aria-label": @aria_label,
67
+ "data-state": @pressed ? "on" : "off",
68
+ "data-controller": "shadcn--toggle",
69
+ "data-action": "click->shadcn--toggle#toggle",
70
+ "data-shadcn--toggle-pressed-value": @pressed.to_s
71
+ }
72
+ attrs.merge!(html_options)
73
+ attrs.merge!(build_data)
74
+ attrs.compact
75
+ end
76
+ end
77
+ end