ruby_cms 0.2.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (647) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -72
  3. data/README.md +308 -153
  4. data/Rakefile +0 -18
  5. data/SYNC_RULES.md +84 -0
  6. data/exe/ruby_cms +13 -18
  7. data/lib/generators/ruby_cms/admin_page_generator.rb +116 -60
  8. data/lib/generators/ruby_cms/templates/admin.html.erb +35 -17
  9. data/lib/generators/ruby_cms/templates/admin_page/_admin_table_content.html.erb.tt +25 -0
  10. data/lib/generators/ruby_cms/templates/admin_page/_form.html.erb.tt +36 -0
  11. data/lib/generators/ruby_cms/templates/admin_page/_row.html.erb.tt +20 -0
  12. data/lib/generators/ruby_cms/templates/admin_page/controller.rb.tt +74 -2
  13. data/lib/generators/ruby_cms/templates/admin_page/edit.html.erb.tt +14 -0
  14. data/lib/generators/ruby_cms/templates/admin_page/index.html.erb.tt +21 -10
  15. data/lib/generators/ruby_cms/templates/admin_page/new.html.erb.tt +14 -0
  16. data/lib/generators/ruby_cms/templates/assets/tailwind/application.css +225 -0
  17. data/lib/generators/ruby_cms/templates/components/ruby_ui/accordion/accordion.rb +17 -0
  18. data/lib/generators/ruby_cms/templates/components/ruby_ui/accordion/accordion_content.rb +21 -0
  19. data/lib/generators/ruby_cms/templates/components/ruby_ui/accordion/accordion_default_content.rb +17 -0
  20. data/lib/generators/ruby_cms/templates/components/ruby_ui/accordion/accordion_default_trigger.rb +19 -0
  21. data/lib/generators/ruby_cms/templates/components/ruby_ui/accordion/accordion_icon.rb +38 -0
  22. data/lib/generators/ruby_cms/templates/components/ruby_ui/accordion/accordion_item.rb +28 -0
  23. data/lib/generators/ruby_cms/templates/components/ruby_ui/accordion/accordion_trigger.rb +17 -0
  24. data/lib/generators/ruby_cms/templates/components/ruby_ui/admin_page/admin_page.rb +31 -0
  25. data/lib/generators/ruby_cms/templates/components/ruby_ui/admin_page/admin_page_header.rb +65 -0
  26. data/lib/generators/ruby_cms/templates/components/ruby_ui/admin_page/admin_resource_card.rb +28 -0
  27. data/lib/generators/ruby_cms/templates/components/ruby_ui/admin_page/admin_table_content.rb +20 -0
  28. data/lib/generators/ruby_cms/templates/components/ruby_ui/alert/alert.rb +36 -0
  29. data/lib/generators/ruby_cms/templates/components/ruby_ui/alert/alert_description.rb +17 -0
  30. data/lib/generators/ruby_cms/templates/components/ruby_ui/alert/alert_title.rb +17 -0
  31. data/lib/generators/ruby_cms/templates/components/ruby_ui/aspect_ratio/aspect_ratio.rb +33 -0
  32. data/lib/generators/ruby_cms/templates/components/ruby_ui/avatar/avatar.rb +31 -0
  33. data/lib/generators/ruby_cms/templates/components/ruby_ui/avatar/avatar_fallback.rb +17 -0
  34. data/lib/generators/ruby_cms/templates/components/ruby_ui/avatar/avatar_image.rb +26 -0
  35. data/lib/generators/ruby_cms/templates/components/ruby_ui/badge/badge.rb +60 -0
  36. data/lib/generators/ruby_cms/templates/components/ruby_ui/base.rb +29 -0
  37. data/lib/generators/ruby_cms/templates/components/ruby_ui/box/box.rb +15 -0
  38. data/lib/generators/ruby_cms/templates/components/ruby_ui/breadcrumb/breadcrumb.rb +17 -0
  39. data/lib/generators/ruby_cms/templates/components/ruby_ui/breadcrumb/breadcrumb_ellipsis.rb +39 -0
  40. data/lib/generators/ruby_cms/templates/components/ruby_ui/breadcrumb/breadcrumb_item.rb +17 -0
  41. data/lib/generators/ruby_cms/templates/components/ruby_ui/breadcrumb/breadcrumb_link.rb +22 -0
  42. data/lib/generators/ruby_cms/templates/components/ruby_ui/breadcrumb/breadcrumb_list.rb +17 -0
  43. data/lib/generators/ruby_cms/templates/components/ruby_ui/breadcrumb/breadcrumb_page.rb +19 -0
  44. data/lib/generators/ruby_cms/templates/components/ruby_ui/breadcrumb/breadcrumb_separator.rb +38 -0
  45. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_button.rb +82 -0
  46. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_table.rb +133 -0
  47. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_table_actions.rb +104 -0
  48. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_table_body.rb +17 -0
  49. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_table_checkbox_cell.rb +40 -0
  50. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_table_checkbox_head.rb +34 -0
  51. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_table_delete_modal.rb +43 -0
  52. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_table_header.rb +42 -0
  53. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_table_pagination.rb +115 -0
  54. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_action_table_row.rb +77 -0
  55. data/lib/generators/ruby_cms/templates/components/ruby_ui/bulk_action_table/bulk_actions.rb +86 -0
  56. data/lib/generators/ruby_cms/templates/components/ruby_ui/button/button.rb +113 -0
  57. data/lib/generators/ruby_cms/templates/components/ruby_ui/calendar/calendar.rb +39 -0
  58. data/lib/generators/ruby_cms/templates/components/ruby_ui/calendar/calendar_body.rb +19 -0
  59. data/lib/generators/ruby_cms/templates/components/ruby_ui/calendar/calendar_days.rb +104 -0
  60. data/lib/generators/ruby_cms/templates/components/ruby_ui/calendar/calendar_header.rb +17 -0
  61. data/lib/generators/ruby_cms/templates/components/ruby_ui/calendar/calendar_next.rb +43 -0
  62. data/lib/generators/ruby_cms/templates/components/ruby_ui/calendar/calendar_prev.rb +43 -0
  63. data/lib/generators/ruby_cms/templates/components/ruby_ui/calendar/calendar_title.rb +27 -0
  64. data/lib/generators/ruby_cms/templates/components/ruby_ui/calendar/calendar_weekdays.rb +33 -0
  65. data/lib/generators/ruby_cms/templates/components/ruby_ui/card/card.rb +17 -0
  66. data/lib/generators/ruby_cms/templates/components/ruby_ui/card/card_content.rb +17 -0
  67. data/lib/generators/ruby_cms/templates/components/ruby_ui/card/card_description.rb +17 -0
  68. data/lib/generators/ruby_cms/templates/components/ruby_ui/card/card_footer.rb +17 -0
  69. data/lib/generators/ruby_cms/templates/components/ruby_ui/card/card_header.rb +17 -0
  70. data/lib/generators/ruby_cms/templates/components/ruby_ui/card/card_title.rb +17 -0
  71. data/lib/generators/ruby_cms/templates/components/ruby_ui/carousel/carousel.rb +44 -0
  72. data/lib/generators/ruby_cms/templates/components/ruby_ui/carousel/carousel_content.rb +23 -0
  73. data/lib/generators/ruby_cms/templates/components/ruby_ui/carousel/carousel_item.rb +23 -0
  74. data/lib/generators/ruby_cms/templates/components/ruby_ui/carousel/carousel_next.rb +48 -0
  75. data/lib/generators/ruby_cms/templates/components/ruby_ui/carousel/carousel_previous.rb +49 -0
  76. data/lib/generators/ruby_cms/templates/components/ruby_ui/checkbox/checkbox.rb +29 -0
  77. data/lib/generators/ruby_cms/templates/components/ruby_ui/checkbox/checkbox_group.rb +20 -0
  78. data/lib/generators/ruby_cms/templates/components/ruby_ui/clipboard/clipboard.rb +42 -0
  79. data/lib/generators/ruby_cms/templates/components/ruby_ui/clipboard/clipboard_popover.rb +40 -0
  80. data/lib/generators/ruby_cms/templates/components/ruby_ui/clipboard/clipboard_source.rb +19 -0
  81. data/lib/generators/ruby_cms/templates/components/ruby_ui/clipboard/clipboard_trigger.rb +20 -0
  82. data/lib/generators/ruby_cms/templates/components/ruby_ui/codeblock/codeblock.rb +102 -0
  83. data/lib/generators/ruby_cms/templates/components/ruby_ui/collapsible/collapsible.rb +25 -0
  84. data/lib/generators/ruby_cms/templates/components/ruby_ui/collapsible/collapsible_content.rb +18 -0
  85. data/lib/generators/ruby_cms/templates/components/ruby_ui/collapsible/collapsible_trigger.rb +19 -0
  86. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox.rb +26 -0
  87. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_checkbox.rb +25 -0
  88. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_empty_state.rb +21 -0
  89. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_item.rb +25 -0
  90. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_list.rb +18 -0
  91. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_list_group.rb +20 -0
  92. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_popover.rb +30 -0
  93. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_radio.rb +26 -0
  94. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_search_input.rb +53 -0
  95. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +25 -0
  96. data/lib/generators/ruby_cms/templates/components/ruby_ui/combobox/combobox_trigger.rb +57 -0
  97. data/lib/generators/ruby_cms/templates/components/ruby_ui/command/command.rb +9 -0
  98. data/lib/generators/ruby_cms/templates/components/ruby_ui/command/command_dialog.rb +17 -0
  99. data/lib/generators/ruby_cms/templates/components/ruby_ui/command/command_dialog_content.rb +48 -0
  100. data/lib/generators/ruby_cms/templates/components/ruby_ui/command/command_dialog_trigger.rb +29 -0
  101. data/lib/generators/ruby_cms/templates/components/ruby_ui/command/command_empty.rb +19 -0
  102. data/lib/generators/ruby_cms/templates/components/ruby_ui/command/command_group.rb +40 -0
  103. data/lib/generators/ruby_cms/templates/components/ruby_ui/command/command_input.rb +56 -0
  104. data/lib/generators/ruby_cms/templates/components/ruby_ui/command/command_item.rb +32 -0
  105. data/lib/generators/ruby_cms/templates/components/ruby_ui/command/command_list.rb +17 -0
  106. data/lib/generators/ruby_cms/templates/components/ruby_ui/container/container.rb +33 -0
  107. data/lib/generators/ruby_cms/templates/components/ruby_ui/container/container_section.rb +31 -0
  108. data/lib/generators/ruby_cms/templates/components/ruby_ui/context_menu/context_menu.rb +26 -0
  109. data/lib/generators/ruby_cms/templates/components/ruby_ui/context_menu/context_menu_content.rb +25 -0
  110. data/lib/generators/ruby_cms/templates/components/ruby_ui/context_menu/context_menu_item.rb +66 -0
  111. data/lib/generators/ruby_cms/templates/components/ruby_ui/context_menu/context_menu_label.rb +24 -0
  112. data/lib/generators/ruby_cms/templates/components/ruby_ui/context_menu/context_menu_separator.rb +19 -0
  113. data/lib/generators/ruby_cms/templates/components/ruby_ui/context_menu/context_menu_trigger.rb +20 -0
  114. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table.rb +29 -0
  115. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_bulk_actions.rb +18 -0
  116. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_column_toggle.rb +62 -0
  117. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_expand_toggle.rb +53 -0
  118. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_form.rb +39 -0
  119. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_kaminari_adapter.rb +17 -0
  120. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_manual_adapter.rb +17 -0
  121. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_pagination.rb +100 -0
  122. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_pagination_bar.rb +15 -0
  123. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_pagy_adapter.rb +17 -0
  124. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_per_page_select.rb +35 -0
  125. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_row_checkbox.rb +30 -0
  126. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_search.rb +57 -0
  127. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_select_all_checkbox.rb +21 -0
  128. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_selection_summary.rb +25 -0
  129. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_sort_head.rb +112 -0
  130. data/lib/generators/ruby_cms/templates/components/ruby_ui/data_table/data_table_toolbar.rb +15 -0
  131. data/lib/generators/ruby_cms/templates/components/ruby_ui/dialog/dialog.rb +25 -0
  132. data/lib/generators/ruby_cms/templates/components/ruby_ui/dialog/dialog_content.rb +78 -0
  133. data/lib/generators/ruby_cms/templates/components/ruby_ui/dialog/dialog_description.rb +17 -0
  134. data/lib/generators/ruby_cms/templates/components/ruby_ui/dialog/dialog_footer.rb +17 -0
  135. data/lib/generators/ruby_cms/templates/components/ruby_ui/dialog/dialog_header.rb +17 -0
  136. data/lib/generators/ruby_cms/templates/components/ruby_ui/dialog/dialog_middle.rb +17 -0
  137. data/lib/generators/ruby_cms/templates/components/ruby_ui/dialog/dialog_title.rb +17 -0
  138. data/lib/generators/ruby_cms/templates/components/ruby_ui/dialog/dialog_trigger.rb +20 -0
  139. data/lib/generators/ruby_cms/templates/components/ruby_ui/dropdown_menu/dropdown_menu.rb +35 -0
  140. data/lib/generators/ruby_cms/templates/components/ruby_ui/dropdown_menu/dropdown_menu_content.rb +44 -0
  141. data/lib/generators/ruby_cms/templates/components/ruby_ui/dropdown_menu/dropdown_menu_item.rb +28 -0
  142. data/lib/generators/ruby_cms/templates/components/ruby_ui/dropdown_menu/dropdown_menu_label.rb +17 -0
  143. data/lib/generators/ruby_cms/templates/components/ruby_ui/dropdown_menu/dropdown_menu_separator.rb +19 -0
  144. data/lib/generators/ruby_cms/templates/components/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb +18 -0
  145. data/lib/generators/ruby_cms/templates/components/ruby_ui/empty_state/empty_state.rb +190 -0
  146. data/lib/generators/ruby_cms/templates/components/ruby_ui/form/form.rb +15 -0
  147. data/lib/generators/ruby_cms/templates/components/ruby_ui/form/form_field.rb +20 -0
  148. data/lib/generators/ruby_cms/templates/components/ruby_ui/form/form_field_error.rb +20 -0
  149. data/lib/generators/ruby_cms/templates/components/ruby_ui/form/form_field_hint.rb +15 -0
  150. data/lib/generators/ruby_cms/templates/components/ruby_ui/form/form_field_label.rb +15 -0
  151. data/lib/generators/ruby_cms/templates/components/ruby_ui/hover_card/hover_card.rb +27 -0
  152. data/lib/generators/ruby_cms/templates/components/ruby_ui/hover_card/hover_card_content.rb +22 -0
  153. data/lib/generators/ruby_cms/templates/components/ruby_ui/hover_card/hover_card_trigger.rb +20 -0
  154. data/lib/generators/ruby_cms/templates/components/ruby_ui/input/input.rb +34 -0
  155. data/lib/generators/ruby_cms/templates/components/ruby_ui/link/link.rb +106 -0
  156. data/lib/generators/ruby_cms/templates/components/ruby_ui/masked_input/masked_input.rb +15 -0
  157. data/lib/generators/ruby_cms/templates/components/ruby_ui/modal/modal.rb +26 -0
  158. data/lib/generators/ruby_cms/templates/components/ruby_ui/modal/modal_action.rb +17 -0
  159. data/lib/generators/ruby_cms/templates/components/ruby_ui/modal/modal_cancel.rb +20 -0
  160. data/lib/generators/ruby_cms/templates/components/ruby_ui/modal/modal_content.rb +68 -0
  161. data/lib/generators/ruby_cms/templates/components/ruby_ui/modal/modal_description.rb +17 -0
  162. data/lib/generators/ruby_cms/templates/components/ruby_ui/modal/modal_footer.rb +17 -0
  163. data/lib/generators/ruby_cms/templates/components/ruby_ui/modal/modal_header.rb +17 -0
  164. data/lib/generators/ruby_cms/templates/components/ruby_ui/modal/modal_title.rb +17 -0
  165. data/lib/generators/ruby_cms/templates/components/ruby_ui/native_select/native_select.rb +39 -0
  166. data/lib/generators/ruby_cms/templates/components/ruby_ui/native_select/native_select_group.rb +15 -0
  167. data/lib/generators/ruby_cms/templates/components/ruby_ui/native_select/native_select_icon.rb +39 -0
  168. data/lib/generators/ruby_cms/templates/components/ruby_ui/native_select/native_select_option.rb +15 -0
  169. data/lib/generators/ruby_cms/templates/components/ruby_ui/pagination/pagination.rb +19 -0
  170. data/lib/generators/ruby_cms/templates/components/ruby_ui/pagination/pagination_content.rb +17 -0
  171. data/lib/generators/ruby_cms/templates/components/ruby_ui/pagination/pagination_ellipsis.rb +42 -0
  172. data/lib/generators/ruby_cms/templates/components/ruby_ui/pagination/pagination_item.rb +28 -0
  173. data/lib/generators/ruby_cms/templates/components/ruby_ui/popover/popover.rb +26 -0
  174. data/lib/generators/ruby_cms/templates/components/ruby_ui/popover/popover_content.rb +27 -0
  175. data/lib/generators/ruby_cms/templates/components/ruby_ui/popover/popover_trigger.rb +20 -0
  176. data/lib/generators/ruby_cms/templates/components/ruby_ui/progress/progress.rb +37 -0
  177. data/lib/generators/ruby_cms/templates/components/ruby_ui/radio_button/radio_button.rb +25 -0
  178. data/lib/generators/ruby_cms/templates/components/ruby_ui/select/select.rb +23 -0
  179. data/lib/generators/ruby_cms/templates/components/ruby_ui/select/select_content.rb +32 -0
  180. data/lib/generators/ruby_cms/templates/components/ruby_ui/select/select_group.rb +15 -0
  181. data/lib/generators/ruby_cms/templates/components/ruby_ui/select/select_input.rb +22 -0
  182. data/lib/generators/ruby_cms/templates/components/ruby_ui/select/select_item.rb +52 -0
  183. data/lib/generators/ruby_cms/templates/components/ruby_ui/select/select_label.rb +17 -0
  184. data/lib/generators/ruby_cms/templates/components/ruby_ui/select/select_trigger.rb +54 -0
  185. data/lib/generators/ruby_cms/templates/components/ruby_ui/select/select_value.rb +27 -0
  186. data/lib/generators/ruby_cms/templates/components/ruby_ui/separator/separator.rb +38 -0
  187. data/lib/generators/ruby_cms/templates/components/ruby_ui/sheet/sheet.rb +17 -0
  188. data/lib/generators/ruby_cms/templates/components/ruby_ui/sheet/sheet_content.rb +77 -0
  189. data/lib/generators/ruby_cms/templates/components/ruby_ui/sheet/sheet_description.rb +17 -0
  190. data/lib/generators/ruby_cms/templates/components/ruby_ui/sheet/sheet_footer.rb +17 -0
  191. data/lib/generators/ruby_cms/templates/components/ruby_ui/sheet/sheet_header.rb +17 -0
  192. data/lib/generators/ruby_cms/templates/components/ruby_ui/sheet/sheet_middle.rb +17 -0
  193. data/lib/generators/ruby_cms/templates/components/ruby_ui/sheet/sheet_title.rb +17 -0
  194. data/lib/generators/ruby_cms/templates/components/ruby_ui/sheet/sheet_trigger.rb +17 -0
  195. data/lib/generators/ruby_cms/templates/components/ruby_ui/shortcut_key/shortcut_key.rb +17 -0
  196. data/lib/generators/ruby_cms/templates/components/ruby_ui/skeleton/skeleton.rb +17 -0
  197. data/lib/generators/ruby_cms/templates/components/ruby_ui/switch/switch.rb +24 -0
  198. data/lib/generators/ruby_cms/templates/components/ruby_ui/table/table.rb +19 -0
  199. data/lib/generators/ruby_cms/templates/components/ruby_ui/table/table_body.rb +17 -0
  200. data/lib/generators/ruby_cms/templates/components/ruby_ui/table/table_caption.rb +17 -0
  201. data/lib/generators/ruby_cms/templates/components/ruby_ui/table/table_cell.rb +17 -0
  202. data/lib/generators/ruby_cms/templates/components/ruby_ui/table/table_footer.rb +17 -0
  203. data/lib/generators/ruby_cms/templates/components/ruby_ui/table/table_head.rb +17 -0
  204. data/lib/generators/ruby_cms/templates/components/ruby_ui/table/table_header.rb +17 -0
  205. data/lib/generators/ruby_cms/templates/components/ruby_ui/table/table_row.rb +17 -0
  206. data/lib/generators/ruby_cms/templates/components/ruby_ui/tabs/tabs.rb +25 -0
  207. data/lib/generators/ruby_cms/templates/components/ruby_ui/tabs/tabs_content.rb +26 -0
  208. data/lib/generators/ruby_cms/templates/components/ruby_ui/tabs/tabs_list.rb +17 -0
  209. data/lib/generators/ruby_cms/templates/components/ruby_ui/tabs/tabs_trigger.rb +28 -0
  210. data/lib/generators/ruby_cms/templates/components/ruby_ui/textarea/textarea.rb +26 -0
  211. data/lib/generators/ruby_cms/templates/components/ruby_ui/theme_toggle/set_dark_mode.rb +16 -0
  212. data/lib/generators/ruby_cms/templates/components/ruby_ui/theme_toggle/set_light_mode.rb +16 -0
  213. data/lib/generators/ruby_cms/templates/components/ruby_ui/theme_toggle/theme_toggle.rb +9 -0
  214. data/lib/generators/ruby_cms/templates/components/ruby_ui/tooltip/tooltip.rb +26 -0
  215. data/lib/generators/ruby_cms/templates/components/ruby_ui/tooltip/tooltip_content.rb +26 -0
  216. data/lib/generators/ruby_cms/templates/components/ruby_ui/tooltip/tooltip_trigger.rb +19 -0
  217. data/lib/generators/ruby_cms/templates/components/ruby_ui/typography/heading.rb +60 -0
  218. data/lib/generators/ruby_cms/templates/components/ruby_ui/typography/inline_code.rb +17 -0
  219. data/lib/generators/ruby_cms/templates/components/ruby_ui/typography/inline_link.rb +22 -0
  220. data/lib/generators/ruby_cms/templates/components/ruby_ui/typography/text.rb +53 -0
  221. data/lib/generators/ruby_cms/templates/components/ruby_ui/typography/typography_blockquote.rb +17 -0
  222. data/lib/generators/ruby_cms/templates/{ruby_cms.rb → config/initializers/admin.rb} +15 -9
  223. data/lib/generators/ruby_cms/templates/config/initializers/admin_dashboard.rb +43 -0
  224. data/lib/generators/ruby_cms/templates/config/initializers/pagy.rb +7 -0
  225. data/lib/generators/ruby_cms/templates/config/initializers/ruby_cms_core.rb +25 -0
  226. data/lib/generators/ruby_cms/templates/config/initializers/ruby_cms_custom_settings.rb +35 -0
  227. data/lib/generators/ruby_cms/templates/config/initializers/webauthn.rb +28 -0
  228. data/lib/generators/ruby_cms/templates/config/locales/en.yml +277 -0
  229. data/lib/generators/ruby_cms/templates/config/locales/nl.yml +329 -0
  230. data/lib/generators/ruby_cms/templates/controllers/admin/analytics_controller.rb +211 -0
  231. data/lib/generators/ruby_cms/templates/controllers/admin/application_controller.rb +186 -0
  232. data/lib/generators/ruby_cms/templates/controllers/admin/audit_log_entries_controller.rb +69 -0
  233. data/lib/generators/ruby_cms/templates/controllers/admin/commands_controller.rb +146 -0
  234. data/lib/generators/ruby_cms/templates/controllers/admin/content_block_versions_controller.rb +96 -0
  235. data/lib/generators/ruby_cms/templates/controllers/admin/content_blocks_controller.rb +405 -0
  236. data/lib/generators/ruby_cms/templates/controllers/admin/dashboard_controller.rb +114 -0
  237. data/lib/generators/ruby_cms/templates/controllers/admin/invitations_controller.rb +64 -0
  238. data/lib/generators/ruby_cms/templates/controllers/admin/locale_controller.rb +19 -0
  239. data/lib/generators/ruby_cms/templates/controllers/admin/media_assets_controller.rb +186 -0
  240. data/lib/generators/ruby_cms/templates/controllers/admin/notifications_controller.rb +37 -0
  241. data/lib/generators/ruby_cms/templates/controllers/admin/passkey_credentials_controller.rb +17 -0
  242. data/lib/generators/ruby_cms/templates/controllers/admin/passkey_registrations_controller.rb +50 -0
  243. data/lib/generators/ruby_cms/templates/controllers/admin/permissions_controller.rb +132 -0
  244. data/lib/generators/ruby_cms/templates/controllers/admin/redirects_controller.rb +175 -0
  245. data/lib/generators/ruby_cms/templates/controllers/admin/security_controller.rb +146 -0
  246. data/lib/generators/ruby_cms/templates/controllers/admin/settings_controller.rb +298 -0
  247. data/lib/generators/ruby_cms/templates/controllers/admin/system_health_controller.rb +11 -0
  248. data/lib/generators/ruby_cms/templates/controllers/admin/trash_controller.rb +61 -0
  249. data/lib/generators/ruby_cms/templates/controllers/admin/user_permissions_controller.rb +91 -0
  250. data/lib/generators/ruby_cms/templates/controllers/admin/users_controller.rb +220 -0
  251. data/lib/generators/ruby_cms/templates/controllers/admin/visitor_errors_controller.rb +114 -0
  252. data/lib/generators/ruby_cms/templates/controllers/admin/visual_editor_controller.rb +384 -0
  253. data/lib/generators/ruby_cms/templates/controllers/concerns/.keep +0 -0
  254. data/lib/generators/ruby_cms/templates/controllers/concerns/admin_bulk_actions.rb +59 -0
  255. data/lib/generators/ruby_cms/templates/controllers/concerns/admin_pagination.rb +106 -0
  256. data/lib/generators/ruby_cms/templates/controllers/concerns/admin_turbo_table.rb +46 -0
  257. data/lib/generators/ruby_cms/templates/controllers/concerns/audit_loggable.rb +28 -0
  258. data/lib/generators/ruby_cms/templates/controllers/concerns/authentication.rb +63 -0
  259. data/lib/generators/ruby_cms/templates/controllers/concerns/page_tracking.rb +45 -0
  260. data/lib/generators/ruby_cms/templates/controllers/concerns/security_monitoring.rb +102 -0
  261. data/lib/generators/ruby_cms/templates/controllers/concerns/sudo_mode.rb +29 -0
  262. data/lib/generators/ruby_cms/templates/controllers/concerns/visitor_error_capture.rb +35 -0
  263. data/lib/generators/ruby_cms/templates/controllers/errors_controller.rb +36 -0
  264. data/lib/generators/ruby_cms/templates/controllers/passkey_sessions_controller.rb +69 -0
  265. data/lib/generators/ruby_cms/templates/db/migrate/20251121085414_create_email_blocklists.rb +12 -0
  266. data/lib/generators/ruby_cms/templates/db/migrate/20251121161828_create_ip_blocklists.rb +12 -0
  267. data/lib/generators/ruby_cms/templates/db/migrate/20251205120000_add_analytics_indices.rb +48 -0
  268. data/lib/generators/ruby_cms/templates/db/migrate/20251216064753_backfill_security_fields_in_ahoy_events.rb +49 -0
  269. data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260125000001_create_ruby_cms_permissions.rb +2 -2
  270. data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260125000002_create_ruby_cms_user_permissions.rb +2 -2
  271. data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260125000003_create_ruby_cms_content_blocks.rb +3 -3
  272. data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +1 -1
  273. data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +8 -6
  274. data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +4 -4
  275. data/lib/generators/ruby_cms/templates/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +10 -0
  276. data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260130000002_create_ruby_cms_preferences.rb +2 -2
  277. data/lib/generators/ruby_cms/templates/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +10 -0
  278. data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260328000001_create_content_block_versions.rb +2 -2
  279. data/lib/generators/ruby_cms/templates/db/migrate/20260525120000_create_audit_log_entries.rb +24 -0
  280. data/lib/generators/ruby_cms/templates/db/migrate/20260525130000_create_redirects.rb +28 -0
  281. data/lib/generators/ruby_cms/templates/db/migrate/20260525140000_create_media_assets.rb +25 -0
  282. data/lib/generators/ruby_cms/templates/db/migrate/20260525150000_create_invitations.rb +22 -0
  283. data/lib/generators/ruby_cms/templates/db/migrate/20260525160000_create_command_runs.rb +22 -0
  284. data/lib/generators/ruby_cms/templates/db/migrate/20260525190000_add_state_to_visitor_errors.rb +20 -0
  285. data/lib/generators/ruby_cms/templates/db/migrate/20260527165017_add_discarded_at_to_visitor_errors.rb +10 -0
  286. data/lib/generators/ruby_cms/templates/db/migrate/20260527165728_add_discarded_at_to_cms_tables.rb +12 -0
  287. data/lib/generators/ruby_cms/templates/db/migrate/20260605151935_create_passkey_credentials.rb +15 -0
  288. data/lib/generators/ruby_cms/templates/db/migrate/20260605151936_add_webauthn_id_to_users.rb +22 -0
  289. data/lib/generators/ruby_cms/templates/db/migrate/20260606000001_add_authenticated_at_to_sessions.rb +14 -0
  290. data/lib/generators/ruby_cms/templates/helpers/admin/admin_page_helper.rb +9 -0
  291. data/lib/generators/ruby_cms/templates/helpers/admin/dashboard_helper.rb +16 -0
  292. data/lib/generators/ruby_cms/templates/helpers/admin/media_assets_helper.rb +38 -0
  293. data/lib/generators/ruby_cms/templates/helpers/admin/relative_time_helper.rb +37 -0
  294. data/lib/generators/ruby_cms/templates/helpers/admin/ui_helper.rb +48 -0
  295. data/lib/generators/ruby_cms/templates/helpers/cms_application_helpers.rb +70 -0
  296. data/lib/generators/ruby_cms/templates/helpers/content_blocks_helper.rb +382 -0
  297. data/lib/generators/ruby_cms/templates/helpers/settings_helper.rb +192 -0
  298. data/lib/generators/ruby_cms/templates/javascript/admin.js +6 -0
  299. data/{app/javascript/controllers/ruby_cms → lib/generators/ruby_cms/templates/javascript/controllers/admin}/admin_commands_controller.js +2 -2
  300. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/autosearch_controller.js +18 -0
  301. data/{app/javascript/controllers/ruby_cms → lib/generators/ruby_cms/templates/javascript/controllers/admin}/clickable_row_controller.js +11 -0
  302. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/clipboard_controller.js +34 -0
  303. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/command_palette_controller.js +189 -0
  304. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/commands_controller.js +300 -0
  305. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/content_block_history_controller.js +167 -0
  306. data/{app/javascript/controllers/ruby_cms → lib/generators/ruby_cms/templates/javascript/controllers/admin}/flash_messages_controller.js +1 -1
  307. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/inline_rich_editor_controller.js +157 -0
  308. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/media_selection_controller.js +76 -0
  309. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/media_uploader_controller.js +23 -0
  310. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/mobile_menu_controller.js +125 -0
  311. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/nav_order_sortable_controller.js +296 -0
  312. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/nav_shortcuts_controller.js +47 -0
  313. data/{app/javascript/controllers/ruby_cms → lib/generators/ruby_cms/templates/javascript/controllers/admin}/page_preview_controller.js +64 -3
  314. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/permission_matrix_controller.js +49 -0
  315. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/search_shortcut_controller.js +24 -0
  316. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/settings_rail_search_controller.js +17 -0
  317. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/shortcuts_overlay_controller.js +39 -0
  318. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/sidebar_collapse_controller.js +135 -0
  319. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/sidebar_nav_persist_controller.js +48 -0
  320. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/sidebar_toggle_trigger_controller.js +10 -0
  321. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/topbar_controller.js +88 -0
  322. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/visual_editor_controller.js +721 -0
  323. data/lib/generators/ruby_cms/templates/javascript/controllers/admin/visual_editor_page_select_controller.js +174 -0
  324. data/lib/generators/ruby_cms/templates/javascript/controllers/application.js +10 -0
  325. data/lib/generators/ruby_cms/templates/javascript/controllers/index.js +2 -0
  326. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/accordion_controller.js +97 -0
  327. data/{app/javascript/controllers/ruby_cms → lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui}/bulk_action_table_controller.js +86 -233
  328. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/calendar_controller.js +249 -0
  329. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/calendar_input_controller.js +8 -0
  330. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/carousel_controller.js +60 -0
  331. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/checkbox_group_controller.js +21 -0
  332. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/clipboard_controller.js +54 -0
  333. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/collapsible_controller.js +47 -0
  334. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/combobox_controller.js +190 -0
  335. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/command_controller.js +138 -0
  336. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/context_menu_controller.js +144 -0
  337. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/data_table_column_visibility_controller.js +14 -0
  338. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/data_table_controller.js +109 -0
  339. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/data_table_search_controller.js +62 -0
  340. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/dialog_controller.js +32 -0
  341. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/dropdown_menu_controller.js +149 -0
  342. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/form_field_controller.js +61 -0
  343. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/hover_card_controller.js +153 -0
  344. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/masked_input_controller.js +9 -0
  345. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/modal_controller.js +45 -0
  346. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/popover_controller.js +107 -0
  347. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/select_controller.js +192 -0
  348. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/select_item_controller.js +11 -0
  349. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/sheet_content_controller.js +7 -0
  350. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/sheet_controller.js +9 -0
  351. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/tabs_controller.js +67 -0
  352. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/theme_toggle_controller.js +30 -0
  353. data/lib/generators/ruby_cms/templates/javascript/controllers/ruby_ui/tooltip_controller.js +38 -0
  354. data/lib/generators/ruby_cms/templates/javascript/controllers/webauthn_controller.js +153 -0
  355. data/lib/generators/ruby_cms/templates/lib/ruby_cms/cli.rb +170 -0
  356. data/lib/generators/ruby_cms/templates/lib/ruby_cms/commands_registry.rb +76 -0
  357. data/lib/{ruby_cms → generators/ruby_cms/templates/lib/ruby_cms}/content_blocks_grouping.rb +2 -6
  358. data/lib/{ruby_cms → generators/ruby_cms/templates/lib/ruby_cms}/content_blocks_sync.rb +6 -8
  359. data/lib/{ruby_cms → generators/ruby_cms/templates/lib/ruby_cms}/dashboard_blocks.rb +5 -5
  360. data/lib/{ruby_cms → generators/ruby_cms/templates/lib/ruby_cms}/icons.rb +41 -10
  361. data/lib/{ruby_cms → generators/ruby_cms/templates/lib/ruby_cms}/settings.rb +60 -10
  362. data/lib/{ruby_cms → generators/ruby_cms/templates/lib/ruby_cms}/settings_registry.rb +79 -3
  363. data/lib/generators/ruby_cms/templates/lib/ruby_cms.rb +342 -0
  364. data/lib/{tasks → generators/ruby_cms/templates/lib/tasks}/admin.rake +16 -16
  365. data/lib/generators/ruby_cms/templates/lib/tasks/ruby_cms.rake +158 -0
  366. data/lib/generators/ruby_cms/templates/models/audit_log_entry.rb +56 -0
  367. data/lib/generators/ruby_cms/templates/models/command_run.rb +29 -0
  368. data/lib/generators/ruby_cms/templates/models/concerns/content_block/searchable.rb +18 -0
  369. data/{app → lib/generators/ruby_cms/templates}/models/concerns/content_block/versionable.rb +1 -1
  370. data/lib/generators/ruby_cms/templates/models/concerns/ransackable.rb +32 -0
  371. data/{app → lib/generators/ruby_cms/templates}/models/content_block.rb +13 -5
  372. data/lib/generators/ruby_cms/templates/models/email_blocklist.rb +21 -0
  373. data/lib/generators/ruby_cms/templates/models/invitation.rb +86 -0
  374. data/lib/generators/ruby_cms/templates/models/ip_blocklist.rb +34 -0
  375. data/lib/generators/ruby_cms/templates/models/media_asset.rb +90 -0
  376. data/lib/generators/ruby_cms/templates/models/passkey_credential.rb +6 -0
  377. data/lib/generators/ruby_cms/templates/models/permission.rb +71 -0
  378. data/lib/generators/ruby_cms/templates/models/permittable.rb +69 -0
  379. data/lib/generators/ruby_cms/templates/models/preference.rb +109 -0
  380. data/lib/generators/ruby_cms/templates/models/redirect.rb +100 -0
  381. data/lib/generators/ruby_cms/templates/models/user_permission.rb +25 -0
  382. data/lib/generators/ruby_cms/templates/models/visitor_error.rb +157 -0
  383. data/lib/generators/ruby_cms/templates/nav/analytics.rb +3 -0
  384. data/lib/generators/ruby_cms/templates/nav/audit_log.rb +3 -0
  385. data/lib/generators/ruby_cms/templates/nav/commands.rb +3 -0
  386. data/lib/generators/ruby_cms/templates/nav/content_blocks.rb +3 -0
  387. data/lib/generators/ruby_cms/templates/nav/core.rb +13 -0
  388. data/lib/generators/ruby_cms/templates/nav/media.rb +3 -0
  389. data/lib/generators/ruby_cms/templates/nav/permissions.rb +3 -0
  390. data/lib/generators/ruby_cms/templates/nav/redirects.rb +3 -0
  391. data/lib/generators/ruby_cms/templates/nav/security.rb +4 -0
  392. data/lib/generators/ruby_cms/templates/nav/system_health.rb +3 -0
  393. data/lib/generators/ruby_cms/templates/nav/trash.rb +3 -0
  394. data/lib/generators/ruby_cms/templates/nav/users.rb +3 -0
  395. data/lib/generators/ruby_cms/templates/nav/visual_editor.rb +3 -0
  396. data/lib/generators/ruby_cms/templates/routes/analytics.rb +5 -0
  397. data/lib/generators/ruby_cms/templates/routes/audit_log.rb +3 -0
  398. data/lib/generators/ruby_cms/templates/routes/auth.public.rb +4 -0
  399. data/lib/generators/ruby_cms/templates/routes/auth.rb +6 -0
  400. data/lib/generators/ruby_cms/templates/routes/commands.rb +4 -0
  401. data/lib/generators/ruby_cms/templates/routes/content_blocks.rb +12 -0
  402. data/lib/generators/ruby_cms/templates/routes/core.public.rb +5 -0
  403. data/lib/generators/ruby_cms/templates/routes/core.rb +16 -0
  404. data/lib/generators/ruby_cms/templates/routes/media.rb +9 -0
  405. data/lib/generators/ruby_cms/templates/routes/passkeys.public.rb +4 -0
  406. data/lib/generators/ruby_cms/templates/routes/passkeys.rb +5 -0
  407. data/lib/generators/ruby_cms/templates/routes/permissions.rb +5 -0
  408. data/lib/generators/ruby_cms/templates/routes/redirects.rb +10 -0
  409. data/lib/generators/ruby_cms/templates/routes/security.rb +20 -0
  410. data/lib/generators/ruby_cms/templates/routes/system_health.rb +3 -0
  411. data/lib/generators/ruby_cms/templates/routes/trash.rb +5 -0
  412. data/lib/generators/ruby_cms/templates/routes/users.rb +10 -0
  413. data/lib/generators/ruby_cms/templates/routes/visual_editor.rb +5 -0
  414. data/lib/generators/ruby_cms/templates/services/admin/health_check.rb +362 -0
  415. data/lib/generators/ruby_cms/templates/services/analytics/report.rb +503 -0
  416. data/lib/generators/ruby_cms/templates/services/analytics_service.rb +305 -0
  417. data/lib/generators/ruby_cms/templates/services/audit_log.rb +60 -0
  418. data/lib/generators/ruby_cms/templates/services/command_runner.rb +79 -0
  419. data/lib/generators/ruby_cms/templates/services/security_service.rb +203 -0
  420. data/lib/generators/ruby_cms/templates/services/security_tracker.rb +90 -0
  421. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/index.html.erb +61 -50
  422. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/page_details.html.erb +15 -10
  423. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_browser_device.html.erb +2 -2
  424. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_conversions.html.erb +3 -3
  425. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_daily_activity_chart.html.erb +18 -17
  426. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_exit_pages.html.erb +4 -4
  427. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_hourly_activity_chart.html.erb +18 -17
  428. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_landing_pages.html.erb +3 -3
  429. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_os_stats.html.erb +2 -2
  430. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_popular_pages.html.erb +4 -4
  431. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_recent_activity.html.erb +4 -4
  432. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_top_referrers.html.erb +3 -3
  433. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_top_visitors.html.erb +4 -4
  434. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_utm_sources.html.erb +2 -2
  435. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/visitor_details.html.erb +15 -10
  436. data/lib/generators/ruby_cms/templates/views/admin/audit_log_entries/_admin_table_content.html.erb +55 -0
  437. data/lib/generators/ruby_cms/templates/views/admin/audit_log_entries/_row.html.erb +84 -0
  438. data/lib/generators/ruby_cms/templates/views/admin/audit_log_entries/index.html.erb +33 -0
  439. data/lib/generators/ruby_cms/templates/views/admin/audit_log_entries/show.html.erb +89 -0
  440. data/lib/generators/ruby_cms/templates/views/admin/commands/index.html.erb +393 -0
  441. data/lib/generators/ruby_cms/templates/views/admin/content_block_versions/index.html.erb +63 -0
  442. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/content_block_versions/show.html.erb +2 -2
  443. data/lib/generators/ruby_cms/templates/views/admin/content_blocks/_audits.html.erb +49 -0
  444. data/lib/generators/ruby_cms/templates/views/admin/content_blocks/_form.html.erb +79 -0
  445. data/lib/generators/ruby_cms/templates/views/admin/content_blocks/_rich_field.html.erb +44 -0
  446. data/lib/generators/ruby_cms/templates/views/admin/content_blocks/_row.html.erb +60 -0
  447. data/lib/generators/ruby_cms/templates/views/admin/content_blocks/index.html.erb +84 -0
  448. data/lib/generators/ruby_cms/templates/views/admin/content_blocks/new.html.erb +18 -0
  449. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/content_blocks/show.html.erb +67 -61
  450. data/lib/generators/ruby_cms/templates/views/admin/dashboard/_gmail_token_status.html.erb +69 -0
  451. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/dashboard/blocks/_analytics_overview.html.erb +11 -11
  452. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/dashboard/blocks/_content_blocks_stats.html.erb +3 -3
  453. data/lib/generators/ruby_cms/templates/views/admin/dashboard/blocks/_notifications.html.erb +50 -0
  454. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/dashboard/blocks/_permissions_stats.html.erb +3 -3
  455. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/dashboard/blocks/_quick_actions.html.erb +15 -15
  456. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/dashboard/blocks/_recent_errors.html.erb +8 -8
  457. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/dashboard/blocks/_users_stats.html.erb +3 -3
  458. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/dashboard/blocks/_visitor_errors_stats.html.erb +3 -3
  459. data/lib/generators/ruby_cms/templates/views/admin/dashboard/index.html.erb +34 -0
  460. data/lib/generators/ruby_cms/templates/views/admin/media_assets/_admin_table_content.html.erb +107 -0
  461. data/lib/generators/ruby_cms/templates/views/admin/media_assets/_form.html.erb +33 -0
  462. data/lib/generators/ruby_cms/templates/views/admin/media_assets/_grid.html.erb +58 -0
  463. data/lib/generators/ruby_cms/templates/views/admin/media_assets/_list.html.erb +43 -0
  464. data/lib/generators/ruby_cms/templates/views/admin/media_assets/edit.html.erb +13 -0
  465. data/lib/generators/ruby_cms/templates/views/admin/media_assets/index.html.erb +36 -0
  466. data/lib/generators/ruby_cms/templates/views/admin/media_assets/show.html.erb +97 -0
  467. data/lib/generators/ruby_cms/templates/views/admin/passkey_credentials/index.html.erb +30 -0
  468. data/lib/generators/ruby_cms/templates/views/admin/passkey_registrations/new.html.erb +23 -0
  469. data/lib/generators/ruby_cms/templates/views/admin/permissions/_admin_table_content.html.erb +45 -0
  470. data/lib/generators/ruby_cms/templates/views/admin/permissions/_matrix.html.erb +75 -0
  471. data/lib/generators/ruby_cms/templates/views/admin/permissions/_row.html.erb +62 -0
  472. data/lib/generators/ruby_cms/templates/views/admin/permissions/index.html.erb +65 -0
  473. data/lib/generators/ruby_cms/templates/views/admin/redirects/_admin_table_content.html.erb +77 -0
  474. data/lib/generators/ruby_cms/templates/views/admin/redirects/_form.html.erb +97 -0
  475. data/lib/generators/ruby_cms/templates/views/admin/redirects/_row.html.erb +82 -0
  476. data/lib/generators/ruby_cms/templates/views/admin/redirects/edit.html.erb +21 -0
  477. data/lib/generators/ruby_cms/templates/views/admin/redirects/index.html.erb +40 -0
  478. data/lib/generators/ruby_cms/templates/views/admin/redirects/new.html.erb +12 -0
  479. data/lib/generators/ruby_cms/templates/views/admin/security/index.html.erb +88 -0
  480. data/lib/generators/ruby_cms/templates/views/admin/security/ip_blocklist_content.html.erb +3 -0
  481. data/lib/generators/ruby_cms/templates/views/admin/security/partials/_events_by_type.html.erb +24 -0
  482. data/lib/generators/ruby_cms/templates/views/admin/security/partials/_ip_blocklist.html.erb +37 -0
  483. data/lib/generators/ruby_cms/templates/views/admin/security/partials/_ip_blocklist_content.html.erb +41 -0
  484. data/lib/generators/ruby_cms/templates/views/admin/security/partials/_ip_management.html.erb +11 -0
  485. data/lib/generators/ruby_cms/templates/views/admin/security/partials/_recent_events.html.erb +59 -0
  486. data/lib/generators/ruby_cms/templates/views/admin/security/partials/_top_threat_ips.html.erb +21 -0
  487. data/lib/generators/ruby_cms/templates/views/admin/settings/index.html.erb +509 -0
  488. data/lib/generators/ruby_cms/templates/views/admin/shared/_add_button.html.erb +15 -0
  489. data/lib/generators/ruby_cms/templates/views/admin/shared/_data_table.html.erb +255 -0
  490. data/lib/generators/ruby_cms/templates/views/admin/shared/_turbo_pagination.html.erb +53 -0
  491. data/lib/generators/ruby_cms/templates/views/admin/system_health/index.html.erb +248 -0
  492. data/lib/generators/ruby_cms/templates/views/admin/trash/index.html.erb +58 -0
  493. data/lib/generators/ruby_cms/templates/views/admin/user_permissions/_row.html.erb +29 -0
  494. data/lib/generators/ruby_cms/templates/views/admin/user_permissions/index.html.erb +78 -0
  495. data/lib/generators/ruby_cms/templates/views/admin/users/_admin_table_content.html.erb +50 -0
  496. data/lib/generators/ruby_cms/templates/views/admin/users/_invite_form.html.erb +43 -0
  497. data/lib/generators/ruby_cms/templates/views/admin/users/_invites_panel.html.erb +91 -0
  498. data/lib/generators/ruby_cms/templates/views/admin/users/_row.html.erb +109 -0
  499. data/lib/generators/ruby_cms/templates/views/admin/users/index.html.erb +43 -0
  500. data/lib/generators/ruby_cms/templates/views/admin/users/show.html.erb +343 -0
  501. data/lib/generators/ruby_cms/templates/views/admin/visitor_errors/_admin_table_content.html.erb +61 -0
  502. data/lib/generators/ruby_cms/templates/views/admin/visitor_errors/_row.html.erb +90 -0
  503. data/lib/generators/ruby_cms/templates/views/admin/visitor_errors/index.html.erb +18 -0
  504. data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/visitor_errors/show.html.erb +4 -4
  505. data/lib/generators/ruby_cms/templates/views/admin/visual_editor/index.html.erb +383 -0
  506. data/{app/views/layouts/ruby_cms → lib/generators/ruby_cms/templates/views/layouts/admin}/_admin_flash_messages.html.erb +3 -3
  507. data/lib/generators/ruby_cms/templates/views/layouts/admin/_admin_sidebar.html.erb +239 -0
  508. data/lib/generators/ruby_cms/templates/views/layouts/admin/_command_palette.html.erb +65 -0
  509. data/lib/generators/ruby_cms/templates/views/layouts/admin/_shortcuts_overlay.html.erb +81 -0
  510. data/lib/generators/ruby_cms/templates/views/layouts/admin/_topbar_widgets.html.erb +9 -0
  511. data/lib/generators/ruby_cms/templates/views/layouts/admin/minimal.html.erb +71 -0
  512. data/lib/generators/ruby_cms/templates/views/layouts/admin/topbar/_bell.html.erb +81 -0
  513. data/lib/generators/ruby_cms/templates/views/layouts/admin/topbar/_profile_menu.html.erb +86 -0
  514. data/lib/generators/ruby_cms/templates/views/layouts/admin/topbar/_search_trigger.html.erb +9 -0
  515. data/lib/generators/ruby_cms/templates/views/layouts/admin/topbar/_shortcuts_button.html.erb +8 -0
  516. data/lib/generators/ruby_cms/templates/views/shared/_maintenance_banner.html.erb +10 -0
  517. data/lib/ruby_cms/app_wiring.rb +56 -0
  518. data/lib/ruby_cms/auth_wiring.rb +64 -0
  519. data/lib/ruby_cms/cli.rb +181 -121
  520. data/lib/ruby_cms/excludes.rb +41 -0
  521. data/lib/ruby_cms/file_installer.rb +41 -0
  522. data/lib/ruby_cms/gem_setup.rb +84 -0
  523. data/lib/ruby_cms/helper_wiring.rb +32 -0
  524. data/lib/ruby_cms/importmap_wiring.rb +41 -0
  525. data/lib/ruby_cms/installer.rb +128 -0
  526. data/lib/ruby_cms/lockfile.rb +49 -0
  527. data/lib/ruby_cms/manifest.rb +88 -0
  528. data/lib/ruby_cms/manifest_data.rb +233 -0
  529. data/lib/ruby_cms/migration_installer.rb +64 -0
  530. data/lib/ruby_cms/nav_assembler.rb +43 -0
  531. data/lib/ruby_cms/passkey_wiring.rb +42 -0
  532. data/lib/ruby_cms/path_map.rb +26 -0
  533. data/lib/ruby_cms/routes_assembler.rb +103 -0
  534. data/lib/ruby_cms/syncer.rb +81 -0
  535. data/lib/ruby_cms/updater.rb +76 -0
  536. data/lib/ruby_cms/version.rb +1 -1
  537. data/lib/ruby_cms.rb +4 -245
  538. metadata +606 -169
  539. data/.cursor/dhh.mdc +0 -698
  540. data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +0 -32
  541. data/app/components/ruby_cms/admin/admin_page.rb +0 -345
  542. data/app/components/ruby_cms/admin/admin_page_header.rb +0 -79
  543. data/app/components/ruby_cms/admin/admin_resource_card.rb +0 -55
  544. data/app/components/ruby_cms/admin/base_component.rb +0 -78
  545. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +0 -157
  546. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +0 -127
  547. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +0 -15
  548. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +0 -43
  549. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +0 -35
  550. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +0 -174
  551. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +0 -59
  552. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +0 -169
  553. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +0 -192
  554. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +0 -96
  555. data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +0 -196
  556. data/app/controllers/concerns/ruby_cms/admin_pagination.rb +0 -120
  557. data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +0 -68
  558. data/app/controllers/concerns/ruby_cms/page_tracking.rb +0 -67
  559. data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +0 -39
  560. data/app/controllers/ruby_cms/admin/analytics_controller.rb +0 -213
  561. data/app/controllers/ruby_cms/admin/base_controller.rb +0 -132
  562. data/app/controllers/ruby_cms/admin/commands_controller.rb +0 -122
  563. data/app/controllers/ruby_cms/admin/content_block_versions_controller.rb +0 -62
  564. data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +0 -391
  565. data/app/controllers/ruby_cms/admin/dashboard_controller.rb +0 -89
  566. data/app/controllers/ruby_cms/admin/locale_controller.rb +0 -21
  567. data/app/controllers/ruby_cms/admin/permissions_controller.rb +0 -66
  568. data/app/controllers/ruby_cms/admin/settings_controller.rb +0 -223
  569. data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +0 -77
  570. data/app/controllers/ruby_cms/admin/users_controller.rb +0 -107
  571. data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +0 -89
  572. data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +0 -345
  573. data/app/controllers/ruby_cms/errors_controller.rb +0 -35
  574. data/app/helpers/ruby_cms/admin/admin_page_helper.rb +0 -21
  575. data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +0 -159
  576. data/app/helpers/ruby_cms/admin/dashboard_helper.rb +0 -20
  577. data/app/helpers/ruby_cms/application_helper.rb +0 -41
  578. data/app/helpers/ruby_cms/bulk_action_table_helper.rb +0 -151
  579. data/app/helpers/ruby_cms/content_blocks_helper.rb +0 -399
  580. data/app/helpers/ruby_cms/settings_helper.rb +0 -172
  581. data/app/javascript/controllers/ruby_cms/content_block_history_controller.js +0 -91
  582. data/app/javascript/controllers/ruby_cms/index.js +0 -117
  583. data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +0 -55
  584. data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +0 -192
  585. data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +0 -325
  586. data/app/models/concerns/content_block/searchable.rb +0 -22
  587. data/app/models/ruby_cms/content_block.rb +0 -8
  588. data/app/models/ruby_cms/permission.rb +0 -57
  589. data/app/models/ruby_cms/permittable.rb +0 -37
  590. data/app/models/ruby_cms/preference.rb +0 -111
  591. data/app/models/ruby_cms/user_permission.rb +0 -12
  592. data/app/models/ruby_cms/visitor_error.rb +0 -109
  593. data/app/services/ruby_cms/analytics/report.rb +0 -512
  594. data/app/services/ruby_cms/command_runner.rb +0 -42
  595. data/app/services/ruby_cms/security_tracker.rb +0 -92
  596. data/app/views/admin/content_block_versions/index.html.erb +0 -52
  597. data/app/views/admin/content_block_versions/show.html.erb +0 -37
  598. data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +0 -122
  599. data/app/views/layouts/ruby_cms/admin.html.erb +0 -77
  600. data/app/views/layouts/ruby_cms/minimal.html.erb +0 -181
  601. data/app/views/ruby_cms/admin/commands/index.html.erb +0 -104
  602. data/app/views/ruby_cms/admin/content_block_versions/index.html.erb +0 -52
  603. data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +0 -92
  604. data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +0 -25
  605. data/app/views/ruby_cms/admin/content_blocks/index.html.erb +0 -62
  606. data/app/views/ruby_cms/admin/content_blocks/new.html.erb +0 -10
  607. data/app/views/ruby_cms/admin/dashboard/index.html.erb +0 -31
  608. data/app/views/ruby_cms/admin/permissions/_row.html.erb +0 -11
  609. data/app/views/ruby_cms/admin/permissions/index.html.erb +0 -62
  610. data/app/views/ruby_cms/admin/settings/index.html.erb +0 -268
  611. data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +0 -56
  612. data/app/views/ruby_cms/admin/user_permissions/index.html.erb +0 -85
  613. data/app/views/ruby_cms/admin/users/_row.html.erb +0 -17
  614. data/app/views/ruby_cms/admin/users/index.html.erb +0 -70
  615. data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +0 -35
  616. data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +0 -57
  617. data/app/views/ruby_cms/admin/visual_editor/index.html.erb +0 -157
  618. data/app/views/ruby_cms/errors/not_found.html.erb +0 -92
  619. data/config/database.yml +0 -6
  620. data/config/importmap.rb +0 -40
  621. data/config/locales/en.yml +0 -198
  622. data/config/locales/nl.yml +0 -156
  623. data/config/routes.rb +0 -76
  624. data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +0 -8
  625. data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +0 -8
  626. data/docs/assets/ruby_cms_logo.png +0 -0
  627. data/lib/generators/ruby_cms/install_generator.rb +0 -1080
  628. data/lib/ruby_cms/app_integration.rb +0 -82
  629. data/lib/ruby_cms/commands_registry.rb +0 -40
  630. data/lib/ruby_cms/css_compiler.rb +0 -35
  631. data/lib/ruby_cms/engine/admin_permissions.rb +0 -69
  632. data/lib/ruby_cms/engine/content_blocks_tasks.rb +0 -66
  633. data/lib/ruby_cms/engine/css.rb +0 -14
  634. data/lib/ruby_cms/engine/dashboard_registration.rb +0 -66
  635. data/lib/ruby_cms/engine/navigation_registration.rb +0 -89
  636. data/lib/ruby_cms/engine.rb +0 -273
  637. /data/{app → lib/generators/ruby_cms/templates}/assets/images/ruby_cms/logo.png +0 -0
  638. /data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +0 -0
  639. /data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260212000001_use_unprefixed_cms_tables.rb +0 -0
  640. /data/{db → lib/generators/ruby_cms/templates/db}/migrate/20260409000001_add_analytics_performance_indexes.rb +0 -0
  641. /data/{app/javascript/controllers/ruby_cms → lib/generators/ruby_cms/templates/javascript/controllers/admin}/auto_save_preference_controller.js +0 -0
  642. /data/{app/javascript/controllers/ruby_cms → lib/generators/ruby_cms/templates/javascript/controllers/admin}/locale_tabs_controller.js +0 -0
  643. /data/{app/javascript/controllers/ruby_cms → lib/generators/ruby_cms/templates/javascript/controllers/admin}/toggle_controller.js +0 -0
  644. /data/{app → lib/generators/ruby_cms/templates}/models/concerns/content_block/publishable.rb +0 -0
  645. /data/{app → lib/generators/ruby_cms/templates}/models/content_block_version.rb +0 -0
  646. /data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_back_button.html.erb +0 -0
  647. /data/{app/views/ruby_cms → lib/generators/ruby_cms/templates/views}/admin/analytics/partials/_security_alert.html.erb +0 -0
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tracks security events to Ahoy::Event for analytics and monitoring.
4
+ # Use from host app controllers: SecurityTracker.track(...)
5
+ #
6
+ # Example:
7
+ # SecurityTracker.track("failed_login", description: "Invalid password", request: request)
8
+ class SecurityTracker
9
+ EVENT_TYPES = %w[
10
+ failed_login
11
+ successful_login
12
+ logout
13
+ admin_access_denied
14
+ suspicious_user_agent
15
+ unusual_request_pattern
16
+ session_hijack_attempt
17
+ rate_limit_exceeded
18
+ csrf_token_mismatch
19
+ unauthorized_admin_attempt
20
+ contact_honeypot_triggered
21
+ contact_blocked_email_attempt
22
+ email_blocklist_error
23
+ ip_blocklist_error
24
+ ip_blocklist_blocked
25
+ ].freeze
26
+
27
+ def self.track(event_type, description:, user: nil, request: nil, ip_address: nil,
28
+ user_agent: nil, request_path: nil)
29
+ return nil unless EVENT_TYPES.include?(event_type)
30
+
31
+ attrs = {
32
+ name: event_type,
33
+ ip_address: ip_address || request&.remote_ip,
34
+ request_path: request_path || request&.path,
35
+ user_agent: user_agent || request&.user_agent,
36
+ description: description,
37
+ time: Time.current,
38
+ user: user
39
+ }
40
+
41
+ Ahoy::Event.create!(**attrs)
42
+ rescue StandardError => e
43
+ Rails.logger.error "Failed to track security event: #{e.message}"
44
+ nil
45
+ end
46
+
47
+ def self.risk_level(event_type)
48
+ case event_type
49
+ when "session_hijack_attempt", "unauthorized_admin_attempt" then "high"
50
+ when "failed_login", "csrf_token_mismatch" then "medium"
51
+ when "successful_login", "logout" then "info"
52
+ else "low"
53
+ end
54
+ end
55
+
56
+ def self.risk_color(event_type)
57
+ case risk_level(event_type)
58
+ when "high" then "red"
59
+ when "medium" then "yellow"
60
+ when "info" then "green"
61
+ else "blue"
62
+ end
63
+ end
64
+
65
+ def self.formatted_description(event)
66
+ event_type = event.name
67
+ ip = event.ip_address
68
+ user_agent = event.user_agent
69
+ path = event.request_path
70
+ description = event.try(:description) || event.properties&.dig("description")
71
+
72
+ case event_type
73
+ when "failed_login" then "Failed login attempt from #{ip}"
74
+ when "successful_login" then "Successful admin login from #{ip}"
75
+ when "logout" then "Admin logout from #{ip}"
76
+ when "admin_access_denied" then "Unauthorized admin access attempt from #{ip}"
77
+ when "unauthorized_admin_attempt" then "Non-admin user attempted admin login"
78
+ when "suspicious_user_agent" then "Suspicious user agent: #{user_agent&.truncate(50)}"
79
+ when "unusual_request_pattern" then "Unusual request pattern: #{path}"
80
+ when "session_hijack_attempt" then "Potential session hijacking from #{ip}"
81
+ when "rate_limit_exceeded" then "Rate limit exceeded from #{ip}"
82
+ when "csrf_token_mismatch" then "CSRF token mismatch on #{path}"
83
+ when "contact_honeypot_triggered" then "Contact honeypot triggered from #{ip}"
84
+ when "contact_blocked_email_attempt" then "Blocked contact email attempt: #{description}"
85
+ when "email_blocklist_error" then "Error adding email to blocklist: #{description}"
86
+ when "ip_blocklist_blocked" then "Blocked IP #{ip} attempting #{path}"
87
+ else description.presence || event_type.humanize
88
+ end
89
+ end
90
+ end
@@ -1,35 +1,45 @@
1
- <%= admin_page(
2
- title: t("ruby_cms.admin.analytics.title", default: "Analytics"),
3
- subtitle: t("ruby_cms.admin.analytics.subtitle", default: "Traffic and engagement overview"),
4
- content_card: false
5
- ) do %>
6
- <div class="space-y-6">
1
+ <%= AdminPage do %>
2
+ <%= AdminPageHeader(
3
+ title: t("ruby_cms.admin.analytics.title", default: "Analytics"),
4
+ subtitle: t("ruby_cms.admin.analytics.subtitle", default: "Traffic and engagement overview"),
5
+ breadcrumbs: [
6
+ { label: t("ruby_cms.admin.breadcrumb.admin", default: "Admin"), url: admin_root_path },
7
+ { label: t("ruby_cms.admin.analytics.title", default: "Analytics") }
8
+ ]
9
+ ) %>
7
10
 
