hakumi_components 0.1.18.pre → 0.1.19.pre

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 (467) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/README.md +208 -381
  4. data/app/assets/javascripts/hakumi_components.js +40 -34
  5. data/app/assets/stylesheets/hakumi_components.css +1 -1
  6. data/app/components/hakumi_components/admin_panel/component.rb +19 -9
  7. data/app/components/hakumi_components/affix/component.rb +54 -30
  8. data/app/components/hakumi_components/alert/component.html.erb +2 -2
  9. data/app/components/hakumi_components/alert/component.rb +68 -18
  10. data/app/components/hakumi_components/anchor/component.rb +35 -27
  11. data/app/components/hakumi_components/anchor/link/component.html.erb +1 -1
  12. data/app/components/hakumi_components/anchor/link/component.rb +28 -9
  13. data/app/components/hakumi_components/autocomplete/component.html.erb +86 -66
  14. data/app/components/hakumi_components/autocomplete/component.rb +115 -89
  15. data/app/components/hakumi_components/avatar/component.rb +45 -12
  16. data/app/components/hakumi_components/badge/component.rb +99 -45
  17. data/app/components/hakumi_components/base_component.rb +290 -91
  18. data/app/components/hakumi_components/breadcrumb/component.rb +16 -5
  19. data/app/components/hakumi_components/breadcrumb/item/component.html.erb +5 -5
  20. data/app/components/hakumi_components/breadcrumb/item/component.rb +56 -8
  21. data/app/components/hakumi_components/breadcrumb/item/menu_entry.rb +71 -0
  22. data/app/components/hakumi_components/button/component.rb +62 -20
  23. data/app/components/hakumi_components/calendar/component.html.erb +19 -19
  24. data/app/components/hakumi_components/calendar/component.rb +225 -202
  25. data/app/components/hakumi_components/calendar/day_cell.rb +62 -0
  26. data/app/components/hakumi_components/calendar/display_model.rb +237 -0
  27. data/app/components/hakumi_components/calendar/event_entry.rb +54 -0
  28. data/app/components/hakumi_components/calendar/locale_pack.rb +28 -0
  29. data/app/components/hakumi_components/calendar/panel_entry.rb +48 -0
  30. data/app/components/hakumi_components/calendar/week_row.rb +28 -0
  31. data/app/components/hakumi_components/card/component.html.erb +2 -2
  32. data/app/components/hakumi_components/card/component.rb +76 -49
  33. data/app/components/hakumi_components/card/grid/component.html.erb +1 -1
  34. data/app/components/hakumi_components/card/grid/component.rb +23 -17
  35. data/app/components/hakumi_components/card/meta/component.html.erb +8 -8
  36. data/app/components/hakumi_components/card/meta/component.rb +42 -24
  37. data/app/components/hakumi_components/carousel/autoplay_config.rb +27 -0
  38. data/app/components/hakumi_components/carousel/component.rb +85 -67
  39. data/app/components/hakumi_components/carousel/dots_config.rb +36 -0
  40. data/app/components/hakumi_components/cascader/component.html.erb +54 -36
  41. data/app/components/hakumi_components/cascader/component.rb +111 -62
  42. data/app/components/hakumi_components/checkbox/component.html.erb +5 -5
  43. data/app/components/hakumi_components/checkbox/component.rb +40 -6
  44. data/app/components/hakumi_components/checkbox/group/component.html.erb +4 -4
  45. data/app/components/hakumi_components/checkbox/group/component.rb +40 -13
  46. data/app/components/hakumi_components/checkbox/group/option.rb +34 -0
  47. data/app/components/hakumi_components/collapse/component.rb +64 -48
  48. data/app/components/hakumi_components/collapse/panel/component.rb +64 -21
  49. data/app/components/hakumi_components/color_picker/component.html.erb +21 -22
  50. data/app/components/hakumi_components/color_picker/component.rb +159 -49
  51. data/app/components/hakumi_components/color_picker/preset_group.rb +27 -0
  52. data/app/components/hakumi_components/concerns/form_field.rb +169 -68
  53. data/app/components/hakumi_components/concerns/form_field_contract.rb +57 -0
  54. data/app/components/hakumi_components/concerns/form_field_interface.rb +16 -0
  55. data/app/components/hakumi_components/concerns/input_control.rb +111 -0
  56. data/app/components/hakumi_components/concerns/input_control_contract.rb +47 -0
  57. data/app/components/hakumi_components/concerns/input_control_interface.rb +34 -0
  58. data/app/components/hakumi_components/concerns/selection_control.rb +122 -0
  59. data/app/components/hakumi_components/concerns/selection_control_contract.rb +47 -0
  60. data/app/components/hakumi_components/concerns/selection_control_interface.rb +34 -0
  61. data/app/components/hakumi_components/container/component.rb +29 -14
  62. data/app/components/hakumi_components/date_picker/component.html.erb +1 -3
  63. data/app/components/hakumi_components/date_picker/component.rb +130 -16
  64. data/app/components/hakumi_components/date_picker/range_picker.rb +159 -28
  65. data/app/components/hakumi_components/date_picker/shared_rendering.rb +151 -61
  66. data/app/components/hakumi_components/descriptions/component.html.erb +6 -6
  67. data/app/components/hakumi_components/descriptions/component.rb +98 -61
  68. data/app/components/hakumi_components/descriptions/item/component.rb +32 -8
  69. data/app/components/hakumi_components/divider/component.rb +36 -16
  70. data/app/components/hakumi_components/drawer/component.html.erb +2 -2
  71. data/app/components/hakumi_components/drawer/component.rb +112 -29
  72. data/app/components/hakumi_components/dropdown/component.rb +32 -35
  73. data/app/components/hakumi_components/dropdown/divider/component.rb +4 -0
  74. data/app/components/hakumi_components/dropdown/item/component.rb +42 -16
  75. data/app/components/hakumi_components/empty/component.rb +66 -31
  76. data/app/components/hakumi_components/flex/component.rb +39 -21
  77. data/app/components/hakumi_components/float_button/back_top/component.rb +35 -28
  78. data/app/components/hakumi_components/float_button/component.rb +161 -75
  79. data/app/components/hakumi_components/float_button/group/component.rb +85 -98
  80. data/app/components/hakumi_components/float_button/group_cluster/component.rb +46 -54
  81. data/app/components/hakumi_components/float_button/group_spec.rb +259 -0
  82. data/app/components/hakumi_components/float_button/item_spec.rb +115 -0
  83. data/app/components/hakumi_components/float_button/position_config.rb +68 -0
  84. data/app/components/hakumi_components/form/item/component.rb +34 -27
  85. data/app/components/hakumi_components/grid/col/component.rb +88 -17
  86. data/app/components/hakumi_components/grid/row/component.rb +35 -17
  87. data/app/components/hakumi_components/icon/component.rb +49 -49
  88. data/app/components/hakumi_components/image/component.html.erb +7 -7
  89. data/app/components/hakumi_components/image/component.rb +103 -40
  90. data/app/components/hakumi_components/image/preview_group/component.html.erb +1 -1
  91. data/app/components/hakumi_components/image/preview_group/component.rb +41 -36
  92. data/app/components/hakumi_components/image/preview_group/item.rb +48 -0
  93. data/app/components/hakumi_components/input/component.html.erb +1 -1
  94. data/app/components/hakumi_components/input/component.rb +142 -66
  95. data/app/components/hakumi_components/input/password/component.rb +30 -41
  96. data/app/components/hakumi_components/input/text_area/component.html.erb +2 -2
  97. data/app/components/hakumi_components/input/text_area/component.rb +89 -42
  98. data/app/components/hakumi_components/input_number/component.html.erb +5 -5
  99. data/app/components/hakumi_components/input_number/component.rb +137 -55
  100. data/app/components/hakumi_components/layout/component.html.erb +1 -1
  101. data/app/components/hakumi_components/layout/component.rb +23 -16
  102. data/app/components/hakumi_components/layout/content/component.html.erb +1 -1
  103. data/app/components/hakumi_components/layout/content/component.rb +24 -14
  104. data/app/components/hakumi_components/layout/footer/component.html.erb +1 -1
  105. data/app/components/hakumi_components/layout/footer/component.rb +24 -14
  106. data/app/components/hakumi_components/layout/header/component.html.erb +1 -1
  107. data/app/components/hakumi_components/layout/header/component.rb +24 -14
  108. data/app/components/hakumi_components/layout/sider/component.html.erb +1 -1
  109. data/app/components/hakumi_components/layout/sider/component.rb +83 -49
  110. data/app/components/hakumi_components/mentions/coercion.rb +46 -0
  111. data/app/components/hakumi_components/mentions/component.html.erb +6 -6
  112. data/app/components/hakumi_components/mentions/component.rb +131 -67
  113. data/app/components/hakumi_components/mentions/option.rb +30 -0
  114. data/app/components/hakumi_components/menu/component.rb +50 -59
  115. data/app/components/hakumi_components/menu/divider/component.rb +4 -0
  116. data/app/components/hakumi_components/menu/group/component.rb +16 -15
  117. data/app/components/hakumi_components/menu/item/component.rb +42 -19
  118. data/app/components/hakumi_components/menu/sub_menu/component.rb +49 -37
  119. data/app/components/hakumi_components/message/component.rb +62 -27
  120. data/app/components/hakumi_components/modal/component.html.erb +6 -6
  121. data/app/components/hakumi_components/modal/component.rb +93 -40
  122. data/app/components/hakumi_components/modal/confirm/component.html.erb +2 -2
  123. data/app/components/hakumi_components/modal/confirm/component.rb +49 -8
  124. data/app/components/hakumi_components/modal/error/component.rb +8 -39
  125. data/app/components/hakumi_components/modal/info/component.rb +6 -40
  126. data/app/components/hakumi_components/modal/status_component.rb +150 -0
  127. data/app/components/hakumi_components/modal/success/component.rb +6 -40
  128. data/app/components/hakumi_components/modal/warning/component.rb +14 -44
  129. data/app/components/hakumi_components/notification/component.rb +72 -38
  130. data/app/components/hakumi_components/pagination/component.html.erb +7 -11
  131. data/app/components/hakumi_components/pagination/component.rb +105 -72
  132. data/app/components/hakumi_components/pagination/page_item.rb +49 -0
  133. data/app/components/hakumi_components/popconfirm/component.rb +67 -19
  134. data/app/components/hakumi_components/popover/component.html.erb +2 -2
  135. data/app/components/hakumi_components/popover/component.rb +102 -38
  136. data/app/components/hakumi_components/progress/attribute_renderer.rb +354 -0
  137. data/app/components/hakumi_components/progress/circle_geometry.rb +76 -0
  138. data/app/components/hakumi_components/progress/component.html.erb +18 -17
  139. data/app/components/hakumi_components/progress/component.rb +230 -402
  140. data/app/components/hakumi_components/progress/controller_locals_parser.rb +130 -0
  141. data/app/components/hakumi_components/progress/info_tooltip_policy.rb +39 -0
  142. data/app/components/hakumi_components/progress/info_value.rb +75 -0
  143. data/app/components/hakumi_components/progress/status_state.rb +31 -0
  144. data/app/components/hakumi_components/progress/steps_renderer.rb +149 -0
  145. data/app/components/hakumi_components/progress/stroke_gradient_value.rb +90 -0
  146. data/app/components/hakumi_components/qr_code/component.rb +138 -72
  147. data/app/components/hakumi_components/radio/component.html.erb +3 -3
  148. data/app/components/hakumi_components/radio/component.rb +42 -9
  149. data/app/components/hakumi_components/radio/group/component.html.erb +9 -9
  150. data/app/components/hakumi_components/radio/group/component.rb +76 -27
  151. data/app/components/hakumi_components/radio/group/option.rb +43 -0
  152. data/app/components/hakumi_components/rate/component.html.erb +1 -1
  153. data/app/components/hakumi_components/rate/component.rb +55 -9
  154. data/app/components/hakumi_components/result/component.rb +87 -37
  155. data/app/components/hakumi_components/segmented/component.html.erb +1 -1
  156. data/app/components/hakumi_components/segmented/component.rb +81 -118
  157. data/app/components/hakumi_components/segmented/option.rb +191 -0
  158. data/app/components/hakumi_components/select/component.html.erb +19 -19
  159. data/app/components/hakumi_components/select/component.rb +88 -37
  160. data/app/components/hakumi_components/selection_control/coercion.rb +161 -0
  161. data/app/components/hakumi_components/selection_control/entry.rb +22 -0
  162. data/app/components/hakumi_components/selection_control/option.rb +42 -0
  163. data/app/components/hakumi_components/selection_control/option_group.rb +34 -0
  164. data/app/components/hakumi_components/selection_control/tree_node.rb +123 -0
  165. data/app/components/hakumi_components/skeleton/avatar/component.rb +36 -14
  166. data/app/components/hakumi_components/skeleton/avatar_config.rb +79 -0
  167. data/app/components/hakumi_components/skeleton/button/component.rb +32 -14
  168. data/app/components/hakumi_components/skeleton/component.rb +79 -93
  169. data/app/components/hakumi_components/skeleton/image/component.rb +46 -22
  170. data/app/components/hakumi_components/skeleton/input/component.rb +25 -10
  171. data/app/components/hakumi_components/skeleton/node/component.rb +23 -7
  172. data/app/components/hakumi_components/skeleton/paragraph_config.rb +92 -0
  173. data/app/components/hakumi_components/skeleton/title_config.rb +31 -0
  174. data/app/components/hakumi_components/slider/component.html.erb +11 -11
  175. data/app/components/hakumi_components/slider/component.rb +172 -53
  176. data/app/components/hakumi_components/slider/mark.rb +27 -0
  177. data/app/components/hakumi_components/space/compact/component.html.erb +1 -3
  178. data/app/components/hakumi_components/space/compact/component.rb +35 -19
  179. data/app/components/hakumi_components/space/component.html.erb +1 -1
  180. data/app/components/hakumi_components/space/component.rb +75 -39
  181. data/app/components/hakumi_components/spin/component.rb +92 -31
  182. data/app/components/hakumi_components/splitter/component.html.erb +9 -3
  183. data/app/components/hakumi_components/splitter/component.rb +55 -22
  184. data/app/components/hakumi_components/splitter/panel/component.html.erb +1 -3
  185. data/app/components/hakumi_components/splitter/panel/component.rb +64 -39
  186. data/app/components/hakumi_components/statistic/component.rb +143 -59
  187. data/app/components/hakumi_components/steps/component.rb +87 -55
  188. data/app/components/hakumi_components/steps/item/component.rb +59 -20
  189. data/app/components/hakumi_components/switch/component.html.erb +2 -3
  190. data/app/components/hakumi_components/switch/component.rb +62 -36
  191. data/app/components/hakumi_components/table/column/component.rb +80 -23
  192. data/app/components/hakumi_components/table/column_definition.rb +181 -0
  193. data/app/components/hakumi_components/table/column_group/component.rb +32 -21
  194. data/app/components/hakumi_components/table/component.html.erb +22 -31
  195. data/app/components/hakumi_components/table/component.rb +248 -509
  196. data/app/components/hakumi_components/table/concerns/attribute_helpers.rb +22 -0
  197. data/app/components/hakumi_components/table/concerns/cell_rendering.rb +277 -0
  198. data/app/components/hakumi_components/table/concerns/columns.rb +293 -91
  199. data/app/components/hakumi_components/table/concerns/editable.rb +36 -89
  200. data/app/components/hakumi_components/table/concerns/ellipsis.rb +31 -30
  201. data/app/components/hakumi_components/table/concerns/fixed_columns.rb +26 -19
  202. data/app/components/hakumi_components/table/concerns/surface_rendering.rb +365 -0
  203. data/app/components/hakumi_components/table/configs.rb +518 -0
  204. data/app/components/hakumi_components/table/definition_types.rb +40 -0
  205. data/app/components/hakumi_components/table/display_rows.rb +56 -0
  206. data/app/components/hakumi_components/table/ellipsis_config.rb +143 -0
  207. data/app/components/hakumi_components/table/fixed_offset.rb +36 -0
  208. data/app/components/hakumi_components/table/header_cell.rb +23 -0
  209. data/app/components/hakumi_components/table/row_record.rb +60 -0
  210. data/app/components/hakumi_components/table/row_render_state.rb +85 -0
  211. data/app/components/hakumi_components/tabs/component.html.erb +8 -8
  212. data/app/components/hakumi_components/tabs/component.rb +114 -64
  213. data/app/components/hakumi_components/tabs/indicator_config.rb +34 -0
  214. data/app/components/hakumi_components/tabs/item/component.rb +73 -13
  215. data/app/components/hakumi_components/tag/component.rb +142 -47
  216. data/app/components/hakumi_components/tag/group/component.rb +18 -6
  217. data/app/components/hakumi_components/time_picker/component.html.erb +8 -8
  218. data/app/components/hakumi_components/time_picker/component.rb +71 -17
  219. data/app/components/hakumi_components/timeline/component.html.erb +3 -2
  220. data/app/components/hakumi_components/timeline/component.rb +188 -55
  221. data/app/components/hakumi_components/timeline/item/component.html.erb +2 -2
  222. data/app/components/hakumi_components/timeline/item/component.rb +121 -60
  223. data/app/components/hakumi_components/tooltip/component.html.erb +1 -1
  224. data/app/components/hakumi_components/tooltip/component.rb +89 -64
  225. data/app/components/hakumi_components/tour/component.rb +74 -49
  226. data/app/components/hakumi_components/tour/step.rb +86 -0
  227. data/app/components/hakumi_components/transfer/coercion.rb +66 -0
  228. data/app/components/hakumi_components/transfer/component.html.erb +171 -140
  229. data/app/components/hakumi_components/transfer/component.rb +232 -82
  230. data/app/components/hakumi_components/transfer/item.rb +54 -0
  231. data/app/components/hakumi_components/transfer/operations.rb +31 -0
  232. data/app/components/hakumi_components/tree/component.rb +219 -400
  233. data/app/components/hakumi_components/tree/concerns/node_rendering.rb +365 -0
  234. data/app/components/hakumi_components/tree/node.rb +276 -0
  235. data/app/components/hakumi_components/tree_select/component.html.erb +23 -9
  236. data/app/components/hakumi_components/tree_select/component.rb +116 -107
  237. data/app/components/hakumi_components/typography/base_component.rb +69 -36
  238. data/app/components/hakumi_components/typography/link/component.rb +63 -6
  239. data/app/components/hakumi_components/typography/paragraph/component.rb +6 -0
  240. data/app/components/hakumi_components/typography/text/component.rb +4 -0
  241. data/app/components/hakumi_components/typography/title/component.rb +61 -6
  242. data/app/components/hakumi_components/upload/component.html.erb +106 -88
  243. data/app/components/hakumi_components/upload/component.rb +222 -48
  244. data/app/components/hakumi_components/upload/file_entry.rb +53 -0
  245. data/app/controllers/hakumi_components/components_controller.rb +2 -1
  246. data/app/form_builders/hakumi_components/form_builder/contracts.rb +55 -0
  247. data/app/form_builders/hakumi_components/form_builder/field_context.rb +43 -0
  248. data/app/form_builders/hakumi_components/form_builder/model_binding.rb +163 -0
  249. data/app/form_builders/hakumi_components/form_builder.rb +238 -317
  250. data/app/helpers/hakumi_components/form_helper.rb +27 -23
  251. data/app/javascript/hakumi_components/controllers/hakumi/admin_panel_controller.js +5 -7
  252. data/app/javascript/hakumi_components/controllers/hakumi/back_top_controller.js +1 -1
  253. data/app/javascript/hakumi_components/controllers/hakumi/button_controller.js +108 -2
  254. data/app/javascript/hakumi_components/controllers/hakumi/calendar_controller.js +183 -95
  255. data/app/javascript/hakumi_components/controllers/hakumi/color_picker_controller.js +23 -285
  256. data/app/javascript/hakumi_components/controllers/hakumi/date_picker_controller.js +274 -262
  257. data/app/javascript/hakumi_components/controllers/hakumi/float_button_group_controller.js +2 -2
  258. data/app/javascript/hakumi_components/controllers/hakumi/message_controller.js +4 -2
  259. data/app/javascript/hakumi_components/controllers/hakumi/modal_controller.js +119 -125
  260. data/app/javascript/hakumi_components/controllers/hakumi/table/editable.js +291 -0
  261. data/app/javascript/hakumi_components/controllers/hakumi/table_controller.js +166 -366
  262. data/app/javascript/hakumi_components/controllers/hakumi/tabs_controller.js +8 -2
  263. data/app/javascript/hakumi_components/controllers/hakumi/tag_controller.js +27 -25
  264. data/app/javascript/hakumi_components/controllers/hakumi/tag_group_controller.js +19 -18
  265. data/app/javascript/hakumi_components/controllers/hakumi/theme_controller.js +116 -117
  266. data/app/javascript/hakumi_components/controllers/hakumi/transfer_controller.js +17 -1
  267. data/app/javascript/hakumi_components/controllers/hakumi/tree_controller.js +363 -78
  268. data/app/javascript/hakumi_components/controllers/hakumi/typography_controller.js +3 -3
  269. data/app/javascript/hakumi_components/controllers/hakumi/upload_controller.js +320 -204
  270. data/app/javascript/hakumi_components/core/render_component.js +37 -11
  271. data/app/javascript/hakumi_components/utils/color_helper.js +262 -0
  272. data/app/javascript/stylesheets/_base.scss +9 -0
  273. data/app/javascript/stylesheets/_hakumi_components.scss +1 -0
  274. data/app/javascript/stylesheets/components/_breadcrumb.scss +2 -2
  275. data/app/javascript/stylesheets/components/_calendar.scss +13 -13
  276. data/app/javascript/stylesheets/components/_cascader.scss +5 -5
  277. data/app/javascript/stylesheets/components/_checkbox.scss +9 -11
  278. data/app/javascript/stylesheets/components/_color_picker.scss +11 -11
  279. data/app/javascript/stylesheets/components/_date_picker.scss +4 -4
  280. data/app/javascript/stylesheets/components/_descriptions.scss +2 -2
  281. data/app/javascript/stylesheets/components/_drawer.scss +3 -3
  282. data/app/javascript/stylesheets/components/_dropdown.scss +2 -2
  283. data/app/javascript/stylesheets/components/_flex.scss +1 -1
  284. data/app/javascript/stylesheets/components/_float_button.scss +5 -5
  285. data/app/javascript/stylesheets/components/_form_item.scss +92 -0
  286. data/app/javascript/stylesheets/components/_image.scss +15 -15
  287. data/app/javascript/stylesheets/components/_input.scss +23 -113
  288. data/app/javascript/stylesheets/components/_layout.scss +27 -26
  289. data/app/javascript/stylesheets/components/_menu.scss +15 -15
  290. data/app/javascript/stylesheets/components/_modal.scss +13 -13
  291. data/app/javascript/stylesheets/components/_notification.scss +3 -3
  292. data/app/javascript/stylesheets/components/_popover.scss +1 -1
  293. data/app/javascript/stylesheets/components/_segmented.scss +3 -3
  294. data/app/javascript/stylesheets/components/_select.scss +6 -6
  295. data/app/javascript/stylesheets/components/_slider.scss +1 -1
  296. data/app/javascript/stylesheets/components/_spin.scss +2 -2
  297. data/app/javascript/stylesheets/components/_steps.scss +10 -10
  298. data/app/javascript/stylesheets/components/_switch.scss +11 -10
  299. data/app/javascript/stylesheets/components/_table.scss +6 -6
  300. data/app/javascript/stylesheets/components/_tag.scss +2 -2
  301. data/app/javascript/stylesheets/components/_tooltip.scss +4 -4
  302. data/app/javascript/stylesheets/components/_tree_select.scss +3 -3
  303. data/app/javascript/stylesheets/components/_typography.scss +3 -3
  304. data/app/javascript/stylesheets/components/_upload.scss +4 -4
  305. data/app/services/hakumi_components/component_handler.rb +95 -24
  306. data/app/services/hakumi_components/icon/loader.rb +84 -67
  307. data/app/services/hakumi_components/illustrations/loader.rb +35 -27
  308. data/app/views/hakumi/_alert.html.erb +4 -1
  309. data/app/views/hakumi/_drawer.html.erb +1 -1
  310. data/app/views/hakumi/_statistic.html.erb +11 -11
  311. data/lib/generators/hakumi_components/install_generator.rb +14 -1
  312. data/lib/hakumi_components/documentation/models.rb +290 -0
  313. data/lib/hakumi_components/documentation/node.rb +73 -0
  314. data/lib/hakumi_components/documentation.rb +139 -62
  315. data/lib/hakumi_components/engine.rb +17 -11
  316. data/lib/hakumi_components/file_size_checker.rb +215 -0
  317. data/lib/hakumi_components/locales/en.yml +115 -0
  318. data/lib/hakumi_components/rails/attribute_introspection.rb +39 -107
  319. data/lib/hakumi_components/rails/model_reflection.rb +154 -0
  320. data/lib/hakumi_components/rails/validation_introspection.rb +25 -73
  321. data/lib/hakumi_components/rails/validation_mapper.rb +148 -201
  322. data/lib/hakumi_components/rails.rb +2 -0
  323. data/lib/hakumi_components/stylesheet_ownership_checker.rb +191 -0
  324. data/lib/hakumi_components/types/form_field.rb +24 -0
  325. data/lib/hakumi_components/types/html.rb +28 -0
  326. data/lib/hakumi_components/types/rendering.rb +10 -0
  327. data/lib/hakumi_components/types/stimulus.rb +19 -0
  328. data/lib/hakumi_components/types/validation.rb +22 -0
  329. data/lib/hakumi_components/types.rb +13 -0
  330. data/lib/hakumi_components/version.rb +2 -1
  331. data/lib/hakumi_components.rb +26 -6
  332. data/lib/tasks/coverage.rake +1 -0
  333. metadata +100 -136
  334. data/sig/action_view/tag_builder.rbs +0 -24
  335. data/sig/active_support/concern_patch.rbs +0 -7
  336. data/sig/generators/hakumi_components/install_generator.rbs +0 -11
  337. data/sig/hakumi_components/admin_panel/component.rbs +0 -28
  338. data/sig/hakumi_components/affix/component.rbs +0 -44
  339. data/sig/hakumi_components/alert/component.rbs +0 -64
  340. data/sig/hakumi_components/anchor/component.rbs +0 -45
  341. data/sig/hakumi_components/anchor/link/component.rbs +0 -31
  342. data/sig/hakumi_components/autocomplete/component.rbs +0 -68
  343. data/sig/hakumi_components/avatar/component.rbs +0 -44
  344. data/sig/hakumi_components/badge/component.rbs +0 -41
  345. data/sig/hakumi_components/base_component.rbs +0 -82
  346. data/sig/hakumi_components/breadcrumb/component.rbs +0 -21
  347. data/sig/hakumi_components/breadcrumb/item/component.rbs +0 -36
  348. data/sig/hakumi_components/button/component.rbs +0 -56
  349. data/sig/hakumi_components/calendar/component.rbs +0 -41
  350. data/sig/hakumi_components/card/component.rbs +0 -50
  351. data/sig/hakumi_components/card/grid/component.rbs +0 -21
  352. data/sig/hakumi_components/card/meta/component.rbs +0 -25
  353. data/sig/hakumi_components/carousel/component.rbs +0 -88
  354. data/sig/hakumi_components/cascader/component.rbs +0 -52
  355. data/sig/hakumi_components/checkbox/component.rbs +0 -46
  356. data/sig/hakumi_components/checkbox/group/component.rbs +0 -34
  357. data/sig/hakumi_components/collapse/component.rbs +0 -57
  358. data/sig/hakumi_components/collapse/panel/component.rbs +0 -47
  359. data/sig/hakumi_components/color_picker/component.rbs +0 -80
  360. data/sig/hakumi_components/concerns/form_field.rbs +0 -89
  361. data/sig/hakumi_components/container/component.rbs +0 -37
  362. data/sig/hakumi_components/date_picker/component.rbs +0 -72
  363. data/sig/hakumi_components/date_picker/range_picker.rbs +0 -68
  364. data/sig/hakumi_components/date_picker/shared_rendering.rbs +0 -41
  365. data/sig/hakumi_components/descriptions/component.rbs +0 -59
  366. data/sig/hakumi_components/descriptions/item/component.rbs +0 -31
  367. data/sig/hakumi_components/divider/component.rbs +0 -39
  368. data/sig/hakumi_components/drawer/component.rbs +0 -66
  369. data/sig/hakumi_components/dropdown/component.rbs +0 -45
  370. data/sig/hakumi_components/dropdown/divider/component.rbs +0 -11
  371. data/sig/hakumi_components/dropdown/item/component.rbs +0 -45
  372. data/sig/hakumi_components/empty/component.rbs +0 -40
  373. data/sig/hakumi_components/flex/component.rbs +0 -47
  374. data/sig/hakumi_components/float_button/back_top/component.rbs +0 -45
  375. data/sig/hakumi_components/float_button/component.rbs +0 -80
  376. data/sig/hakumi_components/float_button/group/component.rbs +0 -95
  377. data/sig/hakumi_components/float_button/group_cluster/component.rbs +0 -60
  378. data/sig/hakumi_components/form/item/component.rbs +0 -39
  379. data/sig/hakumi_components/form_builder.rbs +0 -37
  380. data/sig/hakumi_components/grid/col/component.rbs +0 -50
  381. data/sig/hakumi_components/grid/row/component.rbs +0 -46
  382. data/sig/hakumi_components/icon/component.rbs +0 -56
  383. data/sig/hakumi_components/image/component.rbs +0 -56
  384. data/sig/hakumi_components/image/preview_group/component.rbs +0 -28
  385. data/sig/hakumi_components/input/component.rbs +0 -71
  386. data/sig/hakumi_components/input/password/component.rbs +0 -27
  387. data/sig/hakumi_components/input/text_area/component.rbs +0 -42
  388. data/sig/hakumi_components/input_number/component.rbs +0 -85
  389. data/sig/hakumi_components/layout/component.rbs +0 -21
  390. data/sig/hakumi_components/layout/content/component.rbs +0 -19
  391. data/sig/hakumi_components/layout/footer/component.rbs +0 -19
  392. data/sig/hakumi_components/layout/header/component.rbs +0 -19
  393. data/sig/hakumi_components/layout/sider/component.rbs +0 -19
  394. data/sig/hakumi_components/mentions/component.rbs +0 -75
  395. data/sig/hakumi_components/menu/component.rbs +0 -52
  396. data/sig/hakumi_components/menu/divider/component.rbs +0 -11
  397. data/sig/hakumi_components/menu/group/component.rbs +0 -20
  398. data/sig/hakumi_components/menu/item/component.rbs +0 -42
  399. data/sig/hakumi_components/menu/sub_menu/component.rbs +0 -37
  400. data/sig/hakumi_components/message/component.rbs +0 -50
  401. data/sig/hakumi_components/modal/component.rbs +0 -76
  402. data/sig/hakumi_components/modal/confirm/component.rbs +0 -32
  403. data/sig/hakumi_components/modal/error/component.rbs +0 -27
  404. data/sig/hakumi_components/modal/info/component.rbs +0 -27
  405. data/sig/hakumi_components/modal/success/component.rbs +0 -27
  406. data/sig/hakumi_components/modal/warning/component.rbs +0 -27
  407. data/sig/hakumi_components/notification/component.rbs +0 -64
  408. data/sig/hakumi_components/pagination/component.rbs +0 -113
  409. data/sig/hakumi_components/popconfirm/component.rbs +0 -52
  410. data/sig/hakumi_components/popover/component.rbs +0 -66
  411. data/sig/hakumi_components/progress/component.rbs +0 -201
  412. data/sig/hakumi_components/qr_code/component.rbs +0 -111
  413. data/sig/hakumi_components/radio/component.rbs +0 -55
  414. data/sig/hakumi_components/radio/group/component.rbs +0 -71
  415. data/sig/hakumi_components/rails/attribute_introspection.rbs +0 -16
  416. data/sig/hakumi_components/rails/validation_introspection.rbs +0 -18
  417. data/sig/hakumi_components/rails/validation_mapper.rbs +0 -53
  418. data/sig/hakumi_components/rails.rbs +0 -6
  419. data/sig/hakumi_components/rate/component.rbs +0 -56
  420. data/sig/hakumi_components/result/component.rbs +0 -65
  421. data/sig/hakumi_components/segmented/component.rbs +0 -72
  422. data/sig/hakumi_components/select/component.rbs +0 -73
  423. data/sig/hakumi_components/skeleton/avatar/component.rbs +0 -23
  424. data/sig/hakumi_components/skeleton/button/component.rbs +0 -24
  425. data/sig/hakumi_components/skeleton/component.rbs +0 -76
  426. data/sig/hakumi_components/skeleton/image/component.rbs +0 -22
  427. data/sig/hakumi_components/skeleton/input/component.rbs +0 -23
  428. data/sig/hakumi_components/skeleton/node/component.rbs +0 -21
  429. data/sig/hakumi_components/slider/component.rbs +0 -85
  430. data/sig/hakumi_components/space/compact/component.rbs +0 -21
  431. data/sig/hakumi_components/space/component.rbs +0 -47
  432. data/sig/hakumi_components/spin/component.rbs +0 -77
  433. data/sig/hakumi_components/splitter/component.rbs +0 -27
  434. data/sig/hakumi_components/splitter/panel/component.rbs +0 -36
  435. data/sig/hakumi_components/statistic/component.rbs +0 -97
  436. data/sig/hakumi_components/steps/component.rbs +0 -82
  437. data/sig/hakumi_components/steps/item/component.rbs +0 -29
  438. data/sig/hakumi_components/switch/component.rbs +0 -52
  439. data/sig/hakumi_components/table/column/component.rbs +0 -57
  440. data/sig/hakumi_components/table/column_group/component.rbs +0 -20
  441. data/sig/hakumi_components/table/component.rbs +0 -279
  442. data/sig/hakumi_components/table/concerns/columns.rbs +0 -95
  443. data/sig/hakumi_components/table/concerns/editable.rbs +0 -40
  444. data/sig/hakumi_components/table/concerns/ellipsis.rbs +0 -27
  445. data/sig/hakumi_components/table/concerns/fixed_columns.rbs +0 -33
  446. data/sig/hakumi_components/tabs/component.rbs +0 -66
  447. data/sig/hakumi_components/tabs/item/component.rbs +0 -29
  448. data/sig/hakumi_components/tag/component.rbs +0 -86
  449. data/sig/hakumi_components/tag/group/component.rbs +0 -24
  450. data/sig/hakumi_components/time_picker/component.rbs +0 -72
  451. data/sig/hakumi_components/timeline/component.rbs +0 -61
  452. data/sig/hakumi_components/timeline/item/component.rbs +0 -70
  453. data/sig/hakumi_components/tooltip/component.rbs +0 -73
  454. data/sig/hakumi_components/tour/component.rbs +0 -84
  455. data/sig/hakumi_components/transfer/component.rbs +0 -75
  456. data/sig/hakumi_components/tree/component.rbs +0 -126
  457. data/sig/hakumi_components/tree_select/component.rbs +0 -122
  458. data/sig/hakumi_components/typography/base_component.rbs +0 -57
  459. data/sig/hakumi_components/typography/link/component.rbs +0 -28
  460. data/sig/hakumi_components/typography/paragraph/component.rbs +0 -13
  461. data/sig/hakumi_components/typography/text/component.rbs +0 -10
  462. data/sig/hakumi_components/typography/title/component.rbs +0 -28
  463. data/sig/hakumi_components/upload/component.rbs +0 -78
  464. data/sig/hakumi_components.rbs +0 -96
  465. data/sig/rails/active_model/validations/comparison_validator.rbs +0 -6
  466. data/sig/rails.rbs +0 -18
  467. data/sig/view_component/base.rbs +0 -28
