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,35 +1,19 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module HakumiComponents
4
5
  module Rails
5
- # Maps Rails ActiveModel validations to frontend validation rules
6
- # compatible with Hakumi's ValidationManager (JavaScript)
7
- #
8
- # @example
9
- # class User < ApplicationRecord
10
- # validates :email, presence: true, length: { maximum: 255 }
11
- # end
12
- #
13
- # ValidationMapper.to_frontend_rules(user, :email)
14
- # # => [
15
- # # { required: true, message: "Email can't be blank" },
16
- # # { maxLength: { value: 255 }, message: "Email is too long (maximum is 255 characters)" }
17
- # # ]
18
6
  class ValidationMapper
19
7
  class << self
20
- # Convert Rails validations to frontend validation rules
21
- #
22
- # @param object [ActiveModel::Model, ActiveRecord::Base]
23
- # @param attribute [Symbol] Attribute name
24
- # @return [Array<Hash>] Array of validation rules for frontend
25
- def to_frontend_rules(object, attribute)
26
- return [] unless object
27
- return [] unless object.class.respond_to?(:validators_on)
8
+ extend T::Sig
28
9
 
29
- validators = object.class.validators_on(attribute)
10
+ sig { params(object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
11
+ def to_frontend_rules(object, attribute)
12
+ reflection = ModelReflection.for(object)
13
+ validators = reflection.validators_for(attribute)
30
14
  return [] if validators.empty?
31
15
 
32
- rules = Array.new
16
+ rules = T.let([], T::Array[Types::ValidationRule])
33
17
 
34
18
  validators.each do |validator|
35
19
  mapped_rules = map_validator(validator, object, attribute)
@@ -39,20 +23,18 @@ module HakumiComponents
39
23
  rules.compact
40
24
  end
41
25
 
42
- # Merge manual rules with auto-detected rules
43
- # Manual rules take priority over auto-detected ones for the same validation type
44
- #
45
- # @param manual_rules [Array<Hash>, nil] User-provided rules
46
- # @param auto_rules [Array<Hash>] Auto-detected rules from validators
47
- # @return [Array<Hash>] Merged rules with manual rules taking priority
26
+ sig do
27
+ params(
28
+ manual_rules: T.nilable(T::Array[Types::ValidationRule]),
29
+ auto_rules: T::Array[Types::ValidationRule]
30
+ ).returns(T::Array[Types::ValidationRule])
31
+ end
48
32
  def merge_rules(manual_rules, auto_rules)
49
33
  return auto_rules if manual_rules.nil? || manual_rules.empty?
50
- return manual_rules if auto_rules.nil? || auto_rules.empty?
34
+ return manual_rules if auto_rules.empty?
51
35
 
52
- # Get rule types from manual rules
53
36
  manual_types = manual_rules.map { |rule| extract_rule_type(rule) }.compact
54
37
 
55
- # Filter out auto-detected rules that have the same type as manual rules
56
38
  merged = manual_rules.dup
57
39
  auto_rules.each do |auto_rule|
58
40
  type = extract_rule_type(auto_rule)
@@ -64,15 +46,19 @@ module HakumiComponents
64
46
 
65
47
  private
66
48
 
67
- # Extract the validation type from a rule
68
- # Rules have format: { required: true, message: "..." }
69
- # or { minLength: { value: 3 }, message: "..." }
49
+ sig { params(rule: Types::ValidationRule).returns(T.nilable(Symbol)) }
70
50
  def extract_rule_type(rule)
71
- # The first key that isn't :message is the rule type
72
- (rule.keys - [ :message ]).first
51
+ key = (rule.keys - [ :message ]).first
52
+ key.is_a?(Symbol) ? key : nil
73
53
  end
74
54
 
75
- # Map a single validator to frontend rule(s)
55
+ sig do
56
+ params(
57
+ validator: ActiveModel::Validator,
58
+ object: T.untyped,
59
+ attribute: Symbol
60
+ ).returns(T.nilable(T::Array[Types::ValidationRule]))
61
+ end
76
62
  def map_validator(validator, object, attribute)
77
63
  case validator
78
64
  when ActiveModel::Validations::PresenceValidator
@@ -95,16 +81,10 @@ module HakumiComponents
95
81
  map_acceptance(validator, object, attribute)
96
82
  when ActiveModel::Validations::AbsenceValidator
97
83
  map_absence(validator, object, attribute)
98
- when defined?(ActiveRecord) && ActiveRecord::Validations::PresenceValidator
99
- map_presence(validator, object, attribute)
100
- else
101
- # Unknown validator type - skip for now
102
- # Can be extended by adding more cases
103
- nil
104
84
  end
105
85
  end
106
86
 
107
- # Map presence validation to required rule
87
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
108
88
  def map_presence(validator, object, attribute)
109
89
  [ {
110
90
  required: true,
@@ -112,25 +92,24 @@ module HakumiComponents
112
92
  } ]
113
93
  end
114
94
 
115
- # Map length validation to minLength/maxLength rules
95
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
116
96
  def map_length(validator, object, attribute)
117
- rules = Array.new
97
+ rules = T.let([], T::Array[Types::ValidationRule])
118
98
  options = validator.options
119
99
 
120
- # Exact length
121
100
  exact = options[:is]
122
- if exact
101
+ if exact.is_a?(Numeric)
123
102
  rules << {
124
103
  minLength: { value: exact },
125
104
  maxLength: { value: exact },
126
105
  message: validation_message(object, attribute, :wrong_length, { count: exact }, options)
127
106
  }
128
-
129
107
  return rules
130
108
  end
131
109
 
132
- # Minimum length
133
- min = options[:minimum] || (options[:in]&.min if options[:in].respond_to?(:min))
110
+ min_val = options[:minimum]
111
+ in_range = options[:in]
112
+ min = min_val.is_a?(Numeric) ? min_val : (in_range.is_a?(Range) ? in_range.min : nil)
134
113
  if min
135
114
  rules << {
136
115
  minLength: { value: min },
@@ -138,8 +117,8 @@ module HakumiComponents
138
117
  }
139
118
  end
140
119
 
141
- # Maximum length
142
- max = options[:maximum] || options[:is] || (options[:in]&.max if options[:in].respond_to?(:max))
120
+ max_val = options[:maximum]
121
+ max = max_val.is_a?(Numeric) ? max_val : (in_range.is_a?(Range) ? in_range.max : nil)
143
122
  if max
144
123
  rules << {
145
124
  maxLength: { value: max },
@@ -150,73 +129,47 @@ module HakumiComponents
150
129
  rules
151
130
  end
152
131
 
153
- # Map numericality validation to min/max rules
132
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
154
133
  def map_numericality(validator, object, attribute)
155
- rules = Array.new
134
+ rules = T.let([], T::Array[Types::ValidationRule])
156
135
  options = validator.options
157
136
 
158
- # Minimum value (inclusive)
159
- if options[:greater_than_or_equal_to]
137
+ gte = options[:greater_than_or_equal_to]
138
+ if gte.is_a?(Numeric)
160
139
  rules << {
161
- min: { value: options[:greater_than_or_equal_to] },
162
- message: validation_message(
163
- object,
164
- attribute,
165
- :greater_than_or_equal_to,
166
- { count: options[:greater_than_or_equal_to] },
167
- options
168
- )
140
+ min: { value: gte },
141
+ message: validation_message(object, attribute, :greater_than_or_equal_to, { count: gte }, options)
169
142
  }
170
143
  end
171
144
 
172
- # Minimum value (exclusive) - add 1 for frontend
173
- if options[:greater_than]
174
- min_value = options[:greater_than] + 1
145
+ gt = options[:greater_than]
146
+ if gt.is_a?(Numeric)
175
147
  rules << {
176
- min: { value: min_value },
177
- message: validation_message(
178
- object,
179
- attribute,
180
- :greater_than,
181
- { count: options[:greater_than] },
182
- options
183
- )
148
+ min: { value: gt + 1 },
149
+ message: validation_message(object, attribute, :greater_than, { count: gt }, options)
184
150
  }
185
151
  end
186
152
 
187
- # Maximum value (inclusive)
188
- if options[:less_than_or_equal_to]
153
+ lte = options[:less_than_or_equal_to]
154
+ if lte.is_a?(Numeric)
189
155
  rules << {
190
- max: { value: options[:less_than_or_equal_to] },
191
- message: validation_message(
192
- object,
193
- attribute,
194
- :less_than_or_equal_to,
195
- { count: options[:less_than_or_equal_to] },
196
- options
197
- )
156
+ max: { value: lte },
157
+ message: validation_message(object, attribute, :less_than_or_equal_to, { count: lte }, options)
198
158
  }
199
159
  end
200
160
 
201
- # Maximum value (exclusive) - subtract 1 for frontend
202
- if options[:less_than]
203
- max_value = options[:less_than] - 1
161
+ lt = options[:less_than]
162
+ if lt.is_a?(Numeric)
204
163
  rules << {
205
- max: { value: max_value },
206
- message: validation_message(
207
- object,
208
- attribute,
209
- :less_than,
210
- { count: options[:less_than] },
211
- options
212
- )
164
+ max: { value: lt - 1 },
165
+ message: validation_message(object, attribute, :less_than, { count: lt }, options)
213
166
  }
214
167
  end
215
168
 
216
169
  rules
217
170
  end
218
171
 
219
- # Map format validation to pattern/email/url rule
172
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
220
173
  def map_format(validator, object, attribute)
221
174
  options = validator.options
222
175
  regex = options[:with]
@@ -224,7 +177,6 @@ module HakumiComponents
224
177
 
225
178
  regex_source = regex.source
226
179
 
227
- # Detect email pattern
228
180
  if email_regex?(regex_source) || attribute.to_s.match?(/email/i)
229
181
  return [ {
230
182
  email: true,
@@ -232,7 +184,6 @@ module HakumiComponents
232
184
  } ]
233
185
  end
234
186
 
235
- # Detect URL pattern
236
187
  if url_regex?(regex_source) || attribute.to_s.match?(/url|website|link/i)
237
188
  return [ {
238
189
  url: true,
@@ -240,8 +191,6 @@ module HakumiComponents
240
191
  } ]
241
192
  end
242
193
 
243
- # Generic pattern rule
244
- # Remove anchors (\A, \z, ^, $) as HTML pattern adds them implicitly
245
194
  pattern = regex_source.gsub(/\A(\\A|\^)|(\$|\\z)\z/, "")
246
195
 
247
196
  [ {
@@ -250,7 +199,7 @@ module HakumiComponents
250
199
  } ]
251
200
  end
252
201
 
253
- # Map confirmation validation to match rule
202
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
254
203
  def map_confirmation(validator, object, attribute)
255
204
  [ {
256
205
  match: { field: "#{attribute}_confirmation" },
@@ -258,25 +207,25 @@ module HakumiComponents
258
207
  } ]
259
208
  end
260
209
 
261
- # Map comparison validation to comparison rule
210
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
262
211
  def map_comparison(validator, object, attribute)
263
212
  options = validator.options
264
- rules = Array.new
213
+ rules = T.let([], T::Array[Types::ValidationRule])
265
214
 
266
- comparison_keys = %i[
215
+ comparison_keys = T.let(%i[
267
216
  greater_than
268
217
  greater_than_or_equal_to
269
218
  equal_to
270
219
  less_than
271
220
  less_than_or_equal_to
272
221
  other_than
273
- ]
222
+ ], T::Array[Symbol])
274
223
 
275
224
  comparison_keys.each do |key|
276
225
  next unless options.key?(key)
277
226
 
278
227
  target = options[key]
279
- comparison = build_comparison_target(target, object)
228
+ comparison = build_comparison_target(target)
280
229
  next unless comparison
281
230
 
282
231
  message_options = comparison_message_options(target, object, comparison)
@@ -289,33 +238,31 @@ module HakumiComponents
289
238
  rules
290
239
  end
291
240
 
292
- # Map inclusion validation to enum rule
241
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
293
242
  def map_inclusion(validator, object, attribute)
294
243
  options = validator.options
295
- values = options[:in] || options[:within]
296
-
297
- return [] unless values
244
+ collection = validator_collection(options)
245
+ return [] unless collection
298
246
 
299
247
  [ {
300
- enum: { values: values.to_a },
248
+ enum: { values: collection },
301
249
  message: validation_message(object, attribute, :inclusion, {}, options)
302
250
  } ]
303
251
  end
304
252
 
305
- # Map exclusion validation to forbidden values rule
253
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
306
254
  def map_exclusion(validator, object, attribute)
307
255
  options = validator.options
308
- values = options[:in] || options[:within]
309
-
310
- return [] unless values
256
+ collection = validator_collection(options)
257
+ return [] unless collection
311
258
 
312
259
  [ {
313
- exclusion: { values: values.to_a },
260
+ exclusion: { values: collection },
314
261
  message: validation_message(object, attribute, :exclusion, {}, options)
315
262
  } ]
316
263
  end
317
264
 
318
- # Map acceptance validation to required rule
265
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
319
266
  def map_acceptance(validator, object, attribute)
320
267
  [ {
321
268
  required: true,
@@ -323,7 +270,7 @@ module HakumiComponents
323
270
  } ]
324
271
  end
325
272
 
326
- # Map absence validation to absent rule (field must be blank)
273
+ sig { params(validator: ActiveModel::Validator, object: T.untyped, attribute: Symbol).returns(T::Array[Types::ValidationRule]) }
327
274
  def map_absence(validator, object, attribute)
328
275
  [ {
329
276
  absent: true,
@@ -331,93 +278,109 @@ module HakumiComponents
331
278
  } ]
332
279
  end
333
280
 
334
- # Check if regex matches email pattern
281
+ sig { params(options: Types::ValidatorOptions).returns(T.nilable(Types::ValidationList)) }
282
+ def validator_collection(options)
283
+ values = options[:in] || options[:within]
284
+ return values if values.is_a?(Array)
285
+ return values.to_a if values.is_a?(Range)
286
+
287
+ nil
288
+ end
289
+
290
+ sig { params(regex_source: String).returns(T::Boolean) }
335
291
  def email_regex?(regex_source)
336
- # Common email regex patterns
337
292
  regex_source.match?(/@.*\./) || regex_source.include?("EMAIL")
338
293
  end
339
294
 
340
- # Check if regex matches URL pattern
295
+ sig { params(regex_source: String).returns(T::Boolean) }
341
296
  def url_regex?(regex_source)
342
- # Common URL regex patterns
343
297
  regex_source.match?(/https?|:\/\//) || regex_source.include?("URL")
344
298
  end
345
299
 
346
- # Humanize attribute name
300
+ sig { params(attribute: T.any(Symbol, String)).returns(String) }
347
301
  def humanize(attribute)
348
302
  attribute.to_s.humanize
349
303
  end
350
304
 
351
- # Use Rails' error message generation to honor I18n and custom messages
305
+ sig do
306
+ params(
307
+ object: T.untyped,
308
+ attribute: Symbol,
309
+ type: Symbol,
310
+ options: Types::MessageOptions,
311
+ validator_options: Types::ValidatorOptions
312
+ ).returns(String)
313
+ end
352
314
  def validation_message(object, attribute, type, options, validator_options)
353
- return fallback_message(attribute, type, options, validator_options) unless object&.respond_to?(:errors)
354
- return fallback_message(attribute, type, options, validator_options) if anonymous_class?(object)
355
-
356
- value = object.respond_to?(attribute) ? object.public_send(attribute) : nil
357
- message = object.errors.generate_message(
358
- attribute,
359
- type,
360
- options.merge(value: value, message: validator_options[:message])
361
- )
315
+ reflection = ModelReflection.for(object)
316
+ errors_obj = reflection.errors
317
+ return fallback_message(attribute, type, options, validator_options) unless errors_obj
318
+ return fallback_message(attribute, type, options, validator_options) if reflection.anonymous_class?
362
319
 
363
- if object.errors.respond_to?(:full_message) && object.class.name && !object.class.name.empty?
364
- object.errors.full_message(attribute, message)
320
+ value = reflection.attribute_value(attribute)
321
+ merge_opts = options.merge(value: value, message: validator_options[:message])
322
+ message = errors_obj.generate_message(attribute, type, merge_opts)
323
+
324
+ class_name = object.class.name
325
+ if errors_obj.respond_to?(:full_message) && class_name && !class_name.empty?
326
+ errors_obj.full_message(attribute, message)
365
327
  else
366
328
  message
367
329
  end
368
330
  end
369
331
 
332
+ sig { params(object: T.untyped).returns(T::Boolean) }
370
333
  def anonymous_class?(object)
371
- class_name = object.class.name
372
- class_name.nil? || class_name.empty?
334
+ ModelReflection.for(object).anonymous_class?
373
335
  end
374
336
 
375
- # Build comparison target from validator option value
376
- #
377
- # @param target [Symbol, String, Proc, Object] The comparison target value
378
- # @param _object [Object] The model object (unused, Procs are not evaluated)
379
- # @return [Hash, nil] Hash with :field or :value key, or nil if cannot be used client-side
380
- #
381
- # @note Procs are always ignored (return nil) because they cannot be reliably
382
- # evaluated on the client-side. For field comparisons, use Symbol references:
383
- # `comparison: { greater_than: :start_date }` instead of Procs.
384
- def build_comparison_target(target, _object)
337
+ sig { params(target: Types::ValidatorOptionValue).returns(T.nilable(Types::ComparisonOperand)) }
338
+ def build_comparison_target(target)
385
339
  case target
386
340
  when Symbol, String
387
341
  { field: target.to_s }
388
- when Proc
389
- # Procs cannot be evaluated reliably on client-side
390
- # Skip this validation rule for frontend
391
- nil
392
- else
342
+ when Numeric
393
343
  { value: target }
344
+ else
345
+ nil
394
346
  end
395
347
  end
396
348
 
349
+ sig { params(target: Types::ValidatorOptionValue, object: T.untyped, comparison: Types::ComparisonOperand).returns(Types::MessageOptions) }
397
350
  def comparison_message_options(target, object, comparison)
398
351
  if comparison[:field]
399
- {
400
- count: humanize(comparison[:field])
401
- }
352
+ field = comparison[:field]
353
+ return { count: humanize(field) } if field.is_a?(String)
354
+
355
+ {}
402
356
  else
403
357
  resolved = resolve_comparison_value(target, object)
404
- {
405
- count: resolved
406
- }
358
+ { count: resolved }
407
359
  end
408
360
  end
409
361
 
362
+ sig { params(target: Types::ValidatorOptionValue, object: T.untyped).returns(T.nilable(Types::ValidationComparable)) }
410
363
  def resolve_comparison_value(target, object)
411
- return object.public_send(target) if target.is_a?(Symbol) && object&.respond_to?(target)
412
- return target.call(object) if target.respond_to?(:call)
413
-
414
- target
364
+ if target.is_a?(Symbol) && object&.respond_to?(target)
365
+ result = ModelReflection.for(object).attribute_value(target)
366
+ result.is_a?(Numeric) ? result : result.to_s
367
+ elsif target.is_a?(Numeric)
368
+ target
369
+ elsif target.is_a?(String)
370
+ target
371
+ end
415
372
  rescue StandardError
416
373
  nil
417
374
  end
418
375
 
419
- # Fallback for standalone objects or anonymous classes where Rails can't build model-based messages.
420
- # This keeps validations usable without requiring ActiveRecord models.
376
+ sig do
377
+ params(
378
+ attribute: T.any(Symbol, String),
379
+ type: Symbol,
380
+ options: Types::MessageOptions,
381
+ validator_options: Types::ValidatorOptions
382
+ ).returns(String)
383
+ end
421
384
  def fallback_message(attribute, type, options, validator_options)
422
385
  message = validator_options[:message]
423
386
  return message if message.is_a?(String)
@@ -439,43 +402,27 @@ module HakumiComponents
439
402
  "#{humanize(attribute)} #{translated}"
440
403
  end
441
404
 
405
+ sig { params(type: Symbol, options: Types::MessageOptions).returns(String) }
442
406
  def fallback_message_text(type, options)
443
407
  count = options[:count]
444
408
  case type
445
- when :blank
446
- "can't be blank"
447
- when :too_short
448
- "is too short (minimum is #{count} characters)"
449
- when :too_long
450
- "is too long (maximum is #{count} characters)"
451
- when :wrong_length
452
- "is the wrong length (should be #{count} characters)"
453
- when :greater_than_or_equal_to
454
- "must be greater than or equal to #{count}"
455
- when :greater_than
456
- "must be greater than #{count}"
457
- when :less_than_or_equal_to
458
- "must be less than or equal to #{count}"
459
- when :less_than
460
- "must be less than #{count}"
461
- when :equal_to
462
- "must be equal to #{count}"
463
- when :other_than
464
- "must be other than #{count}"
465
- when :confirmation
466
- "doesn't match #{options[:attribute]}"
467
- when :inclusion
468
- "is not included in the list"
469
- when :exclusion
470
- "is reserved"
471
- when :accepted
472
- "must be accepted"
473
- when :present
474
- "must be blank"
475
- when :invalid
476
- "is invalid"
477
- else
478
- type.to_s.tr("_", " ")
409
+ when :blank then "can't be blank"
410
+ when :too_short then "is too short (minimum is #{count} characters)"
411
+ when :too_long then "is too long (maximum is #{count} characters)"
412
+ when :wrong_length then "is the wrong length (should be #{count} characters)"
413
+ when :greater_than_or_equal_to then "must be greater than or equal to #{count}"
414
+ when :greater_than then "must be greater than #{count}"
415
+ when :less_than_or_equal_to then "must be less than or equal to #{count}"
416
+ when :less_than then "must be less than #{count}"
417
+ when :equal_to then "must be equal to #{count}"
418
+ when :other_than then "must be other than #{count}"
419
+ when :confirmation then "doesn't match #{options[:attribute]}"
420
+ when :inclusion then "is not included in the list"
421
+ when :exclusion then "is reserved"
422
+ when :accepted then "must be accepted"
423
+ when :present then "must be blank"
424
+ when :invalid then "is invalid"
425
+ else type.to_s.tr("_", " ")
479
426
  end
480
427
  end
481
428
  end
@@ -1,5 +1,7 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
4
+ require_relative "rails/model_reflection"
3
5
  require_relative "rails/validation_introspection"
4
6
  require_relative "rails/attribute_introspection"
5
7
  require_relative "rails/validation_mapper"