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,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dialog Content component
5
+ class DialogContentComponent < BaseComponent
6
+ OVERLAY_CLASSES = "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
7
+ CONTENT_CLASSES = "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg"
8
+ CLOSE_CLASSES = "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
9
+
10
+ renders_one :header, lambda { |**options|
11
+ DialogHeaderComponent.new(**options)
12
+ }
13
+ renders_one :title, lambda { |**options|
14
+ DialogTitleComponent.new(**options)
15
+ }
16
+ renders_one :description, lambda { |**options|
17
+ DialogDescriptionComponent.new(**options)
18
+ }
19
+ renders_one :footer, lambda { |**options|
20
+ DialogFooterComponent.new(**options)
21
+ }
22
+ renders_one :close_button
23
+
24
+ def call
25
+ # Portal container that will be moved to body
26
+ content_tag(:template, content_wrapper, { "data-shadcn--dialog-target": "template" })
27
+ end
28
+
29
+ private
30
+
31
+ def content_wrapper
32
+ safe_join([
33
+ overlay,
34
+ dialog_panel
35
+ ])
36
+ end
37
+
38
+ def overlay
39
+ content_tag(:div, "", {
40
+ class: OVERLAY_CLASSES,
41
+ "data-shadcn--dialog-target": "overlay",
42
+ "data-action": "click->shadcn--dialog#close",
43
+ "data-state": "closed",
44
+ "aria-hidden": "true"
45
+ })
46
+ end
47
+
48
+ def dialog_panel
49
+ content_tag(:div, panel_content, {
50
+ class: cn(CONTENT_CLASSES, class_name),
51
+ role: "dialog",
52
+ "aria-modal": "true",
53
+ "data-shadcn--dialog-target": "content",
54
+ "data-state": "closed",
55
+ tabindex: "-1"
56
+ })
57
+ end
58
+
59
+ def panel_content
60
+ safe_join([
61
+ header,
62
+ title,
63
+ description,
64
+ content,
65
+ footer,
66
+ close_element
67
+ ].compact)
68
+ end
69
+
70
+ def close_element
71
+ content_tag(:button, close_icon, {
72
+ type: "button",
73
+ class: CLOSE_CLASSES,
74
+ "data-action": "click->shadcn--dialog#close",
75
+ "aria-label": "Close"
76
+ })
77
+ end
78
+
79
+ def close_icon
80
+ content_tag(:svg,
81
+ content_tag(:path, nil, d: "M18 6 6 18M6 6l12 12", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round"),
82
+ xmlns: "http://www.w3.org/2000/svg",
83
+ width: "16",
84
+ height: "16",
85
+ viewBox: "0 0 24 24",
86
+ fill: "none",
87
+ class: "h-4 w-4"
88
+ )
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dialog Description component
5
+ class DialogDescriptionComponent < BaseComponent
6
+ BASE_CLASSES = "text-sm text-muted-foreground"
7
+
8
+ def call
9
+ content_tag(:p, 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
+ # Dialog Footer component
5
+ class DialogFooterComponent < BaseComponent
6
+ BASE_CLASSES = "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2"
7
+
8
+ def call
9
+ content_tag(:div, content, class: merge_classes(BASE_CLASSES))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dialog Header component
5
+ class DialogHeaderComponent < BaseComponent
6
+ BASE_CLASSES = "flex flex-col space-y-1.5 text-center sm:text-left"
7
+
8
+ renders_one :title, lambda { |**options|
9
+ DialogTitleComponent.new(**options)
10
+ }
11
+ renders_one :description, lambda { |**options|
12
+ DialogDescriptionComponent.new(**options)
13
+ }
14
+
15
+ def call
16
+ content_tag(:div, safe_join([title, description, content].compact), class: merge_classes(BASE_CLASSES))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dialog Title component
5
+ class DialogTitleComponent < BaseComponent
6
+ BASE_CLASSES = "text-lg font-semibold leading-none tracking-tight"
7
+
8
+ def call
9
+ content_tag(:h2, content, class: merge_classes(BASE_CLASSES))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Drawer component for mobile-optimized modal panels
5
+ # Matches shadcn/ui Drawer component (built on Vaul)
6
+ #
7
+ # @example Basic drawer
8
+ # <%= render Shadcn::DrawerComponent.new do |drawer| %>
9
+ # <% drawer.with_trigger do %>
10
+ # <%= render Shadcn::ButtonComponent.new { "Open Drawer" } %>
11
+ # <% end %>
12
+ # <% drawer.with_body do |body| %>
13
+ # <% body.with_header do |header| %>
14
+ # <% header.with_title { "Edit Profile" } %>
15
+ # <% header.with_description { "Make changes to your profile." } %>
16
+ # <% end %>
17
+ # <div class="p-4">Content here</div>
18
+ # <% body.with_footer do %>
19
+ # <%= render Shadcn::ButtonComponent.new { "Save" } %>
20
+ # <% end %>
21
+ # <% end %>
22
+ # <% end %>
23
+ #
24
+ class DrawerComponent < BaseComponent
25
+ renders_one :trigger
26
+ renders_one :body, lambda { |**options|
27
+ DrawerContentComponent.new(**options)
28
+ }
29
+
30
+ # @param open [Boolean] Whether drawer starts open
31
+ # @param direction [Symbol] :bottom, :top, :left, or :right
32
+ def initialize(open: false, direction: :bottom, **options)
33
+ super(**options)
34
+ @open = open
35
+ @direction = direction
36
+ end
37
+
38
+ def call
39
+ content_tag(:div, drawer_content, drawer_attributes)
40
+ end
41
+
42
+ private
43
+
44
+ def drawer_content
45
+ safe_join([
46
+ trigger_wrapper,
47
+ body
48
+ ].compact)
49
+ end
50
+
51
+ def trigger_wrapper
52
+ return unless trigger
53
+
54
+ content_tag(:div, trigger, {
55
+ "data-shadcn--drawer-target": "trigger",
56
+ "data-action": "click->shadcn--drawer#open"
57
+ })
58
+ end
59
+
60
+ def drawer_attributes
61
+ attrs = {
62
+ class: class_name,
63
+ "data-controller": "shadcn--drawer",
64
+ "data-shadcn--drawer-open-value": @open.to_s,
65
+ "data-shadcn--drawer-direction-value": @direction.to_s
66
+ }
67
+ attrs.merge!(html_options)
68
+ attrs.merge!(build_data)
69
+ attrs.compact
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Drawer Content component
5
+ class DrawerContentComponent < BaseComponent
6
+ OVERLAY_CLASSES = "fixed inset-0 z-50 bg-black/80"
7
+ CONTENT_CLASSES = {
8
+ bottom: "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
9
+ top: "fixed inset-x-0 top-0 z-50 mb-24 flex h-auto flex-col rounded-b-[10px] border bg-background",
10
+ left: "fixed inset-y-0 left-0 z-50 h-full w-3/4 max-w-sm flex flex-col border-r bg-background",
11
+ right: "fixed inset-y-0 right-0 z-50 h-full w-3/4 max-w-sm flex flex-col border-l bg-background"
12
+ }.freeze
13
+
14
+ renders_one :header, lambda { |**options|
15
+ DrawerHeaderComponent.new(**options)
16
+ }
17
+ renders_one :footer, lambda { |**options|
18
+ DrawerFooterComponent.new(**options)
19
+ }
20
+
21
+ # @param direction [Symbol] :bottom, :top, :left, or :right
22
+ def initialize(direction: :bottom, **options, &block)
23
+ super(**options, &block)
24
+ @direction = direction
25
+ end
26
+
27
+ def call
28
+ content_tag(:template, content_wrapper, { "data-shadcn--drawer-target": "template" })
29
+ end
30
+
31
+ private
32
+
33
+ def content_wrapper
34
+ safe_join([
35
+ overlay,
36
+ drawer_panel
37
+ ])
38
+ end
39
+
40
+ def overlay
41
+ content_tag(:div, "", {
42
+ class: OVERLAY_CLASSES,
43
+ "data-shadcn--drawer-target": "overlay",
44
+ "data-action": "click->shadcn--drawer#close",
45
+ "data-state": "closed"
46
+ })
47
+ end
48
+
49
+ def drawer_panel
50
+ content_tag(:div, panel_content, {
51
+ class: cn(CONTENT_CLASSES[@direction] || CONTENT_CLASSES[:bottom], class_name),
52
+ role: "dialog",
53
+ "aria-modal": "true",
54
+ "data-shadcn--drawer-target": "content",
55
+ "data-state": "closed",
56
+ "data-direction": @direction.to_s,
57
+ tabindex: "-1"
58
+ })
59
+ end
60
+
61
+ def panel_content
62
+ safe_join([
63
+ handle_bar,
64
+ header,
65
+ content,
66
+ footer
67
+ ].compact)
68
+ end
69
+
70
+ def handle_bar
71
+ return unless [:bottom, :top].include?(@direction)
72
+
73
+ content_tag(:div, class: "mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted") { "" }
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Drawer Description component
5
+ class DrawerDescriptionComponent < BaseComponent
6
+ BASE_CLASSES = "text-sm text-muted-foreground"
7
+
8
+ def call
9
+ content_tag(:p, 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
+ # Drawer Footer component
5
+ class DrawerFooterComponent < BaseComponent
6
+ BASE_CLASSES = "mt-auto flex flex-col gap-2 p-4"
7
+
8
+ def call
9
+ content_tag(:div, content, class: merge_classes(BASE_CLASSES))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Drawer Header component
5
+ class DrawerHeaderComponent < BaseComponent
6
+ BASE_CLASSES = "grid gap-1.5 p-4 text-center sm:text-left"
7
+
8
+ renders_one :title, lambda { |**options|
9
+ DrawerTitleComponent.new(**options)
10
+ }
11
+ renders_one :description, lambda { |**options|
12
+ DrawerDescriptionComponent.new(**options)
13
+ }
14
+
15
+ def call
16
+ content_tag(:div, safe_join([title, description, content].compact), class: merge_classes(BASE_CLASSES))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Drawer Title component
5
+ class DrawerTitleComponent < BaseComponent
6
+ BASE_CLASSES = "text-lg font-semibold leading-none tracking-tight"
7
+
8
+ def call
9
+ content_tag(:h2, content, class: merge_classes(BASE_CLASSES))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dropdown Menu component
5
+ # Matches shadcn/ui DropdownMenu component
6
+ # Uses Stimulus for interactivity
7
+ #
8
+ # @example Basic dropdown
9
+ # <%= render Shadcn::DropdownMenuComponent.new do |menu| %>
10
+ # <% menu.with_trigger do %>
11
+ # <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Menu" } %>
12
+ # <% end %>
13
+ # <% menu.with_content do |content| %>
14
+ # <% content.with_label { "My Account" } %>
15
+ # <% content.with_separator %>
16
+ # <% content.with_item(href: "/profile") { "Profile" } %>
17
+ # <% content.with_item(href: "/settings") { "Settings" } %>
18
+ # <% content.with_separator %>
19
+ # <% content.with_item(variant: :destructive) { "Log out" } %>
20
+ # <% end %>
21
+ # <% end %>
22
+ #
23
+ class DropdownMenuComponent < BaseComponent
24
+ renders_one :trigger
25
+ renders_one :menu, lambda { |**options|
26
+ DropdownMenuContentComponent.new(**options)
27
+ }
28
+
29
+ # @param open [Boolean] Whether dropdown starts open
30
+ # @param align [Symbol] Content alignment (:start, :center, :end)
31
+ # @param side [Symbol] Side to show content (:top, :right, :bottom, :left)
32
+ def initialize(open: false, align: :end, side: :bottom, **options)
33
+ super(**options)
34
+ @open = open
35
+ @align = align
36
+ @side = side
37
+ end
38
+
39
+ def call
40
+ content_tag(:div, dropdown_content, dropdown_attributes)
41
+ end
42
+
43
+ private
44
+
45
+ def dropdown_content
46
+ safe_join([
47
+ trigger_wrapper,
48
+ menu
49
+ ].compact)
50
+ end
51
+
52
+ def trigger_wrapper
53
+ return unless trigger
54
+
55
+ content_tag(:div, trigger, {
56
+ "data-shadcn--dropdown-target": "trigger",
57
+ "data-action": "click->shadcn--dropdown#toggle"
58
+ })
59
+ end
60
+
61
+ def dropdown_attributes
62
+ attrs = {
63
+ class: cn("relative inline-block", class_name),
64
+ "data-controller": "shadcn--dropdown",
65
+ "data-shadcn--dropdown-open-value": @open.to_s,
66
+ "data-shadcn--dropdown-align-value": @align.to_s,
67
+ "data-shadcn--dropdown-side-value": @side.to_s,
68
+ "data-action": "keydown.escape->shadcn--dropdown#close clickOutside->shadcn--dropdown#close"
69
+ }
70
+ attrs.merge!(html_options)
71
+ attrs.merge!(build_data)
72
+ attrs.compact
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dropdown Menu Content component
5
+ class DropdownMenuContentComponent < BaseComponent
6
+ BASE_CLASSES = "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
7
+
8
+ renders_many :items, lambda { |**options, &block|
9
+ DropdownMenuItemComponent.new(**options, &block)
10
+ }
11
+ renders_many :labels, lambda { |**options, &block|
12
+ DropdownMenuLabelComponent.new(**options, &block)
13
+ }
14
+ renders_many :separators, lambda { |**options|
15
+ DropdownMenuSeparatorComponent.new(**options)
16
+ }
17
+ renders_many :groups, lambda { |**options, &block|
18
+ DropdownMenuGroupComponent.new(**options, &block)
19
+ }
20
+
21
+ def call
22
+ content_tag(:div, menu_content, menu_attributes)
23
+ end
24
+
25
+ private
26
+
27
+ def menu_content
28
+ # If items/labels/separators are used, render them
29
+ # Otherwise render the block content
30
+ if items.any? || labels.any? || separators.any? || groups.any?
31
+ safe_join([labels, items, separators, groups, content].flatten.compact)
32
+ else
33
+ content
34
+ end
35
+ end
36
+
37
+ def menu_attributes
38
+ {
39
+ class: merge_classes(BASE_CLASSES),
40
+ role: "menu",
41
+ "aria-orientation": "vertical",
42
+ "data-shadcn--dropdown-target": "content",
43
+ "data-state": "closed",
44
+ "data-side": "bottom",
45
+ hidden: true
46
+ }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dropdown Menu Group component
5
+ class DropdownMenuGroupComponent < BaseComponent
6
+ def call
7
+ content_tag(:div, content, role: "group", **html_options)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dropdown Menu Item component
5
+ class DropdownMenuItemComponent < BaseComponent
6
+ BASE_CLASSES = "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0"
7
+
8
+ VARIANTS = {
9
+ default: "",
10
+ destructive: "text-destructive focus:bg-destructive focus:text-destructive-foreground"
11
+ }.freeze
12
+
13
+ renders_one :shortcut, lambda { |**options|
14
+ DropdownMenuShortcutComponent.new(**options)
15
+ }
16
+
17
+ # @param href [String, nil] Link URL
18
+ # @param variant [Symbol] Item variant (:default, :destructive)
19
+ # @param disabled [Boolean] Whether item is disabled
20
+ # @param inset [Boolean] Whether to add left padding for icons
21
+ def initialize(href: nil, variant: :default, disabled: false, inset: false, **options, &block)
22
+ super(**options, &block)
23
+ @href = href
24
+ @variant = variant.to_sym
25
+ @disabled = disabled
26
+ @inset = inset
27
+ end
28
+
29
+ def call
30
+ tag_name = @href ? :a : :div
31
+ content_tag(tag_name, item_content, item_attributes)
32
+ end
33
+
34
+ private
35
+
36
+ def item_content
37
+ safe_join([content, shortcut].compact)
38
+ end
39
+
40
+ def item_classes
41
+ cn(
42
+ BASE_CLASSES,
43
+ VARIANTS[@variant],
44
+ @inset ? "pl-8" : "",
45
+ class_name
46
+ )
47
+ end
48
+
49
+ def item_attributes
50
+ attrs = {
51
+ class: item_classes,
52
+ role: "menuitem",
53
+ tabindex: @disabled ? nil : "-1",
54
+ href: @href,
55
+ "data-disabled": @disabled ? "" : nil,
56
+ "data-action": "click->shadcn--dropdown#selectItem"
57
+ }
58
+ attrs.merge!(html_options)
59
+ attrs.merge!(build_data)
60
+ attrs.compact
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dropdown Menu Label component
5
+ class DropdownMenuLabelComponent < BaseComponent
6
+ BASE_CLASSES = "px-2 py-1.5 text-sm font-semibold"
7
+
8
+ # @param inset [Boolean] Whether to add left padding
9
+ def initialize(inset: false, **options, &block)
10
+ super(**options, &block)
11
+ @inset = inset
12
+ end
13
+
14
+ def call
15
+ content_tag(:div, content, class: cn(BASE_CLASSES, @inset ? "pl-8" : "", class_name))
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dropdown Menu Separator component
5
+ class DropdownMenuSeparatorComponent < BaseComponent
6
+ BASE_CLASSES = "-mx-1 my-1 h-px bg-muted"
7
+
8
+ def call
9
+ content_tag(:div, "", class: merge_classes(BASE_CLASSES), role: "separator")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dropdown Menu Shortcut component
5
+ class DropdownMenuShortcutComponent < BaseComponent
6
+ BASE_CLASSES = "ml-auto text-xs tracking-widest opacity-60"
7
+
8
+ def call
9
+ content_tag(:span, content, class: merge_classes(BASE_CLASSES))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Empty state component for displaying placeholder content when no data is available
5
+ # Matches shadcn/ui Empty component
6
+ #
7
+ # @example Basic empty state
8
+ # <%= render Shadcn::EmptyComponent.new do |empty| %>
9
+ # <% empty.with_header do |header| %>
10
+ # <% header.with_media(variant: :icon) do %>
11
+ # <svg>...</svg>
12
+ # <% end %>
13
+ # <% header.with_title { "No Projects Yet" } %>
14
+ # <% header.with_description { "Get started by creating your first project." } %>
15
+ # <% end %>
16
+ # <% empty.with_content do %>
17
+ # <%= render Shadcn::ButtonComponent.new { "Create Project" } %>
18
+ # <% end %>
19
+ # <% end %>
20
+ #
21
+ # @example With outline style
22
+ # <%= render Shadcn::EmptyComponent.new(class_name: "border border-dashed") do |empty| %>
23
+ # ...
24
+ # <% end %>
25
+ #
26
+ class EmptyComponent < BaseComponent
27
+ BASE_CLASSES = "flex flex-col items-center justify-center gap-6 py-16 text-center"
28
+
29
+ # Header slot containing media, title, and description
30
+ renders_one :header, lambda { |**options|
31
+ EmptyHeaderComponent.new(**options)
32
+ }
33
+
34
+ # Content slot for action buttons
35
+ renders_one :content_slot, lambda { |**options|
36
+ EmptyContentComponent.new(**options)
37
+ }
38
+
39
+ # Alias for more intuitive API
40
+ alias_method :with_content, :with_content_slot
41
+
42
+ def call
43
+ content_tag(:div, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data)) do
44
+ safe_join([header, content_slot, content].compact)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Empty Content component - container for action buttons
5
+ class EmptyContentComponent < BaseComponent
6
+ BASE_CLASSES = "flex flex-col items-center gap-2"
7
+
8
+ def call
9
+ content_tag(:div, content, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Empty Description component
5
+ class EmptyDescriptionComponent < BaseComponent
6
+ BASE_CLASSES = "text-sm text-muted-foreground max-w-sm"
7
+
8
+ def call
9
+ content_tag(:p, content, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data))
10
+ end
11
+ end
12
+ end