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,31 +1,47 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module HakumiComponents
4
5
  module Badge
5
6
  class Component < HakumiComponents::BaseComponent
7
+ extend T::Sig
8
+
9
+ Status = T.type_alias { T.nilable(T.any(Symbol, String)) }
10
+ Count = T.type_alias { T.nilable(T.any(Integer, Float, String)) }
11
+ IconName = T.type_alias { T.nilable(T.any(Symbol, String)) }
12
+
13
+ STATUSES = T.let(%i[success processing default error warning].freeze, T::Array[Symbol])
14
+
15
+ sig do
16
+ params(
17
+ count: Count,
18
+ dot: T::Boolean,
19
+ overflow_count: Integer,
20
+ status: Status,
21
+ text: T.nilable(String),
22
+ color: T.nilable(String),
23
+ show_zero: T::Boolean,
24
+ icon: IconName,
25
+ html_options: Types::HtmlAttributeValue
26
+ ).void
27
+ end
6
28
  def initialize(count: nil, dot: false, overflow_count: 99, status: nil, text: nil, color: nil, show_zero: false, icon: nil, **html_options)
7
- @count = count
8
- @dot = dot
9
- @overflow_count = overflow_count
10
- @status = status
11
- @text = text
12
- @color = color
13
- @show_zero = show_zero
14
- @icon = icon
15
- @html_options = html_options
29
+ @count = T.let(count, Count)
30
+ @dot = T.let(cast_boolean(dot) ? true : false, T::Boolean)
31
+ @overflow_count = T.let(overflow_count, Integer)
32
+ @status = T.let(status&.to_sym, T.nilable(Symbol))
33
+ @text = T.let(text, T.nilable(String))
34
+ @color = T.let(color, T.nilable(String))
35
+ @show_zero = T.let(cast_boolean(show_zero) ? true : false, T::Boolean)
36
+ @icon = T.let(icon, IconName)
37
+ @html_options = T.let(html_options, Types::HtmlAttributes)
38
+
39
+ validate_props!
16
40
  end
17
41
 
42
+ sig { returns(ActiveSupport::SafeBuffer) }
18
43
  def call