8
- <%# ── Period selector + date range ── %>
9
- <div class="flex flex-wrap items-start justify-between gap-4">
10
- <div class="inline-flex items-center rounded-lg border border-border/60 bg-white p-1 shadow-sm">
11
- <% [["Day", "day"], ["Week", "week"], ["Month", "month"], ["Year", "year"]].each do |label, value| %>
12
- <% start_date = case value
13
- when "day" then Date.current
14
- when "week" then 6.days.ago.to_date
15
- when "month" then 29.days.ago.to_date
16
- else 364.days.ago.to_date
17
- end %>
18
- <% active = params[:period] == value || (params[:period].blank? && value == @period.to_s) %>
19
- <%= link_to label,
20
- ruby_cms_admin_analytics_path(start_date:, end_date: Date.current, period: value),
21
- class: "px-4 py-1.5 text-sm font-medium rounded-md transition-colors #{active ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}" %>
22
- <% end %>
11
+ <div class="min-w-0 space-y-6 p-4">
12
+
13
+ <%# ── Period selector + date range (same label + h-9 control row as date fields) ── %>
14
+ <div class="flex min-w-0 flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-end sm:justify-between">
15
+ <div class="min-w-0 flex flex-col gap-1">
16
+ <span class="text-xs font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.period_label", default: "Period") %></span>
17
+ <div class="overflow-x-auto pb-1 -mx-1 px-1 sm:mx-0 sm:px-0 sm:pb-0">
18
+ <div class="inline-flex h-9 min-w-max items-stretch gap-0.5 rounded-lg border border-border/60 bg-white p-0.5 shadow-sm">
19
+ <% [ [ "Day", "day" ], [ "Week", "week" ], [ "Month", "month" ], [ "Year", "year" ] ].each do |label, value| %>
20
+ <% start_date = case value
21
+ when "day" then Date.current
22
+ when "week" then 6.days.ago.to_date
23
+ when "month" then 29.days.ago.to_date
24
+ else 364.days.ago.to_date
25
+ end %>
26
+ <% active = params[:period] == value || (params[:period].blank? && value == @period.to_s) %>
27
+ <%= link_to label,
28
+ admin_analytics_path(start_date:, end_date: Date.current, period: value),
29
+ class: "inline-flex shrink-0 items-center justify-center px-3.5 text-sm font-medium rounded-md transition-colors #{active ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}" %>
30
+ <% end %>
31
+ </div>
32
+ </div>
23
33
  </div>