@@ -1,109 +1,96 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module HakumiComponents
4
- # Custom FormBuilder that renders Hakumi components for form inputs.
5
- # Extends Rails' ActionView::Helpers::FormBuilder to integrate seamlessly with Rails forms.
6
- # Includes automatic detection of validations, I18n labels, and attribute introspection.
7
- #
8
- # @example Usage
9
- # <%= hakumi_form_with(model: @user) do |f| %>
10
- # <%= f.text_field :email %> # Automatically detects label, required, type, etc.
11
- # <%= f.password_field :password %>
12
- # <%= f.text_area :bio %>
13
- # <%= f.submit "Save" %>
14
- # <% end %>
15
5
  class FormBuilder < ActionView::Helpers::FormBuilder
16
- # Renders a Hakumi text input field with Rails integration
17
- #
18
- # @param method [Symbol] Name of the attribute
19
- # @param options [Hash] Options for the input
20
- # @return [String] Rendered HTML
6
+ extend T::Sig
7
+
8
+ sig do
9
+ params(
10
+ object_name: T.nilable(Types::StringOrSymbol),
11
+ object: T.nilable(ActiveModel::Model),
12
+ template: ActionView::Helpers::RenderingHelper,
13
+ options: Contracts::BuilderInitOptions
14
+ ).void
15
+ end
16
+ def initialize(object_name, object, template, options)
17
+ @object_name = T.let(object_name&.to_s, T.nilable(String))
18
+ @object = T.let(object, T.nilable(ActiveModel::Model))
19
+ @template = T.let(template, ActionView::Helpers::RenderingHelper)
20
+ @model_binding = T.let(nil, T.nilable(HakumiComponents::FormBuilder::ModelBinding))
21
+ super
22
+ end
23
+
24
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
21
25
  def text_field(method, **options)