19
- # Direct rendering to ensure class is present
20
- classes = [ "hakumi-badge", @html_options[:class] ].compact.join(" ")
21
-
22
- if @status || @text
23
- classes += " hakumi-badge-status"
24
- elsif content.blank?
25
- classes += " hakumi-badge-not-a-wrapper"
26
- end
27
-
28
- content_tag(:span, class: classes, **@html_options.except(:class)) do
44
+ content_tag(:span, wrapper_attributes) do
29
45
  safe_join([
30
46
  content,
31
47
  badge_element,
@@ -34,49 +50,87 @@ module HakumiComponents
34
50
  end
35
51
  end
36
52
 
53
+ sig { returns(Types::HtmlAttributes) }
54
+ def wrapper_attributes
55
+ merge_attributes({ class: wrapper_classes }, @html_options.except(:class))
56
+ end
57
+
37
58
  private
38
59
 
39
- def badge_element
40
- if @status || @text
41
- styles = []
42
- styles << "background: #{@color}" if @color
60
+ sig { void }
61
+ def validate_props!
62
+ validate_inclusion!(:status, STATUSES)
63
+ end
43
64
 
44
- classes = [ "hakumi-badge-status-dot" ]
45
- classes << "hakumi-badge-status-#{@status}" if @status
65
+ sig { returns(String) }
66
+ def wrapper_classes
67
+ modifiers = {
68
+ status: (@status || @text).present?,
69
+ "not-a-wrapper": content.blank? && !(@status || @text)
70
+ }
71
+ component_classes("badge", modifiers, @html_options)
72
+ end
46
73
 
47
- tag.span(class: classes.join(" "), style: (styles.join(";") if styles.any?))
74
+ sig { returns(T.nilable(ActiveSupport::SafeBuffer)) }
75
+ def badge_element
76
+ if @status || @text
77
+ status_dot_element
48
78
  elsif @dot
49
- tag.sup(class: "hakumi-badge-dot", style: ("background: #{@color}" if @color))
79
+ dot_element
50
80
  elsif @count || @icon || @show_zero
51
- # Show if count is present (and valid) OR icon is present OR show_zero is true
52
- should_show = @icon.present? || @show_zero || (@count && (@count.is_a?(String) || @count > 0))
53
-
54
- if should_show
55
- classes = [ "hakumi-badge-count" ]
56
- classes << "hakumi-badge-custom-component" if @icon
57
-
58
- styles = []
59
- styles << "background: #{@color}" if @color && !@icon
60
- styles << "color: #{@color}" if @color && @icon
61
-
62
- tag.sup(class: classes.join(" "), title: @count, style: (styles.join("; ") if styles.any?)) do
63
- if @icon
64
- render HakumiComponents::Icon::Component.new(name: @icon)
65
- else
66
- display_count.to_s
67
- end
68
- end
81
+ count_element
82
+ end
83
+ end
84
+
85
+ sig { returns(ActiveSupport::SafeBuffer) }
86
+ def status_dot_element
87
+ styles = T.let([], T::Array[String])
88
+ styles << "background: #{@color}" if @color
89
+
90
+ extras = @status ? [ "hakumi-badge-status-#{@status}" ] : []
91
+ dot_classes = class_names("badge-status-dot", {}, extras)
92
+
93
+ tag.span(class: dot_classes, style: (styles.join(";") if styles.any?))
94
+ end
95
+
96
+ sig { returns(ActiveSupport::SafeBuffer) }
97
+ def dot_element
98
+ tag.sup(class: "hakumi-badge-dot", style: ("background: #{@color}" if @color))
99
+ end
100
+
101
+ sig { returns(T.nilable(ActiveSupport::SafeBuffer)) }
102
+ def count_element
103
+ # Show if count is present (and valid) OR icon is present OR show_zero is true
104
+ should_show = @icon.present? || @show_zero || (@count && (@count.is_a?(String) || @count > 0))
105
+
106
+ return unless should_show
107
+
108
+ extras = @icon ? [ "hakumi-badge-custom-component" ] : []
109
+ count_classes = class_names("badge-count", {}, extras)
110
+
111
+ styles = T.let([], T::Array[String])
112
+ styles << "background: #{@color}" if @color && !@icon
113
+ styles << "color: #{@color}" if @color && @icon
114
+
115
+ tag.sup(class: count_classes, title: @count, style: (styles.join("; ") if styles.any?)) do
116
+ if @icon
117
+ render HakumiComponents::Icon::Component.new(name: @icon)
118
+ else
119
+ display_count.to_s
69
120
  end
70
121
  end
71
122
  end
72
123
 
124
+ sig { returns(T.nilable(ActiveSupport::SafeBuffer)) }
73
125
  def status_text_element
74
126
  return unless @text
75
127
  tag.span(@text, class: "hakumi-badge-status-text")
76
128
  end
77
129
 
130
+ sig { returns(T.any(Integer, Float, String)) }
78
131
  def display_count
79
132
  return @count if @count.is_a?(String)
133
+ return 0 if @count.nil?
80
134
  return "#{@overflow_count}+" if @count > @overflow_count
81
135
  @count
82
136
  end
@@ -1,18 +1,37 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module HakumiComponents
4
- # Base class for all Hakumi components
5
- # Provides shared utilities and helpers
6
5
  class BaseComponent < ViewComponent::Base
7
- # Generate class names with the hakumi- prefix
8
- # @param base [String] Base class name (e.g., 'btn', 'icon')
9
- # @param modifiers [Hash] Hash of modifier keys and their values
10
- # @param extras [Array<String>] Additional classes to append
11
- # @return [String] Combined class string
12
- #
13
- # Example:
14
- # class_names('button', { type: :primary, size: :large }, ['custom-class'])
15
- # # => "hakumi-button hakumi-button-primary hakumi-button-large custom-class"
6
+ extend T::Sig
7
+
8
+ SizeValue = T.type_alias { T.any(Types::HtmlKey, Integer) }
9
+ DimensionInput = T.type_alias { T.nilable(T.any(Numeric, String)) }
10
+ I18nOptionValue = T.type_alias { Types::ValidationComparable }
11
+ DateLikeValue = T.type_alias { T.any(Date, Time, DateTime, ActiveSupport::TimeWithZone) }
12
+ DateInput = T.type_alias { T.nilable(T.any(DateLikeValue, Types::HtmlKey)) }
13
+ SymbolInput = T.type_alias { T.nilable(Types::HtmlKey) }
14
+ PresenceArray = T.type_alias { T::Array[HakumiComponents::Types::FormFieldScalar] }
15
+ ControllerOptions = T.type_alias { T.any(ActionController::Parameters, Types::HtmlAttributes) }
16
+ PresenceScalar = T.type_alias { T.any(Types::ValidationPrimitive, DateLikeValue) }
17
+ HtmlPayloadInput = T.type_alias { T.any(ActionController::Parameters, Types::DataAttributes, Types::HtmlClassList) }
18
+ PresenceValue = T.type_alias do
19
+ T.nilable(T.any(
20
+ Types::Renderable,
21
+ PresenceScalar,
22
+ PresenceArray
23
+ ))
24
+ end
25
+ RawHtmlInput = T.type_alias do
26
+ T.nilable(T.any(
27
+ Types::HtmlPrimitive,
28
+ HtmlPayloadInput
29
+ ))
30
+ end
31
+
32
+ SIZES = T.let(%i[small default large].freeze, T::Array[Symbol])
33
+
34
+ sig { params(base: String, modifiers: Types::ClassModifiers, extras: Types::CssClasses).returns(String) }
16
35
  def class_names(base, modifiers = {}, extras = [])
17
36
  classes = [ "hakumi-#{base}" ]
18
37
 
@@ -30,52 +49,45 @@ module HakumiComponents
30
49
  classes.join(" ")
31
50
  end
32
51
 
33
- # Merge HTML attributes with defaults
34
- # @param defaults [Hash] Default attributes
35
- # @param overrides [Hash] User-provided attributes
36
- # @return [Hash] Merged attributes
52
+ sig { params(defaults: Types::HtmlAttributes, overrides: Types::HtmlAttributes).returns(Types::HtmlAttributes) }
37
53
  def merge_attributes(defaults = {}, overrides = {})
38
- merged = defaults.deep_dup
54
+ merged = T.let(defaults.deep_dup, Types::HtmlAttributes)
39
55
 
40
- # Special handling for class attribute
41
56
  if defaults[:class] && overrides[:class]
42
57
  merged[:class] = [ defaults[:class], overrides[:class] ].join(" ")
43
58
  elsif overrides[:class]
44
59
  merged[:class] = overrides[:class]
45
60
  end
46
61
 
47
- # Special handling for style attribute
48
62
  if defaults[:style] && overrides[:style]
49
63
  merged[:style] = [ defaults[:style].to_s.chomp(";"), overrides[:style] ].join("; ")
50
64
  elsif overrides[:style]
51
65
  merged[:style] = overrides[:style]
52
66
  end
53
67
 
54
- # Merge data attributes
55
- if overrides[:data]
56
- merged[:data] = (defaults[:data] || {}).merge(overrides[:data])
57
- elsif defaults[:data]
58
- merged[:data] = defaults[:data]
59
- end
68
+ merged_data = merge_attribute_hash(defaults[:data], overrides[:data])
69
+ merged[:data] = merged_data if merged_data
60
70
 
61
- # Merge other attributes
62
- merged.merge!(overrides.except(:class, :data, :style))
71
+ merged_aria = merge_attribute_hash(defaults[:aria], overrides[:aria])
72
+ merged[:aria] = merged_aria if merged_aria
73
+
74
+ merged.merge!(overrides.except(:aria, :class, :data, :style))
63
75
 
64
76
  ensure_dom_id!(merged)
65
77
 
66
78
  merged
67
79
  end
68
80
 
69
- # Internal helper to check presence without clobbering ActiveSupport#present?
70
- # @param value [Object] Value to check
71
- # @return [Boolean]
81
+ sig { params(value: PresenceValue).returns(T::Boolean) }
72
82
  def value_present?(value)
73
- !value.nil? && value != false && (value.respond_to?(:empty?) ? !value.empty? : true)
83
+ return false if value.nil? || value == false
84
+ return !value.empty? if value.is_a?(String)
85
+ return !value.empty? if value.is_a?(Array)
86
+
87
+ true
74
88
  end
75
89
 
76
- # Validate required props
77
- # @param props [Array<Symbol>] List of required prop names
78
- # @raise [ArgumentError] if any prop is missing
90
+ sig { params(props: Symbol).void }
79
91
  def validate_required!(*props)
80
92
  props.each do |prop|
81
93
  value = instance_variable_get("@#{prop}")
@@ -83,10 +95,7 @@ module HakumiComponents
83
95
  end
84
96
  end
85
97
 
86
- # Validate prop is one of allowed values
87
- # @param prop [Symbol] Prop name
88
- # @param allowed [Array] Allowed values
89
- # @raise [ArgumentError] if value is not allowed
98
+ sig { params(prop: Symbol, allowed: T::Array[T.nilable(SizeValue)]).void }
90
99
  def validate_inclusion!(prop, allowed)
91
100
  value = instance_variable_get("@#{prop}")
92
101
  return if value.nil?
@@ -95,56 +104,40 @@ module HakumiComponents
95
104
  raise ArgumentError, "#{prop} must be one of #{allowed.inspect}, got #{value.inspect}"
96
105
  end
97
106
 
107
+ sig { params(attrs: Types::HtmlAttributes).void }
98
108
  def ensure_dom_id!(attrs)
99
- controllers = attrs.dig(:data, :controller)
100
- return unless value_present?(controllers)
101
- return if value_present?(attrs[:id])
109
+ data = attrs[:data]
110
+ controllers = data.is_a?(Hash) ? data[:controller] : nil
111
+ return unless controllers.is_a?(String) && !controllers.empty?
112
+
113
+ id = attrs[:id]
114
+ return if id.is_a?(String) && !id.empty?
102
115
 
103
116
  attrs[:id] = generate_id("hakumi-component")
104
117
  end
105
118
 
106
- # Generate a unique ID with a prefix
107
- # @param prefix [String] Prefix for the ID
108
- # @param length [Integer] Length of the random hex (default: 4)
109
- # @return [String] Generated ID
110
- #
111
- # Example:
112
- # generate_id("radio") # => "radio_a3f8"
113
- # generate_id("select", 6) # => "select_a3f8b2"
119
+ sig { params(prefix: String, length: Integer).returns(String) }
114
120
  def generate_id(prefix, length: 4)
115
121
  "#{prefix}_#{SecureRandom.hex(length)}"
116
122
  end
117
123
 
118
- # Render a value that can be a String, ViewComponent, or other object
119
- # @param value [Object] Value to render
120
- # @return [String, nil] Rendered value
124
+ sig { params(value: Types::Renderable).returns(Types::RenderedContent) }
121
125
  def render_value(value)
122
126
  return nil unless value.present?
123
127
  return value if value.is_a?(String) || value.is_a?(ActiveSupport::SafeBuffer)
128
+ return value.to_s if value.is_a?(ViewComponent::Slot)
124
129
  return render(value) if value.respond_to?(:render_in)
125
130
 
126
- value
131
+ nil
127
132
  end
128
133
 
129
- # Convert size string/symbol to integer pixels
130
- # Useful for components that accept both symbolic (:small, :large) and numeric sizes
131
- # @param size [Symbol, Integer] Size value
132
- # @param mapping [Hash] Size to pixel mapping
133
- # @return [Integer] Size in pixels
134
+ sig { params(size: SizeValue, mapping: T::Hash[Symbol, Integer]).returns(SizeValue) }
134
135
  def size_to_pixels(size, mapping = {})
135
136
  return size if size.is_a?(Integer)
136
- mapping[size] || size
137
- end
138
-
139
- # Convert dimension value to CSS string
140
- # @param value [Numeric, String, nil] Dimension value
141
- # @return [String, nil] CSS dimension string
142
- #
143
- # Examples:
144
- # dimension_to_css(100) # => "100px"
145
- # dimension_to_css("50%") # => "50%"
146
- # dimension_to_css("2rem") # => "2rem"
147
- # dimension_to_css(nil) # => nil
137
+ mapping[size.to_sym] || size
138
+ end
139
+
140
+ sig { params(value: DimensionInput).returns(T.nilable(String)) }
148
141
  def dimension_to_css(value)
149
142
  return nil if value.nil?
150
143
  return value if value.is_a?(String) && value.strip.present?
@@ -154,30 +147,174 @@ module HakumiComponents
154
147
  string.empty? ? nil : string
155
148
  end
156
149
 
157
- # Cast a value to boolean using ActiveModel::Type::Boolean
158
- # @param value [Object] Value to cast
159
- # @return [Boolean] Casted boolean value
160
- #
161
- # Example:
162
- # cast_boolean("true") # => true
163
- # cast_boolean("1") # => true
164
- # cast_boolean(0) # => false
165
- # cast_boolean(nil) # => nil
150
+ sig { params(controller: String, values: Types::StimulusValues).returns(Types::StimulusData) }
151
+ def stimulus_attrs(controller, values = {})
152
+ data = T.let({ "controller" => controller }, Types::StimulusData)
153
+ values.each do |key, value|
154
+ next if value.nil?
155
+
156
+ serialized = value.is_a?(Array) || value.is_a?(Hash) ? value.to_json : value
157
+ data["#{controller}-#{key.to_s.underscore.dasherize}-value"] = serialized
158
+ end
159
+ data
160
+ end
161
+
162
+ sig { params(value: Types::HtmlAttributeValue).returns(Types::DataAttributes) }
163
+ def data_attributes_from(value)
164
+ attrs = T.let({}, Types::DataAttributes)
165
+ return attrs unless value.is_a?(Hash)
166
+
167
+ value.each do |key, entry|
168
+ attrs[key] = entry
169
+ end
170
+ attrs
171
+ end
172
+
173
+ sig { params(existing: Types::HtmlAttributeValue, token: String).returns(String) }
174
+ def append_data_token(existing, token)
175
+ return token unless existing.is_a?(String) && existing.present?
176
+
177
+ [ existing, token ].join(" ")
178
+ end
179
+
180
+ BOOLEAN_CASTER = T.let(ActiveModel::Type::Boolean.new.freeze, ActiveModel::Type::Boolean)
181
+ private_constant :BOOLEAN_CASTER
182
+
183
+ sig { params(value: Types::HtmlPrimitive).returns(T.nilable(T::Boolean)) }
184
+ def self.cast_boolean(value)
185
+ BOOLEAN_CASTER.cast(value)
186
+ end
187
+
188
+ sig { params(value: Types::HtmlAttributeValue).returns(Types::HtmlPrimitive) }
189
+ def self.html_primitive_param(value)
190
+ return value if value.is_a?(String)
191
+ return value if value.is_a?(Symbol)
192
+ return value if value.is_a?(TrueClass)
193
+ return value if value.is_a?(FalseClass)
194
+ return value if value.is_a?(Numeric)
195
+
196
+ nil
197
+ end
198
+
199
+ sig do
200
+ params(
201
+ params: ControllerOptions,
202
+ key: Symbol
203
+ ).returns(Types::HtmlAttributeValue)
204
+ end
205
+ def self.html_param(params, key)
206
+ coerce_html_attribute_value(params[key])
207
+ end
208
+
209
+ sig { params(value: Types::HtmlAttributeValue).returns(T.nilable(T::Boolean)) }
210
+ def self.boolean_html_param(value)
211
+ scalar = html_primitive_param(value)
212
+ return nil if scalar.nil?
213
+
214
+ cast_boolean(scalar)
215
+ end
216
+
217
+ sig { params(value: Types::HtmlAttributeValue).returns(T.nilable(Symbol)) }
218
+ def self.symbol_html_param(value)
219
+ return value.to_sym if value.is_a?(String) || value.is_a?(Symbol)
220
+
221
+ nil
222
+ end
223
+
224
+ sig { params(value: Types::HtmlAttributeValue).returns(T.nilable(Integer)) }
225
+ def self.integer_html_param(value)
226
+ return value if value.is_a?(Integer)
227
+ return value.to_i if value.is_a?(String)
228
+
229
+ nil
230
+ end
231
+
232
+ sig { params(value: Types::HtmlAttributeValue).returns(T.nilable(Float)) }
233
+ def self.float_html_param(value)
234
+ return value.to_f if value.is_a?(String)
235
+ return value.to_f if value.is_a?(Numeric)
236
+
237
+ nil
238
+ end
239
+
240
+ sig { params(value: Types::HtmlAttributeValue).returns(T.nilable(String)) }
241
+ def self.string_html_param(value)
242
+ return nil if value.nil?
243
+ return value if value.is_a?(String)
244
+ return value.to_s if value.is_a?(Symbol) || value.is_a?(Numeric)
245
+
246
+ nil
247
+ end
248
+
249
+ sig { params(value: Types::HtmlAttributeValue).returns(T.nilable(Types::StringOrSymbolArray)) }
250
+ def self.string_or_symbol_array_html_param(value)
251
+ return nil unless value.is_a?(Array)
252
+
253
+ entries = T.let([], Types::StringOrSymbolArray)
254
+ value.each do |entry|
255
+ next if entry.nil?
256
+
257
+ entries << entry
258
+ end
259
+ entries
260
+ end
261
+
262
+ class << self
263
+ extend T::Sig
264
+
265
+ private
266
+
267
+ sig { params(value: RawHtmlInput).returns(Types::HtmlPrimitive) }
268
+ def coerce_html_primitive(value)
269
+ return value if value.is_a?(String)
270
+ return value if value.is_a?(Symbol)
271
+ return value if value.is_a?(TrueClass)
272
+ return value if value.is_a?(FalseClass)
273
+ return value if value.is_a?(Numeric)
274
+
275
+ nil
276
+ end
277
+
278
+ sig { params(value: RawHtmlInput).returns(Types::HtmlAttributeValue) }
279
+ def coerce_html_attribute_value(value)
280
+ primitive = coerce_html_primitive(value)
281
+ return primitive unless primitive.nil?
282
+
283
+ if value.is_a?(Array)
284
+ entries = T.let([], T::Array[T.nilable(String)])
285
+ value.each do |entry|
286
+ entries << entry if entry.nil? || entry.is_a?(String)
287
+ end
288
+ return entries
289
+ end
290
+
291
+ source_hash = if value.is_a?(ActionController::Parameters)
292
+ value.to_unsafe_h
293
+ elsif value.is_a?(Hash)
294
+ value
295
+ end
296
+
297
+ if source_hash
298
+ attrs = T.let({}, Types::DataAttributes)
299
+ source_hash.each do |entry_key, entry_value|
300
+ entry_primitive = coerce_html_primitive(entry_value)
301
+ attrs[entry_key] = entry_primitive
302
+ end
303
+ return attrs
304
+ end
305
+
306
+ nil
307
+ end
308
+ end
309
+
310
+ sig { params(value: Types::HtmlPrimitive).returns(T.nilable(T::Boolean)) }
166
311
  def cast_boolean(value)
167
- ActiveModel::Type::Boolean.new.cast(value)
168
- end
169
-
170
- # Build inline style string from array or hash
171
- # @param styles [Array<String>, Hash] Styles as array of CSS strings or hash of property => value
172
- # @return [String, nil] Combined style string
173
- #
174
- # Example with array:
175
- # build_inline_style(["color: red", "font-size: 14px"]) # => "color: red; font-size: 14px"
176
- #
177
- # Example with hash:
178
- # build_inline_style(color: "red", "font-size": "14px") # => "color: red; font-size: 14px"
312
+ self.class.cast_boolean(value)
313
+ end
314
+
315
+ sig { params(styles: T.nilable(Types::StyleValue)).returns(T.nilable(String)) }
179
316
  def build_inline_style(styles)
180
- return nil if styles.blank?
317
+ return nil if styles.respond_to?(:blank?) && styles.blank?
181
318
 
182
319
  style_array = if styles.is_a?(Hash)
183
320
  styles.compact.map { |key, value| "#{key}: #{value}" }
@@ -188,5 +325,67 @@ module HakumiComponents
188
325
  return nil if style_array.empty?
189
326
  style_array.join("; ")
190
327
  end
328
+
329
+ sig { params(attributes: Types::HtmlAttributes).returns(Types::CssClasses) }
330
+ def html_classes(attributes)
331
+ value = attributes[:class]
332
+ return value if value.is_a?(String) || value.is_a?(Array)
333
+
334
+ nil
335
+ end
336
+
337
+ sig { params(base: String, modifiers: Types::ClassModifiers, attributes: Types::HtmlAttributes).returns(String) }
338
+ def component_classes(base, modifiers = {}, attributes = {})
339
+ class_names(base, modifiers, html_classes(attributes))
340
+ end
341
+
342
+ sig { params(attributes: Types::HtmlAttributes).returns(T.nilable(String)) }
343
+ def html_style(attributes)
344
+ value = attributes[:style]
345
+ value.is_a?(String) ? value : nil
346
+ end
347
+
348
+ sig { params(key: Types::HtmlKey, default: String, options: I18nOptionValue).returns(String) }
349
+ def translate_with_default(key, default:, **options)
350
+ I18n.t(key, scope: i18n_scope, default: default, **options)
351
+ end
352
+
353
+ sig { params(key: Types::HtmlKey, default: String, options: I18nOptionValue).returns(String) }
354
+ def t_default(key, default:, **options)
355
+ I18n.t(key, scope: i18n_scope, default: default, **options)
356
+ end
357
+
358
+ sig { returns(String) }
359
+ def i18n_scope
360
+ @i18n_scope = T.let(@i18n_scope, T.nilable(String)) unless instance_variable_defined?(:@i18n_scope)
361
+ @i18n_scope ||= begin
362
+ class_name = self.class.name
363
+ raise "#{self.class} must have a name for i18n scope" if class_name.nil?
364
+
365
+ class_name
366
+ .delete_suffix("::Component")
367
+ .underscore
368
+ .tr("/", ".")
369
+ end
370
+ end
371
+
372
+ private
373
+
374
+ sig do
375
+ params(
376
+ defaults: Types::HtmlAttributeValue,
377
+ overrides: Types::HtmlAttributeValue
378
+ ).returns(T.nilable(Types::DataAttributes))
379
+ end
380
+ def merge_attribute_hash(defaults, overrides)
381
+ return defaults if defaults.is_a?(Hash) && !overrides.is_a?(Hash)
382
+ return overrides if overrides.is_a?(Hash) && !defaults.is_a?(Hash)
383
+ return nil unless defaults.is_a?(Hash) && overrides.is_a?(Hash)
384
+
385
+ merged = T.let({}, Types::DataAttributes)
386
+ defaults.each { |key, value| merged[key] = value }
387
+ overrides.each { |key, value| merged[key] = value }
388
+ merged
389
+ end
191
390
  end
192
391
  end