24
34
 
25
- <%= form_with url: ruby_cms_admin_analytics_path, method: :get, local: true, class: "flex items-end gap-2" do |f| %>
35
+ <%= form_with url: admin_analytics_path, method: :get, local: true, class: "flex min-w-0 flex-1 flex-wrap items-end gap-2" do |f| %>
26
36
  <%= f.hidden_field :period, value: @period %>
27
37
  <div class="flex flex-col gap-1">
28
- <span class="text-xs font-medium text-muted-foreground">From</span>
38
+ <span class="text-xs font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.date_from", default: "From") %></span>
29
39
  <%= f.date_field :start_date, value: @start_date, class: "h-9 rounded-md border border-border bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/20" %>
30
40
  </div>
31
41
  <div class="flex flex-col gap-1">
32
- <span class="text-xs font-medium text-muted-foreground">To</span>
42
+ <span class="text-xs font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.date_to", default: "To") %></span>
33
43
  <%= f.date_field :end_date, value: @end_date, class: "h-9 rounded-md border border-border bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/20" %>
34
44
  </div>
35
45
  <%= f.submit t("ruby_cms.admin.analytics.apply_filters", default: "Apply"),
@@ -37,13 +47,13 @@
37
47
  <% end %>
38
48
  </div>
39
49
 