22
- enhance_options_with_introspection!(method, options)
23
- render_form_field(HakumiComponents::Input::Component, method, **options)
26
+ render_simple_field(HakumiComponents::Input::Component, method, options)
24
27
  end
25
28
 
26
- # Renders a Hakumi password input field with visibility toggle
27
- #
28
- # @param method [Symbol] Name of the attribute
29
- # @param options [Hash] Options for the input
30
- # @return [String] Rendered HTML
29
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
31
30
  def password_field(method, **options)
32
- enhance_options_with_introspection!(method, options)
33
- render_form_field(HakumiComponents::Input::Password::Component, method, **options)
31
+ render_simple_field(HakumiComponents::Input::Password::Component, method, options)
34
32
  end
35
33
 
36
- # Renders a Hakumi textarea field
37
- #
38
- # @param method [Symbol] Name of the attribute
39
- # @param options [Hash] Options for the textarea
40
- # @return [String] Rendered HTML
34
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
41
35
  def text_area(method, **options)
42
- enhance_options_with_introspection!(method, options)
43
- render_form_field(HakumiComponents::Input::TextArea::Component, method, **options)
36
+ render_simple_field(HakumiComponents::Input::TextArea::Component, method, options)
44
37
  end
45
38
 
46
- # Renders a Hakumi number input field
47
- #
48
- # @param method [Symbol] Name of the attribute
49
- # @param options [Hash] Options for the input
50
- # @return [String] Rendered HTML
39
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
51
40
  def number_field(method, **options)