40
- <%# ── Activity charts (3/4) + KPI summary card (1/4), same row height ── %>
41
- <div class="grid gap-4 items-stretch" style="grid-template-columns: 3fr 1fr;">
50
+ <%# ── Activity charts (3/4) + KPI summary card (1/4), same row height on large screens ── %>
51
+ <div class="grid min-w-0 grid-cols-1 items-stretch gap-4 lg:grid-cols-[minmax(0,3fr)_minmax(0,1fr)]">
42
52
 
43
53
  <%# Left: activity charts stacked %>
44
54
  <div class="flex min-h-0 min-w-0 flex-col gap-4">
45
- <%= render "ruby_cms/admin/analytics/partials/daily_activity_chart" %>
46
- <%= render "ruby_cms/admin/analytics/partials/hourly_activity_chart" %>
55
+ <%= render "admin/analytics/partials/daily_activity_chart" %>
56
+ <%= render "admin/analytics/partials/hourly_activity_chart" %>
47
57
  </div>
48
58
 
49
59
  <%# Right: KPI summary — stretches to match combined chart column height %>
@@ -80,13 +90,13 @@
80
90
  <div class="flex items-center justify-between mb-1">
81
91
  <span class="text-xs font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.total_page_views", default: "Page Views") %></span>
82
92
  <div class="rounded-md bg-blue-50 p-1 flex-shrink-0">
83
- <svg class="w-3.5 h-3.5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
93
+ <svg class="w-3.5 h-3.5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
84
94
  </div>
85
95
  </div>
86
96
  <div class="flex items-baseline gap-2">
87
97
  <p class="text-xl font-bold tracking-tight text-foreground tabular-nums"><%= number_with_delimiter(@total_page_views) %></p>
88
98
  <% if (delta = @period_deltas&.dig(:total_page_views)) %>
89
- <span class="inline-flex items-center text-xs font-semibold rounded-full px-1.5 py-0.5 <%= delta >= 0 ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700' %>">
99
+ <span class="inline-flex items-center text-xs font-semibold rounded-full px-1.5 py-0.5 <%= delta >= 0 ? "bg-emerald-50 text-emerald-700" : "bg-red-50 text-red-700" %>">
90
100
  <%= delta >= 0 ? "↑" : "↓" %> <%= delta.abs %>%
91
101
  </span>
92
102
  <% end %>
@@ -99,13 +109,13 @@
99
109
  <div class="flex items-center justify-between mb-1">
100
110
  <span class="text-xs font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.unique_visitors", default: "Unique Visitors") %></span>
101
111
  <div class="rounded-md bg-violet-50 p-1 flex-shrink-0">
102
- <svg class="w-3.5 h-3.5 text-violet-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
112
+ <svg class="w-3.5 h-3.5 text-violet-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
103
113
  </div>