52
- enhance_options_with_introspection!(method, options)
53
- render_form_field(HakumiComponents::InputNumber::Component, method, **options)
41
+ render_simple_field(HakumiComponents::InputNumber::Component, method, options)
54
42
  end
55
43
 
56
- # Renders a Hakumi select field
57
- #
58
- # @param method [Symbol] Name of the attribute
59
- # @param choices [Array] Array of options [[label, value], ...]
60
- # @param options [Hash] Options for the select
61
- # @return [String] Rendered HTML
44
+ sig do
45
+ params(
46
+ method: Symbol,
47
+ choices: T.nilable(T::Array[HakumiComponents::SelectionControl::Coercion::FlatOptionInput]),
48
+ options: Contracts::OptionValue
49
+ ).returns(ActiveSupport::SafeBuffer)
50
+ end
62
51
  def select(method, choices = nil, **options)
63
- choices ||= [] # steep:ignore UnannotatedEmptyCollection
64
- enhance_options_with_introspection!(method, options)
65
-
66
- render_form_field(
52
+ render_collection_field(
67
53
  HakumiComponents::Select::Component,
68
54
  method,
69
- **options.merge(options: choices)
55
+ :options,
56
+ HakumiComponents::SelectionControl::Coercion.options(ensure_flat_selection_array(choices)),
57
+ options
70
58
  )
71
59
  end
72
60
 
73
- # Renders a Hakumi tree select field
74
- #
75
- # @param method [Symbol] Name of the attribute
76
- # @param choices [Array] Array of tree options with keys: key, title, children
77
- # @param options [Hash] Options for the tree select
78
- # @return [String] Rendered HTML
61
+ sig do
62
+ params(
63
+ method: Symbol,
64
+ choices: T.nilable(T::Array[HakumiComponents::SelectionControl::TreeNode]),
65
+ options: Contracts::OptionValue
66
+ ).returns(ActiveSupport::SafeBuffer)
67
+ end
79
68
  def tree_select(method, choices = nil, **options)
80
- choices ||= [] # steep:ignore UnannotatedEmptyCollection
81
- enhance_options_with_introspection!(method, options)
82
-
83
- render_form_field(
69
+ render_collection_field(
84
70
  HakumiComponents::TreeSelect::Component,
85
71
  method,
86
- **options.merge(options: choices)
72
+ :options,
73
+ choices || [],
74
+ options
87
75
  )
88
76
  end
89
77
 
90
- # Renders a Hakumi radio button
91
- #
92
- # @param method [Symbol] Name of the attribute
93
- # @param tag_value [String] Value for this radio button
94
- # @param options [Hash] Options for the radio
95
- # @return [String] Rendered HTML
78
+ sig do
79
+ params(
80
+ method: Symbol,
81
+ tag_value: HakumiComponents::Types::FormFieldValue,
82
+ options: Contracts::OptionValue
83
+ ).returns(ActiveSupport::SafeBuffer)
84
+ end
96
85
  def radio_button(method, tag_value, **options)
97
86
  enhance_options_with_introspection!(method, options)
98
87
 
99
- # Use tag_value as ID suffix unless explicitly provided
100
88
  id_suffix = options[:id] ? nil : tag_value.to_s
101
89
  config = field_configuration(method, options, id_suffix: id_suffix)
102
90
 
103
- # Override with explicit ID if provided
104
- config[:id] = options.delete(:id) if options[:id]
91
+ option_id = options.delete(:id)
92
+ config[:id] = option_id if option_id.is_a?(String) || option_id.is_a?(Symbol)
105
93
 
106
- # Determine if checked
107
94
  checked = config[:value].to_s == tag_value.to_s
108
95
  config[:value] = tag_value
109
96
  config[:checked] = checked
@@ -111,72 +98,48 @@ module HakumiComponents
111
98
  render_form_field(
112
99
  HakumiComponents::Radio::Component,
113
100
  method,
114
- config_overrides: config,
115
- **options
101
+ options,
102
+ config
116
103
  )
117
104
  end
118
105
 
119
- # Renders a Hakumi radio group from a collection
120
- #
121
- # @param method [Symbol] Name of the attribute
122
- # @param collection [Array] Collection of objects
123
- # @param value_method [Symbol] Method to call for value
124
- # @param text_method [Symbol] Method to call for label text
125
- # @param options [Hash] Options for the radio group
126
- # @return [String] Rendered HTML
106
+ sig do
107
+ params(
108
+ method: Symbol,
109
+ collection: T::Array[T.any(Struct, ActiveModel::Model)],
110
+ value_method: Symbol,
111
+ text_method: Symbol,
112
+ options: Contracts::OptionValue
113
+ ).returns(ActiveSupport::SafeBuffer)
114
+ end
127
115
  def collection_radio_buttons(method, collection, value_method, text_method, **options)
128
116
  enhance_options_with_introspection!(method, options)
129
117
 
130
- # Convert collection to radio options format (Hash with :label and :value)
131
118
  radio_options = collection.map do |item|
132
- {
119
+ HakumiComponents::Radio::Group::Option.new(
133
120
  label: item.send(text_method),
134
121
  value: item.send(value_method)
135
- }
122
+ )
136
123
  end
137
124
 
138
125
  render_form_field(
139
126
  HakumiComponents::Radio::Group::Component,
140
127
  method,
141
- **options.merge(options: radio_options)
128
+ options.merge(options: radio_options)
142
129
  )
143
130
  end
144
131
 
145
- # Renders a Hakumi switch (checkbox) field
146
- #
147
- # @param method [Symbol] Name of the attribute
148
- # @param options [Hash] Options for the switch
149
- # @return [String] Rendered HTML
132
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
150
133
  def check_box(method, **options)
151
- enhance_options_with_introspection!(method, options)
152
-
153
- # Convert truthy/falsy value to boolean for checked attribute
154
- value = object_value(method)
155
- checked = value ? true : false
156
-
157
- render_form_field(
158
- HakumiComponents::Switch::Component,
159
- method,
160
- config_overrides: { checked: checked },
161
- **options
162
- )
134
+ render_checked_field(HakumiComponents::Switch::Component, method, options)
163
135
  end
164
136
 
165
- # Renders a Hakumi rate (rating) field
166
- #
167
- # @param method [Symbol] Name of the attribute
168
- # @param options [Hash] Options for the rate component
169
- # @return [String] Rendered HTML
137
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
170
138
  def rate_field(method, **options)
171
- enhance_options_with_introspection!(method, options)
172
- render_form_field(HakumiComponents::Rate::Component, method, **options)
139
+ render_simple_field(HakumiComponents::Rate::Component, method, options)
173
140
  end
174
141
 
175
- # Renders a Hakumi slider field
176
- #
177
- # @param method [Symbol] Name of the attribute
178
- # @param options [Hash] Options for the slider component
179
- # @return [String] Rendered HTML
142
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
180
143
  def slider_field(method, **options)
181
144
  enhance_options_with_introspection!(method, options)
182
145
 
@@ -185,160 +148,103 @@ module HakumiComponents
185
148
  render_form_field(
186
149
  HakumiComponents::Slider::Component,
187
150
  method,
188
- config_overrides: config,
189
- **options
151
+ options,
152
+ config
190
153
  )
191
154
  end
192
155
 
193
- # Renders a Hakumi date picker field
194
- #
195
- # @param method [Symbol] Name of the attribute
196
- # @param options [Hash] Options for the date picker
197
- # @return [String] Rendered HTML
156
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
198
157
  def date_picker(method, **options)
199
- enhance_options_with_introspection!(method, options)
200
- render_form_field(HakumiComponents::DatePicker::Component, method, **options)
201
- end
202
-
203
- # Renders a Hakumi mentions field (textarea with @ mention suggestions)
204
- #
205
- # @param method [Symbol] Name of the attribute
206
- # @param options [Hash] Options for the mentions component
207
- # @option options [Array] :options Array of mention options (names, usernames, etc.)
208
- # @option options [String, Array] :prefix Character(s) that trigger mentions (default: "@")
209
- # @option options [String] :split Character that separates tokens (default: " ")
210
- # @option options [Symbol] :filter_option Filter mode (:contains, :starts_with)
211
- # @return [String] Rendered HTML
212
- def mentions_field(method, **options)
213
- enhance_options_with_introspection!(method, options)
214
-
215
- # Extract mentions-specific options before field configuration
216
- mention_options = options.delete(:options) || []
158
+ render_simple_field(HakumiComponents::DatePicker::Component, method, options)
159
+ end
217
160
 
218
- render_form_field(
161
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
162
+ def mentions_field(method, **options)
163
+ render_collection_field(
219
164
  HakumiComponents::Mentions::Component,
220
165
  method,
221
- **options.merge(options: mention_options)
166
+ :options,
167
+ HakumiComponents::Mentions::Coercion.options(ensure_mentions_array(options.delete(:options))),
168
+ options
222
169
  )
223
170
  end
224
171
 
225
- # Renders a Hakumi autocomplete field (input with dropdown suggestions)
226
- #
227
- # @param method [Symbol] Name of the attribute
228
- # @param choices [Array] Array of autocomplete options
229
- # @param options [Hash] Options for the autocomplete component
230
- # @return [String] Rendered HTML
172
+ sig do
173
+ params(
174
+ method: Symbol,
175
+ choices: T.nilable(T::Array[HakumiComponents::SelectionControl::Coercion::EntryInput]),
176
+ options: Contracts::OptionValue
177
+ ).returns(ActiveSupport::SafeBuffer)
178
+ end
231
179
  def autocomplete_field(method, choices = nil, **options)
232
- choices ||= [] # steep:ignore UnannotatedEmptyCollection
233
- enhance_options_with_introspection!(method, options)
234
-
235
- render_form_field(
180
+ render_collection_field(
236
181
  HakumiComponents::Autocomplete::Component,
237
182
  method,
238
- **options.merge(options: choices)
183
+ :options,
184
+ HakumiComponents::SelectionControl::Coercion.entries(ensure_selection_entry_array(choices)),
185
+ options
239
186
  )
240
187
  end
241
188
 
242
- # Renders a Hakumi cascader field (cascading dropdown selector)
243
- #
244
- # @param method [Symbol] Name of the attribute
245
- # @param choices [Array] Array of cascader options with children
246
- # @param options [Hash] Options for the cascader component
247
- # @return [String] Rendered HTML
189
+ sig do
190
+ params(
191
+ method: Symbol,
192
+ choices: T.nilable(T::Array[HakumiComponents::SelectionControl::TreeNode]),
193
+ options: Contracts::OptionValue
194
+ ).returns(ActiveSupport::SafeBuffer)
195
+ end
248
196
  def cascader_field(method, choices = nil, **options)
249
- choices ||= [] # steep:ignore UnannotatedEmptyCollection
250
- enhance_options_with_introspection!(method, options)
251
-
252
- render_form_field(
197
+ render_collection_field(
253
198
  HakumiComponents::Cascader::Component,
254
199
  method,
255
- **options.merge(options: choices)
200
+ :options,
201
+ choices || [],
202
+ options
256
203
  )
257
204
  end
258
205
 
259
- # Renders a Hakumi checkbox field (traditional checkbox, different from switch)
260
- #
261
- # @param method [Symbol] Name of the attribute
262
- # @param options [Hash] Options for the checkbox
263
- # @return [String] Rendered HTML
206
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
264
207
  def checkbox_field(method, **options)
265
- enhance_options_with_introspection!(method, options)
266
-
267
- # Convert truthy/falsy value to boolean for checked attribute
268
- value = object_value(method)
269
- checked = value ? true : false
270
-
271
- render_form_field(
272
- HakumiComponents::Checkbox::Component,
273
- method,
274
- config_overrides: { checked: checked },
275
- **options
276
- )
208
+ render_checked_field(HakumiComponents::Checkbox::Component, method, options)
277
209
  end
278
210
 
279
- # Renders a Hakumi color picker field
280
- #
281
- # @param method [Symbol] Name of the attribute
282
- # @param options [Hash] Options for the color picker component
283
- # @return [String] Rendered HTML
211
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
284
212
  def color_picker_field(method, **options)
285
- enhance_options_with_introspection!(method, options)
286
- render_form_field(HakumiComponents::ColorPicker::Component, method, **options)
213
+ render_simple_field(HakumiComponents::ColorPicker::Component, method, options)
287
214
  end
288
215
 
289
- # Renders a Hakumi time picker field
290
- #
291
- # @param method [Symbol] Name of the attribute
292
- # @param options [Hash] Options for the time picker component
293
- # @return [String] Rendered HTML
216
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
294
217
  def time_picker_field(method, **options)
295
- enhance_options_with_introspection!(method, options)
296
- render_form_field(HakumiComponents::TimePicker::Component, method, **options)
218
+ render_simple_field(HakumiComponents::TimePicker::Component, method, options)
297
219
  end
298
220
 
299
- # Renders a Hakumi transfer field (dual list box)
300
- #
301
- # @param method [Symbol] Name of the attribute
302
- # @param data_source [Array] Array of items to transfer
303
- # @param options [Hash] Options for the transfer component
304
- # @return [String] Rendered HTML
221
+ sig { params(method: Symbol, data_source: T.nilable(T::Array[HakumiComponents::Transfer::Coercion::ItemInput]), options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
305
222
  def transfer_field(method, data_source = nil, **options)
306
- data_source ||= [] # steep:ignore UnannotatedEmptyCollection
307
- enhance_options_with_introspection!(method, options)
308
-
309
- render_form_field(
223
+ disabled = options[:disabled] == true
224
+ render_collection_field(
310
225
  HakumiComponents::Transfer::Component,
311
226
  method,
312
- **options.merge(data_source: data_source)
227
+ :data_source,
228
+ HakumiComponents::Transfer::Coercion.items(Array(data_source), disabled: disabled),
229
+ options
313
230
  )
314
231
  end
315
232
 
316
- # Renders a Hakumi upload field (file upload)
317
- #
318
- # @param method [Symbol] Name of the attribute
319
- # @param options [Hash] Options for the upload component
320
- # @option options [String] :action Upload URL (required)
321
- # @return [String] Rendered HTML
233
+ sig { params(method: Symbol, options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
322
234
  def upload_field(method, **options)
323
235
  enhance_options_with_introspection!(method, options)
324
236
 
325
- # Upload requires action parameter
326
237
  raise ArgumentError, "action parameter is required for upload_field" unless options[:action]
327
238
 
328
- render_form_field(HakumiComponents::Upload::Component, method, **options)
239
+ render_form_field(HakumiComponents::Upload::Component, method, options)
329
240
  end
330
241
 
331
- # Renders a Hakumi submit button
332
- #
333
- # @param value [String] Button text (defaults to "Save")
334
- # @param options [Hash] Options for the button
335
- # @return [String] Rendered HTML
242
+ sig { params(value: T.nilable(String), options: Contracts::OptionValue).returns(ActiveSupport::SafeBuffer) }
336
243
  def submit(value = nil, **options)
337
244
  value ||= submit_default_value
338
245
 
339
- # Extract Hakumi-specific options
340
246
  button_type = options.delete(:type) || :primary
341
- button_size = options.delete(:size) || :middle
247
+ button_size = options.delete(:size) || :default
342
248
  block = options.delete(:block) || false
343
249
  loading = options.delete(:loading) || false
344
250
  danger = options.delete(:danger) || false
@@ -356,127 +262,142 @@ module HakumiComponents
356
262
 
357
263
  private
358
264
 
359
- # Gets standard field configuration (name, id, value, errors)
360
- #
361
- # @param method [Symbol] Attribute name
362
- # @param options [Hash] Options hash (modifies in place by deleting :value)
363
- # @param id_suffix [String, nil] Optional suffix for field_id (for radio buttons)
364
- # @return [Hash] Field configuration with :name, :id, :value, :errors
365
- def field_configuration(method, options, id_suffix: nil)
366
- field_name = @object_name ? "#{@object_name}[#{method}]" : method.to_s
367
-
368
- base_id = @object_name ? "#{@object_name}_#{method}" : method.to_s
369
- field_id = id_suffix ? "#{base_id}_#{id_suffix}".parameterize.underscore : base_id
265
+ sig { params(component_class: T.class_of(ViewComponent::Base), method: Symbol, options: Contracts::OptionsHash).returns(ActiveSupport::SafeBuffer) }
266
+ def render_simple_field(component_class, method, options)
267
+ enhance_options_with_introspection!(method, options)
268
+ render_form_field(component_class, method, options)
269
+ end
370
270
 
371
- value = options.delete(:value) || object_value(method)
372
- errors = object_errors(method)
271
+ sig do
272
+ params(
273
+ component_class: T.class_of(ViewComponent::Base),
274
+ method: Symbol,
275
+ collection_key: Symbol,
276
+ collection: T.nilable(Contracts::MixedCollection),
277
+ options: Contracts::OptionsHash
278
+ ).returns(ActiveSupport::SafeBuffer)
279
+ end
280
+ def render_collection_field(component_class, method, collection_key, collection, options)
281
+ enhance_options_with_introspection!(method, options)
282
+ options[collection_key] = collection || []
283
+ render_form_field(component_class, method, options)
284
+ end
373
285
 
374
- {
375
- name: field_name,
376
- id: field_id,
377
- value: value,
378
- errors: errors
379
- }
286
+ sig { params(component_class: T.class_of(ViewComponent::Base), method: Symbol, options: Contracts::OptionsHash).returns(ActiveSupport::SafeBuffer) }
287
+ def render_checked_field(component_class, method, options)
288
+ enhance_options_with_introspection!(method, options)
289
+ checked = !!model_binding.value_for(method)
290
+ render_form_field(component_class, method, options, { checked: checked })
380
291
  end
381
292
 
382
- # Enhances options hash with automatic detection from Rails validations and I18n
383
- #
384
- # @param method [Symbol] Attribute name
385
- # @param options [Hash] Options hash to enhance
386
- # @return [Hash] Enhanced options
387
- def enhance_options_with_introspection!(method, options)
388
- return unless @object
293
+ sig do
294
+ params(
295
+ value: T.nilable(T::Array[HakumiComponents::SelectionControl::Coercion::FlatOptionInput])
296
+ ).returns(T::Array[HakumiComponents::SelectionControl::Coercion::FlatOptionInput])
297
+ end
298
+ def ensure_flat_selection_array(value)
299
+ value || []
300
+ end
389
301
 
390
- # Auto-detect label from I18n (if not provided)
391
- if options[:label].nil?
392
- detected_label = HakumiComponents::Rails::AttributeIntrospection.human_attribute_name(@object, method)
393
- options[:label] = detected_label if detected_label
394
- end
302
+ sig do
303
+ params(
304
+ value: T.nilable(T::Array[HakumiComponents::SelectionControl::Coercion::EntryInput])
305
+ ).returns(T::Array[HakumiComponents::SelectionControl::Coercion::EntryInput])
306
+ end
307
+ def ensure_selection_entry_array(value)
308
+ value || []
309
+ end
395
310
 
396
- # Auto-detect required for UI purposes (asterisk display) - NOT for HTML5 validation
397
- if options[:required].nil?
398
- options[:required] = HakumiComponents::Rails::ValidationIntrospection.required?(@object, method)
399
- end
311
+ sig { params(value: T.nilable(Contracts::OptionValue)).returns(T::Array[HakumiComponents::Mentions::Coercion::OptionInput]) }
312
+ def ensure_mentions_array(value)
313
+ return [] if value.nil?
314
+ return value.filter_map { |item| mentions_option_input(item) } if value.is_a?(Array)
400
315
 
401
- # Auto-detect placeholder from I18n (if not provided)
402
- if options[:placeholder].nil?
403
- detected_placeholder = HakumiComponents::Rails::AttributeIntrospection.placeholder(@object, method)
404
- options[:placeholder] = detected_placeholder if detected_placeholder
405
- end
316
+ single_value = mentions_option_input(value)
317
+ single_value.nil? ? [] : [ single_value ]
318
+ end
406
319
 
407
- # Auto-detect caption (hint) from I18n (if not provided)
408
- if options[:caption].nil?
409
- detected_hint = HakumiComponents::Rails::AttributeIntrospection.hint(@object, method)
410
- options[:caption] = detected_hint if detected_hint
411
- end
320
+ sig { params(value: Contracts::OptionValue).returns(T.nilable(HakumiComponents::Mentions::Coercion::OptionInput)) }
321
+ def mentions_option_input(value)
322
+ case value
323
+ when HakumiComponents::Mentions::Option, String, Symbol, Numeric
324
+ value
325
+ when Hash
326
+ mention_hash = T.let({}, T::Hash[T.any(Symbol, String), T.nilable(T.any(String, Symbol, Numeric))])
327
+ value.each do |key, item|
328
+ return nil unless item.nil? || item.is_a?(String) || item.is_a?(Symbol) || item.is_a?(Numeric)
329
+
330
+ mention_hash[key] = item
331
+ end
332
+ mention_hash
333
+ when Array
334
+ mention_array = T.let([], T::Array[T.nilable(T.any(String, Symbol, Numeric))])
335
+ value.each do |item|
336
+ return nil unless item.nil? || item.is_a?(String) || item.is_a?(Symbol) || item.is_a?(Numeric)
412
337
 
413
- # Auto-generate frontend validation rules from Rails validations
414
- # If manual rules are provided, merge them with auto-detected rules
415
- # Manual rules take priority over auto-detected ones for the same validation type
416
- detected_rules = HakumiComponents::Rails::ValidationMapper.to_frontend_rules(@object, method)
417
- manual_rules = options[:rules]
418
-
419
- if manual_rules.nil?
420
- options[:rules] = detected_rules unless detected_rules.empty?
421
- elsif !detected_rules.empty?
422
- # Merge manual and auto-detected rules, with manual rules taking priority
423
- options[:rules] = HakumiComponents::Rails::ValidationMapper.merge_rules(manual_rules, detected_rules)
424
- end
425
- # If manual_rules is present but detected_rules is empty, keep manual_rules as-is
426
-
427
- # Auto-detect input type based on attribute name and validations (if not provided)
428
- if options[:type].nil?
429
- if HakumiComponents::Rails::ValidationIntrospection.email_field?(@object, method)
430
- options[:type] = :email
431
- elsif HakumiComponents::Rails::ValidationIntrospection.url_field?(@object, method)
432
- options[:type] = :url
433
- else
434
- # Fallback to column type
435
- detected_type = HakumiComponents::Rails::AttributeIntrospection.input_type_from_column(@object, method)
436
- options[:type] = detected_type if detected_type && detected_type != :text
338
+ mention_array << item
437
339
  end
340
+ mention_array
341
+ else
342
+ nil
438
343
  end
344
+ end
345
+
346
+ sig do
347
+ params(
348
+ method: Symbol,
349
+ options: Contracts::OptionsHash,
350
+ id_suffix: T.nilable(String)
351
+ ).returns(HakumiComponents::Types::FormFieldConfig)
352
+ end
353
+ def field_configuration(method, options, id_suffix: nil)
354
+ explicit_value = normalize_form_field_value(options.delete(:value))
355
+ model_binding.build_field_context(method: method, explicit_value: explicit_value, id_suffix: id_suffix).to_options
356
+ end
439
357
 
440
- options
358
+ sig { params(method: Symbol, options: Contracts::OptionsHash).returns(Contracts::OptionsHash) }
359
+ def enhance_options_with_introspection!(method, options)
360
+ enhanced = model_binding.enhance_options(method: method, options: options)
361
+ options.replace(enhanced)
441
362
  end
442
363
 
443
- # Renders a form field component with Rails integration
444
- #
445
- # @param component_class [Class] Component class to render
446
- # @param method [Symbol] Name of the attribute
447
- # @param options [Hash] Options for the component
448
- # @param config_overrides [Hash] Override field configuration (for special cases)
449
- # @return [String] Rendered HTML
450
- def render_form_field(component_class, method, config_overrides: {}, **options)
364
+ sig do
365
+ params(
366
+ component_class: T.class_of(ViewComponent::Base),
367
+ method: Symbol,
368
+ options: Contracts::OptionsHash,
369
+ config_overrides: Contracts::OptionsHash
370
+ ).returns(ActiveSupport::SafeBuffer)
371
+ end
372
+ def render_form_field(component_class, method, options, config_overrides = {})
451
373
  config = field_configuration(method, options)
452
374
  config.merge!(config_overrides)
453
375
 
454
- # Merge all options with standalone: false
455
376
  component_options = config.merge(options).merge(standalone: false)
456
377
 
457
- # Render the component with merged configuration
458
378
  @template.render(component_class.new(**component_options))
459
379
  end
460
380
 
461
- # Gets the current value of an attribute from the object
462
- #
463
- # @param method [Symbol] Attribute name
464
- # @return [Object] Current value
465
- def object_value(method)
466
- @object.respond_to?(method) ? @object.public_send(method) : nil
381
+ sig { params(value: T.nilable(Contracts::OptionValue)).returns(HakumiComponents::Types::FormFieldValue) }
382
+ def normalize_form_field_value(value)
383
+ case value
384
+ when nil, String, Symbol, TrueClass, FalseClass, Numeric, Date, Time, DateTime, ActiveSupport::TimeWithZone, ActiveSupport::SafeBuffer
385
+ value
386
+ when Array
387
+ value.filter_map do |item|
388
+ case item
389
+ when nil, String, Symbol, TrueClass, FalseClass, Numeric, Date, Time, DateTime, ActiveSupport::TimeWithZone, ActiveSupport::SafeBuffer
390
+ item
391
+ end
392
+ end
393
+ else
394
+ nil
395
+ end
467
396
  end
468
397
 
469
- # Gets validation errors for an attribute from the object
470
- # Uses full_messages_for to include attribute name in error messages
471
- #
472
- # @param method [Symbol] Attribute name
473
- # @return [Array<String>] Array of error messages
474
- def object_errors(method)
475
- return [] unless @object.respond_to?(:errors)
476
-
477
- # Use full_messages_for which includes the attribute name
478
- # e.g., "Email can't be blank" instead of "can't be blank"
479
- @object.errors.full_messages_for(method)
398
+ sig { returns(HakumiComponents::FormBuilder::ModelBinding) }
399
+ def model_binding
400
+ @model_binding ||= HakumiComponents::FormBuilder::ModelBinding.new(object_name: @object_name, object: @object)
480
401
  end
481
402
  end
482
403
  end