104
114
  </div>
105
115
  <div class="flex items-baseline gap-2">
106
116
  <p class="text-xl font-bold tracking-tight text-foreground tabular-nums"><%= number_with_delimiter(@unique_visitors) %></p>
107
117
  <% if (delta = @period_deltas&.dig(:unique_visitors)) %>
108
- <span class="inline-flex items-center text-xs font-semibold rounded-full px-1.5 py-0.5 <%= delta >= 0 ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700' %>">
118
+ <span class="inline-flex items-center text-xs font-semibold rounded-full px-1.5 py-0.5 <%= delta >= 0 ? "bg-emerald-50 text-emerald-700" : "bg-red-50 text-red-700" %>">
109
119
  <%= delta >= 0 ? "↑" : "↓" %> <%= delta.abs %>%
110
120
  </span>
111
121
  <% elsif @new_visitor_percentage.to_i > 0 %>
@@ -122,13 +132,13 @@
122
132
  <div class="flex items-center justify-between mb-1">
123
133
  <span class="text-xs font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.total_sessions", default: "Sessions") %></span>
124
134
  <div class="rounded-md bg-emerald-50 p-1 flex-shrink-0">
125
- <svg class="w-3.5 h-3.5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
135
+ <svg class="w-3.5 h-3.5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>
126
136
  </div>
127
137
  </div>
128
138
  <div class="flex items-baseline gap-2">
129
139
  <p class="text-xl font-bold tracking-tight text-foreground tabular-nums"><%= number_with_delimiter(@total_sessions) %></p>
130
140
  <% if (delta = @period_deltas&.dig(:total_sessions)) %>
131
- <span class="inline-flex items-center text-xs font-semibold rounded-full px-1.5 py-0.5 <%= delta >= 0 ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700' %>">
141
+ <span class="inline-flex items-center text-xs font-semibold rounded-full px-1.5 py-0.5 <%= delta >= 0 ? "bg-emerald-50 text-emerald-700" : "bg-red-50 text-red-700" %>">
132
142
  <%= delta >= 0 ? "↑" : "↓" %> <%= delta.abs %>%
133
143
  </span>
134
144
  <% end %>
@@ -140,13 +150,13 @@
140
150
  <div class="px-4 py-3">
141
151
  <div class="flex items-center justify-between mb-1">
142
152
  <span class="text-xs font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.bounce_rate", default: "Bounce Rate") %></span>
143
- <div class="rounded-md p-1 flex-shrink-0 <%= @bounce_rate.to_f > 70 ? 'bg-red-50' : @bounce_rate.to_f > 50 ? 'bg-amber-50' : 'bg-emerald-50' %>">
144
- <svg class="w-3.5 h-3.5 <%= @bounce_rate.to_f > 70 ? 'text-red-600' : @bounce_rate.to_f > 50 ? 'text-amber-600' : 'text-emerald-600' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
153
+ <div class="rounded-md p-1 flex-shrink-0 <%= @bounce_rate.to_f > 70 ? "bg-red-50" : @bounce_rate.to_f > 50 ? "bg-amber-50" : "bg-emerald-50" %>">
154
+ <svg class="w-3.5 h-3.5 <%= @bounce_rate.to_f > 70 ? "text-red-600" : @bounce_rate.to_f > 50 ? "text-amber-600" : "text-emerald-600" %>" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
145
155
  </div>
146
156
  </div>
147
157
  <div class="flex items-baseline gap-2">
148
158
  <p class="text-xl font-bold tracking-tight text-foreground tabular-nums"><%= @bounce_rate %>%</p>
149
- <span class="inline-flex items-center text-xs font-semibold rounded-full px-1.5 py-0.5 <%= @bounce_rate.to_f > 70 ? 'bg-red-50 text-red-700' : @bounce_rate.to_f > 50 ? 'bg-amber-50 text-amber-700' : 'bg-emerald-50 text-emerald-700' %>">
159
+ <span class="inline-flex items-center text-xs font-semibold rounded-full px-1.5 py-0.5 <%= @bounce_rate.to_f > 70 ? "bg-red-50 text-red-700" : @bounce_rate.to_f > 50 ? "bg-amber-50 text-amber-700" : "bg-emerald-50 text-emerald-700" %>">
150
160
  <%= @bounce_rate.to_f > 70 ? "High" : @bounce_rate.to_f > 50 ? "Average" : "Good" %>
151
161
  </span>
152
162
  </div>
@@ -166,9 +176,9 @@
166
176
 
167
177
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
168
178
  <div class="lg:col-span-2">
169
- <%= render "ruby_cms/admin/analytics/partials/popular_pages" %>
179
+ <%= render "admin/analytics/partials/popular_pages" %>
170
180
  </div>
171
- <%= render "ruby_cms/admin/analytics/partials/top_visitors" %>
181
+ <%= render "admin/analytics/partials/top_visitors" %>
172
182
  </div>
173
183
 
174
184
  <%# ── Section: Traffic Sources ── %>
@@ -178,9 +188,9 @@
178
188
  </div>
179
189
 
180
190
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
181
- <%= render "ruby_cms/admin/analytics/partials/top_referrers" %>
182
- <%= render "ruby_cms/admin/analytics/partials/landing_pages" %>
183
- <%= render "ruby_cms/admin/analytics/partials/utm_sources" %>
191
+ <%= render "admin/analytics/partials/top_referrers" %>
192
+ <%= render "admin/analytics/partials/landing_pages" %>
193
+ <%= render "admin/analytics/partials/utm_sources" %>
184
194
  </div>
185
195
 
186
196
  <%# ── Section: Audience & Technology ── %>
@@ -190,9 +200,9 @@
190
200
  </div>
191
201
 
192
202
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
193
- <%= render "ruby_cms/admin/analytics/partials/browser_device" %>
194
- <%= render "ruby_cms/admin/analytics/partials/os_stats" %>
195
- <%= render "ruby_cms/admin/analytics/partials/recent_activity" %>
203
+ <%= render "admin/analytics/partials/browser_device" %>
204
+ <%= render "admin/analytics/partials/os_stats" %>
205
+ <%= render "admin/analytics/partials/recent_activity" %>
196
206
  </div>
197
207
 
198
208
  <%# ── Section: Behavior ── %>
@@ -202,8 +212,8 @@
202
212
  </div>
203
213
 
204
214
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
205
- <% if lookup_context.exists?("ruby_cms/admin/analytics/partials/exit_pages", [], true) %>
206
- <%= render "ruby_cms/admin/analytics/partials/exit_pages" %>
215
+ <% if lookup_context.exists?("admin/analytics/partials/exit_pages", [], true) %>
216
+ <%= render "admin/analytics/partials/exit_pages" %>
207
217
  <% else %>
208
218
  <div class="rounded-xl border border-border/60 bg-white p-4 shadow-sm">
209
219
  <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.exit_pages", default: "Exit Pages") %></p>
@@ -211,8 +221,8 @@
211
221
  </div>
212
222
  <% end %>
213
223
 
214
- <% if lookup_context.exists?("ruby_cms/admin/analytics/partials/conversions", [], true) %>
215
- <%= render "ruby_cms/admin/analytics/partials/conversions" %>
224
+ <% if lookup_context.exists?("admin/analytics/partials/conversions", [], true) %>
225
+ <%= render "admin/analytics/partials/conversions" %>
216
226
  <% else %>
217
227
  <div class="rounded-xl border border-border/60 bg-white p-4 shadow-sm">
218
228
  <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.conversions", default: "Conversions") %></p>
@@ -226,7 +236,7 @@
226
236
  <div class="rounded-xl border border-amber-200 bg-amber-50/60 shadow-sm overflow-hidden">
227
237
  <div class="px-4 py-3 border-b border-amber-200/60 flex items-center gap-3">
228
238
  <div class="rounded-md bg-amber-100 p-1.5 flex-shrink-0">
229
- <svg class="w-4 h-4 text-amber-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
239
+ <svg class="w-4 h-4 text-amber-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
230
240
  </div>
231
241
  <div>
232
242
  <p class="text-sm font-semibold text-amber-900"><%= t("ruby_cms.admin.analytics.suspicious_activity", default: "Suspicious activity") %></p>
@@ -260,4 +270,5 @@
260
270
  <% end %>
261
271
 
262
272
  </div>
273
+ </div>
263
274
  <% end %>
@@ -3,18 +3,23 @@
3
3
  suspicious_ua = @page_views.any? { |e| e.visit&.user_agent.to_s.include?("bot") || e.visit&.user_agent.to_s.include?("crawler") }
4
4
  show_security = high_volume || suspicious_ua
5
5
  %>
6
- <%= admin_page(
6
+ <%= AdminPageHeader(
7
7
  title: @page_name.to_s.humanize,
8
8
  subtitle: "Page analytics · #{@start_date} → #{@end_date}",
9
- content_card: false
9
+ breadcrumbs: [
10
+ { label: t("ruby_cms.admin.breadcrumb.admin", default: "Admin"), url: admin_root_path },
11
+ { label: t("ruby_cms.admin.analytics.title", default: "Analytics"), url: admin_analytics_path },
12
+ { label: @page_name.to_s.humanize }
13
+ ]
10
14
  ) do %>
11
- <div class="space-y-6">
12
- <div class="flex items-start justify-between gap-4">
13
- <div class="flex items-center gap-2">
14
- <%= render "ruby_cms/admin/analytics/partials/security_alert" if show_security %>
15
- </div>
16
- <%= render "ruby_cms/admin/analytics/partials/back_button", path: ruby_cms_admin_analytics_path(start_date: @start_date, end_date: @end_date, period: @period) %>
15
+ <%= render "admin/analytics/partials/back_button", path: admin_analytics_path(start_date: @start_date, end_date: @end_date, period: @period) %>
16
+ <% end %>
17
+ <div class="space-y-6 mt-4">
18
+ <div class="flex items-start justify-between gap-4">
19
+ <div class="flex items-center gap-2">
20
+ <%= render "admin/analytics/partials/security_alert" if show_security %>
17
21
  </div>
22
+ </div>
18
23
 
19
24
  <div class="grid grid-cols-3 gap-4">
20
25
  <div class="rounded-lg border border-border/60 bg-white p-6 shadow-sm">
@@ -56,7 +61,7 @@
56
61
  <td class="px-6 py-3 font-medium text-foreground tabular-nums">
57
62
  <% if ip.present? && ip != "Unknown" %>
58
63
  <%= link_to ip,
59
- visitor_details_ruby_cms_admin_analytics_path(ip_address: ip, start_date: @start_date, end_date: @end_date, period: @period),
64
+ admin_analytics_visitor_details_path(ip_address: ip, start_date: @start_date, end_date: @end_date, period: @period),
60
65
  class: "text-primary hover:underline" %>
61
66
  <% else %>
62
67
  <span class="text-muted-foreground"><%= ip || "Unknown" %></span>
@@ -77,4 +82,4 @@
77
82
  </div>
78
83
  </div>
79
84
  </div>
80
- <% end %>
85
+ </div>
@@ -1,7 +1,7 @@
1
1
  <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
2
  <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
3
  <div class="rounded-md bg-slate-100 p-1.5 flex-shrink-0">
4
- <svg class="w-4 h-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
4
+ <svg class="w-4 h-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
5
5
  </div>
6
6
  <div>
7
7
  <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.browsers_devices", default: "Browsers & Devices") %></p>
@@ -52,7 +52,7 @@
52
52
 
53
53
  <% if (@browser_stats.blank? || @browser_stats.empty?) && (@device_stats.blank? || @device_stats.empty?) %>
54
54
  <div class="flex flex-col items-center justify-center py-6 gap-2">
55
- <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
55
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
56
56
  <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_data", default: "No data yet.") %></p>
57
57
  </div>
58
58
  <% end %>
@@ -2,7 +2,7 @@
2
2
  <div class="flex items-center justify-between gap-3 px-4 py-3 border-b border-border/40">
3
3
  <div class="flex items-center gap-3">
4
4
  <div class="rounded-md bg-emerald-50 p-1.5 flex-shrink-0">
5
- <svg class="w-4 h-4 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
5
+ <svg class="w-4 h-4 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
6
6
  </div>
7
7
  <div>
8
8
  <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.conversions", default: "Conversions") %></p>
@@ -37,11 +37,11 @@
37
37
  </div>
38
38
  <% else %>
39
39
  <div class="flex flex-col items-center justify-center px-4 py-10 gap-3">
40
- <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
40
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
41
41
  <div class="text-center">
42
42
  <p class="text-sm font-medium text-foreground"><%= t("ruby_cms.admin.analytics.no_conversions", default: "No conversions recorded.") %></p>
43
43
  <p class="mt-2 text-xs font-mono bg-muted/60 border border-border/40 rounded-md px-3 py-1.5 inline-block text-muted-foreground">
44
- ahoy.track "<%= RubyCms::Analytics::Report::EVENT_CONVERSION %>", goal: "contact_form"
44
+ ahoy.track "<%= Analytics::Report::EVENT_CONVERSION %>", goal: "contact_form"
45
45
  </p>
46
46
  </div>
47
47
  </div>
@@ -4,14 +4,14 @@
4
4
  has_data = daily_arr.present? && daily_arr.any?
5
5
  max_views = daily_arr.map { |_k, v| v.to_i }.max.to_i
6
6
  max_visitors = (@daily_visitors || {}).values.map(&:to_i).max.to_i
7
- max_value = [max_views, max_visitors, 1].max
7
+ max_value = [ max_views, max_visitors, 1 ].max
8
8
  # Plot height + x-axis band (month view labels like "03/12–03/14" need ~32–36px; 20px was too tight and clipped)
9
9
  bar_area_h = 160
10
10
  x_axis_h = 40
11
11
  chart_h = bar_area_h + x_axis_h
12
12
  total_views = daily_arr.sum { |_k, v| v.to_i }
13
13
  total_visitors = (@daily_visitors || {}).values.sum(&:to_i)
14
- n_days = [daily_arr.size.to_f, 1.0].max
14
+ n_days = [ daily_arr.size.to_f, 1.0 ].max
15
15
  line_pts_views = daily_arr.each_with_index.map { |(d, vc), i|
16
16
  vx = ((i + 0.5) / n_days) * 100.0
17
17
  vy = 100.0 - (max_value.positive? ? (vc.to_f / max_value * 100.0) : 0.0)
@@ -24,7 +24,7 @@
24
24
  "#{vx.round(4)},#{vy.round(4)}"
25
25
  }.join(" ")
26
26
  # Month view: compact day-number ticks + scroll; ~14px per slot is enough when labels are "3".."31"
27
- plot_scroll_min_w = [daily_arr.size * 14 + 56, 280].max
27
+ plot_scroll_min_w = [ daily_arr.size * 14 + 56, 280 ].max
28
28
  %>
29
29
 
30
30
  <div class="min-w-0 rounded-xl border border-border/60 bg-white shadow-sm">
@@ -34,7 +34,7 @@
34
34
  <div class="flex items-center gap-3">
35
35
  <div class="rounded-md bg-sky-50 p-1.5 flex-shrink-0">
36
36
  <svg class="w-4 h-4 text-sky-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
37
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/>
37
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
38
38
  </svg>
39
39
  </div>
40
40
  <div>
@@ -64,7 +64,7 @@
64
64
  <div class="rcms-chart-mode-toggle inline-flex rounded-lg border border-border/60 bg-muted/30 p-0.5 text-[11px] font-medium"
65
65
  role="group"
66
66
  data-rcms-chart="<%= chart_id %>"
67
- aria-label="<%= t('ruby_cms.admin.analytics.chart_display', default: 'Chart display') %>">
67
+ aria-label="<%= t("ruby_cms.admin.analytics.chart_display", default: "Chart display") %>">
68
68
  <button type="button"
69
69
  class="rcms-mode-btn px-2 py-1 rounded-md transition-colors text-muted-foreground hover:text-foreground"
70
70
  data-mode="bars"
@@ -99,7 +99,7 @@
99
99
 
100
100
  <%# Y-axis %>
101
101
  <div class="flex flex-col justify-between text-right text-xs text-muted-foreground tabular-nums" style="height:<%= chart_h %>px; padding-bottom:<%= x_axis_h %>px;">
102
- <% [100, 75, 50, 25, 0].each do |pct| %>
102
+ <% [ 100, 75, 50, 25, 0 ].each do |pct| %>
103
103
  <div class="leading-none"><%= number_with_delimiter((max_value * pct / 100.0).round) %></div>
104
104
  <% end %>
105
105
  </div>
@@ -117,8 +117,8 @@
117
117
  style="height:<%= bar_area_h %>px; top:0;"
118
118
  aria-hidden="true">
119
119
  <svg class="w-full h-full overflow-visible" viewBox="0 0 100 100" preserveAspectRatio="none">
120
- <polyline class="rcms-line-views" fill="none" stroke="rgb(37 99 235)" stroke-width="1.25" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round" points="<%= line_pts_views %>"/>
121
- <polyline class="rcms-line-visitors" fill="none" stroke="rgb(96 165 250)" stroke-width="1.25" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round" points="<%= line_pts_visitors %>"/>
120
+ <polyline class="rcms-line-views" fill="none" stroke="rgb(37 99 235)" stroke-width="1.25" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round" points="<%= line_pts_views %>" />
121
+ <polyline class="rcms-line-visitors" fill="none" stroke="rgb(96 165 250)" stroke-width="1.25" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round" points="<%= line_pts_visitors %>" />
122
122
  </svg>
123
123
  </div>
124
124
 
@@ -127,8 +127,8 @@
127
127
  <% daily_arr.each do |date, views_count| %>
128
128
  <%
129
129
  visitors_count = (@daily_visitors || {})[date].to_i
130
- vh = views_count.to_i.positive? ? [((views_count.to_f / max_value) * bar_area_h).round, 2].max : 0
131
- uh = visitors_count.positive? ? [((visitors_count.to_f / max_value) * bar_area_h).round, 2].max : 0
130
+ vh = views_count.to_i.positive? ? [ ((views_count.to_f / max_value) * bar_area_h).round, 2 ].max : 0
131
+ uh = visitors_count.positive? ? [ ((visitors_count.to_f / max_value) * bar_area_h).round, 2 ].max : 0
132
132
  label = format_chart_date(date)
133
133
  short = format_chart_date_axis(date)
134
134
  v_pct = total_views.positive? ? (100.0 * views_count.to_i / total_views) : 0.0
@@ -138,8 +138,8 @@
138
138
  data-label="<%= label %>"
139
139
  data-views="<%= number_with_delimiter(views_count.to_i) %>"
140
140
  data-visitors="<%= number_with_delimiter(visitors_count) %>"
141
- data-views-pct="<%= sprintf('%.1f', v_pct) %>"
142
- data-visitors-pct="<%= sprintf('%.1f', u_pct) %>"
141
+ data-views-pct="<%= sprintf("%.1f", v_pct) %>"
142
+ data-visitors-pct="<%= sprintf("%.1f", u_pct) %>"
143
143
  data-max-scale="<%= number_with_delimiter(max_value) %>">
144
144
  <%# Bar pair %>
145
145
  <div class="w-full flex items-end justify-center gap-px rcms-bar-stack" style="height:<%= bar_area_h %>px;">
@@ -161,24 +161,25 @@
161
161
  </div>
162
162
  </div>
163
163
 
164
- <script>
164
+ <script nonce="<%= request.content_security_policy_nonce %>">
165
165
  (function() {
166
166
  var BLUE600 = "rgb(37 99 235)";
167
167
  var BLUE400 = "rgb(96 165 250)";
168
168
  var MODE_KEY = "ruby_cms.analytics.dailyChartMode";
169
+ var CHART_ID = <%= chart_id.to_json.html_safe %>;
169
170
 
170
171
  function init() {
171
- var chart = document.getElementById("<%= chart_id %>");
172
+ var chart = document.getElementById(CHART_ID);
172
173
  if (!chart || chart.__rcmsInit) return;
173
174
  chart.__rcmsInit = true;
174
175
 
175
- var tip = document.getElementById("<%= chart_id %>-tip");
176
+ var tip = document.getElementById(CHART_ID + "-tip");
176
177
  var groups = chart.querySelectorAll(".rcms-bar-group");
177
178
  var allBars = chart.querySelectorAll(".rcms-bar");
178
179
  var lineLayer = chart.querySelector(".rcms-line-layer");
179
180
  var barsLayer = chart.querySelector(".rcms-bars-layer");
180
181
  var linePolys = lineLayer ? lineLayer.querySelectorAll("polyline") : [];
181
- var toggles = document.querySelectorAll('.rcms-chart-mode-toggle[data-rcms-chart="<%= chart_id %>"] .rcms-mode-btn');
182
+ var toggles = document.querySelectorAll('.rcms-chart-mode-toggle[data-rcms-chart="' + CHART_ID + '"] .rcms-mode-btn');
182
183
 
183
184
  function setMode(mode) {
184
185
  var isLine = mode === "line";
@@ -324,7 +325,7 @@
324
325
  <% else %>
325
326
  <div class="flex flex-col items-center justify-center h-44 gap-2">
326
327
  <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
327
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/>
328
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
328
329
  </svg>
329
330
  <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_daily_data", default: "No daily activity data") %></p>
330
331
  </div>
@@ -1,7 +1,7 @@
1
1
  <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
2
  <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
3
  <div class="rounded-md bg-amber-50 p-1.5 flex-shrink-0">
4
- <svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
4
+ <svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
5
5
  </div>
6
6
  <div>
7
7
  <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.exit_pages", default: "Exit Pages") %></p>
@@ -14,7 +14,7 @@
14
14
  <div class="divide-y divide-border/30">
15
15
  <% @exit_pages.first(8).each_with_index do |(page_name, count), idx| %>
16
16
  <% pct = total.positive? ? (count / total * 100).round(1) : 0 %>
17
- <%= link_to page_details_ruby_cms_admin_analytics_path(page_name:, start_date: @start_date, end_date: @end_date, period: @period),
17
+ <%= link_to admin_analytics_page_details_path(page_name:, start_date: @start_date, end_date: @end_date, period: @period),
18
18
  class: "group relative flex items-center justify-between gap-3 px-4 py-2.5 hover:bg-muted/40 transition-colors" do %>
19
19
  <div class="absolute inset-y-0 left-0 bg-amber-500/5 transition-all" style="width: <%= pct %>%"></div>
20
20
  <div class="relative z-10 flex items-center gap-3 min-w-0">
@@ -24,14 +24,14 @@
24
24
  <div class="relative z-10 flex items-center gap-2.5 flex-shrink-0">
25
25
  <span class="text-xs text-muted-foreground tabular-nums"><%= pct %>%</span>
26
26
  <span class="text-sm font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
27
- <svg class="w-3.5 h-3.5 text-muted-foreground/40 group-hover:text-primary/60 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
27
+ <svg class="w-3.5 h-3.5 text-muted-foreground/40 group-hover:text-primary/60 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
28
28
  </div>
29
29
  <% end %>
30
30
  <% end %>
31
31
  </div>
32
32
  <% else %>
33
33
  <div class="flex flex-col items-center justify-center px-4 py-10 gap-2">
34
- <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
34
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
35
35
  <p class="text-sm font-medium text-foreground"><%= t("ruby_cms.admin.analytics.no_exit_pages", default: "No exit page data available.") %></p>
36
36
  <p class="text-xs text-muted-foreground">Track some traffic to see which pages visitors leave from</p>
37
37
  </div>