ruby_cms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/dhh.mdc +698 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +10 -0
  6. data/README.md +235 -0
  7. data/Rakefile +30 -0
  8. data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
  9. data/app/components/ruby_cms/admin/admin_page.rb +345 -0
  10. data/app/components/ruby_cms/admin/base_component.rb +78 -0
  11. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
  12. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
  13. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
  14. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
  15. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
  16. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
  17. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
  18. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
  19. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
  20. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
  21. data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
  22. data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
  23. data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
  24. data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
  25. data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
  26. data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
  27. data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
  28. data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
  29. data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
  30. data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
  31. data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
  32. data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
  33. data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
  34. data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
  35. data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
  36. data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
  37. data/app/controllers/ruby_cms/errors_controller.rb +35 -0
  38. data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
  39. data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
  40. data/app/helpers/ruby_cms/application_helper.rb +41 -0
  41. data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
  42. data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
  43. data/app/helpers/ruby_cms/settings_helper.rb +160 -0
  44. data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
  45. data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
  46. data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
  47. data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
  48. data/app/javascript/controllers/ruby_cms/index.js +104 -0
  49. data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
  50. data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
  51. data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
  52. data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
  53. data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
  54. data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
  55. data/app/models/concerns/content_block/publishable.rb +54 -0
  56. data/app/models/concerns/content_block/searchable.rb +22 -0
  57. data/app/models/content_block.rb +155 -0
  58. data/app/models/ruby_cms/content_block.rb +8 -0
  59. data/app/models/ruby_cms/permission.rb +28 -0
  60. data/app/models/ruby_cms/permittable.rb +39 -0
  61. data/app/models/ruby_cms/preference.rb +111 -0
  62. data/app/models/ruby_cms/user_permission.rb +12 -0
  63. data/app/models/ruby_cms/visitor_error.rb +109 -0
  64. data/app/services/ruby_cms/analytics/report.rb +362 -0
  65. data/app/services/ruby_cms/security_tracker.rb +92 -0
  66. data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
  67. data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
  68. data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
  69. data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
  70. data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
  71. data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
  72. data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
  73. data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
  74. data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
  75. data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
  76. data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
  77. data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
  78. data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
  79. data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
  80. data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
  81. data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
  82. data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
  83. data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
  84. data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
  85. data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
  86. data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
  87. data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
  88. data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
  89. data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
  90. data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
  91. data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
  92. data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
  93. data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
  94. data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
  95. data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
  96. data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
  97. data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
  98. data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
  99. data/config/database.yml +6 -0
  100. data/config/importmap.rb +36 -0
  101. data/config/locales/en.yml +101 -0
  102. data/config/routes.rb +65 -0
  103. data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
  104. data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
  105. data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
  106. data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
  107. data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
  108. data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
  109. data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
  110. data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
  111. data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
  112. data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
  113. data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
  114. data/exe/ruby_cms +25 -0
  115. data/lib/generators/ruby_cms/install_generator.rb +1062 -0
  116. data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
  117. data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
  118. data/lib/ruby_cms/app_integration.rb +82 -0
  119. data/lib/ruby_cms/cli.rb +169 -0
  120. data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
  121. data/lib/ruby_cms/content_blocks_sync.rb +329 -0
  122. data/lib/ruby_cms/css_compiler.rb +35 -0
  123. data/lib/ruby_cms/engine.rb +498 -0
  124. data/lib/ruby_cms/settings.rb +145 -0
  125. data/lib/ruby_cms/settings_registry.rb +289 -0
  126. data/lib/ruby_cms/version.rb +5 -0
  127. data/lib/ruby_cms.rb +195 -0
  128. data/lib/tasks/ruby_cms.rake +27 -0
  129. data/log/test.log +17875 -0
  130. data/sig/ruby_cms.rbs +4 -0
  131. metadata +223 -0
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ # Base component class for admin components
6
+ # Inherits from RubyUI::Base if available, otherwise Phlex::HTML
7
+ class BaseComponent < (defined?(RubyUI) && RubyUI.const_defined?(:Base) ? RubyUI::Base : Phlex::HTML)
8
+ # Include Rails helpers if Phlex::HTML is used directly
9
+ if defined?(Phlex::Rails::Helpers)
10
+ include Phlex::Rails::Helpers::FormAuthenticityToken if defined?(Phlex::Rails::Helpers::FormAuthenticityToken)
11
+ include Phlex::Rails::Helpers::TurboFrameTag if defined?(Phlex::Rails::Helpers::TurboFrameTag)
12
+ include Phlex::Rails::Helpers::Sanitize if defined?(Phlex::Rails::Helpers::Sanitize)
13
+ include Phlex::Rails::Helpers::LinkTo if defined?(Phlex::Rails::Helpers::LinkTo)
14
+ include Phlex::Rails::Helpers::ButtonTo if defined?(Phlex::Rails::Helpers::ButtonTo)
15
+ include Phlex::Rails::Helpers::FormWith if defined?(Phlex::Rails::Helpers::FormWith)
16
+ end
17
+
18
+ # Helper method to build CSS classes from hash or array
19
+ def build_classes(*classes)
20
+ classes.flatten.compact.join(" ")
21
+ end
22
+
23
+ # Helper method to merge data attributes
24
+ def merge_data_attributes(base_data, additional_data)
25
+ base_data.merge(additional_data || {})
26
+ end
27
+
28
+ # Helper method to conditionally add attributes
29
+ def conditional_attributes(condition, attributes)
30
+ condition ? attributes : {}
31
+ end
32
+
33
+ # Access Rails helpers (works in Phlex and RubyUI contexts)
34
+ def helpers
35
+ @helpers ||=
36
+ if respond_to?(:view_context)
37
+ view_context
38
+ elsif defined?(Phlex::Rails::ViewContext)
39
+ Phlex::Rails::ViewContext.current
40
+ elsif defined?(Phlex::HTML) && respond_to?(:call)
41
+ Thread.current[:phlex_view_context] || raise("View context not available")
42
+ else
43
+ raise("View context not available. Ensure component is rendered from a Rails view.")
44
+ end
45
+ end
46
+
47
+ # Get form authenticity token
48
+ def form_authenticity_token
49
+ return helpers.form_authenticity_token if token_from_helpers?
50
+ return super if respond_to?(:form_authenticity_token, true)
51
+ return token_from_controller if token_from_controller?
52
+
53
+ ""
54
+ end
55
+
56
+ def token_from_helpers?
57
+ respond_to?(:helpers) && helpers.respond_to?(:form_authenticity_token)
58
+ end
59
+
60
+ def token_from_controller?
61
+ defined?(ActionController::Base) && respond_to?(:controller)
62
+ end
63
+
64
+ def token_from_controller
65
+ controller&.form_authenticity_token || ""
66
+ end
67
+
68
+ # Access controller if available
69
+ def controller
70
+ @controller ||= if respond_to?(:helpers) && helpers.respond_to?(:controller)
71
+ helpers.controller
72
+ elsif defined?(ActionController::Base)
73
+ Thread.current[:phlex_controller]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ module BulkActionTable
6
+ # Root component for bulk action table
7
+ # Wraps entire table with Turbo Frame support, pagination, and bulk actions bar
8
+ #
9
+ # @param turbo_frame [String, nil] Turbo Frame ID (e.g., "admin_table_content")
10
+ # @param pagination [Hash, nil] Pagination hash with current_page, total_pages, etc.
11
+ # @param pagination_path [Proc, nil] Lambda that generates pagination URLs
12
+ # @param bulk_actions_url [String, nil] URL for bulk delete action
13
+ # @param bulk_actions_buttons [Array<Hash>] Array of custom bulk action button configs
14
+ # @param item_name [String] Singular name for items (e.g., "error", "schedule")
15
+ # @param controller_name [String] Stimulus controller identifier
16
+ # (default: "ruby-cms--bulk-action-table")
17
+ class BulkActionTable < BaseComponent
18
+ def initialize(**options)
19
+ super()
20
+ @turbo_frame = options[:turbo_frame]
21
+ @pagination = options[:pagination]
22
+ @pagination_path = options[:pagination_path]
23
+ @bulk_actions_url = options[:bulk_actions_url]
24
+ @bulk_actions_buttons = options[:bulk_actions_buttons] || []
25
+ @item_name = options.fetch(:item_name, "item")
26
+ @controller_name = options.fetch(:controller_name, "ruby-cms--bulk-action-table")
27
+ @csrf_token = options[:csrf_token]
28
+ @header = options[:header]
29
+ @header_title = options[:header_title]
30
+ @header_filter = options[:header_filter]
31
+ @header_action_icons = options[:header_action_icons] || []
32
+ @header_search_url = options[:header_search_url] || "#"
33
+ @header_search_param = options[:header_search_param] || "q"
34
+ @user_attrs = extract_user_attrs(options)
35
+ @has_bulk_actions = @bulk_actions_url.present? || @bulk_actions_buttons.any?
36
+ end
37
+
38
+ def extract_user_attrs(options)
39
+ excluded_keys = %i[
40
+ turbo_frame pagination pagination_path bulk_actions_url
41
+ bulk_actions_buttons item_name controller_name csrf_token header
42
+ header_title header_filter header_action_icons header_search_url header_search_param
43
+ ]
44
+ options.except(*excluded_keys)
45
+ end
46
+
47
+ def view_template(&block)
48
+ if @turbo_frame
49
+ # Use turbo_frame_tag for proper Turbo Frame navigation (pagination, search)
50
+ turbo_frame_tag(@turbo_frame, **turbo_frame_options) do
51
+ render_table_content(&block)
52
+ end
53
+ else
54
+ render_table_content(&block)
55
+ end
56
+ end
57
+
58
+ def render_table_content(&block)
59
+ div(
60
+ class: build_classes(
61
+ "rounded-lg border border-gray-200/80 bg-white shadow-sm overflow-hidden " \
62
+ "flex flex-col",
63
+ @user_attrs[:class]
64
+ ),
65
+ **table_data_attributes.except(:class)
66
+ ) do
67
+ render_header
68
+ render_table_wrapper(&block)
69
+ div(class: "border-t border-gray-200/80 bg-white") do
70
+ render_bulk_actions if @has_bulk_actions
71
+ render_pagination if @pagination && @pagination_path
72
+ end
73
+ end
74
+ end
75
+
76
+ def render_header
77
+ if @header_title.present? || @header_action_icons.any? || @header_search_url.present?
78
+ render BulkActionTableHeaderBar.new(
79
+ title: @header_title,
80
+ header_filter: @header_filter,
81
+ action_icons: @header_action_icons,
82
+ search_url: @header_search_url,
83
+ search_param: @header_search_param,
84
+ turbo_frame: @turbo_frame
85
+ )
86
+ elsif @header
87
+ div(class: "px-6 py-4 border-b border-gray-200/80 bg-white") do
88
+ if @header.respond_to?(:call)
89
+ raw(@header.call) # rubocop:disable Rails/OutputSafety -- legacy capture support
90
+ elsif @header.kind_of?(String)
91
+ raw(sanitize(@header)) # rubocop:disable Rails/OutputSafety -- legacy capture support
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def render_table_wrapper(&)
98
+ div(class: "w-full overflow-x-auto") do
99
+ table(class: "min-w-full text-sm") do
100
+ yield if block_given?
101
+ end
102
+ end
103
+ end
104
+
105
+ def render_bulk_actions
106
+ render BulkActions.new(
107
+ controller_name: @controller_name,
108
+ item_name: @item_name,
109
+ bulk_actions_url: @bulk_actions_url,
110
+ bulk_action_buttons: @bulk_actions_buttons
111
+ )
112
+ render BulkActionTableDeleteModal.new(controller_name: @controller_name)
113
+ end
114
+
115
+ def render_pagination
116
+ render BulkActionTablePagination.new(
117
+ pagination: @pagination,
118
+ pagination_path: @pagination_path,
119
+ turbo_frame: @turbo_frame
120
+ )
121
+ end
122
+
123
+ private
124
+
125
+ def table_data_attributes
126
+ csrf_token = @csrf_token || form_authenticity_token
127
+ attrs = {
128
+ data: {
129
+ controller: @controller_name,
130
+ "#{@controller_name}-csrf-token-value": csrf_token,
131
+ "#{@controller_name}-item-name-value": @item_name
132
+ }
133
+ }
134
+
135
+ attrs[:data]["#{@controller_name}-bulk-action-url-value"] = @bulk_actions_url if @bulk_actions_url.present?
136
+
137
+ attrs.merge(@user_attrs)
138
+ end
139
+
140
+ def turbo_frame_options
141
+ {
142
+ class: "flex-1 flex flex-col min-h-0",
143
+ data: { turbo_action: "advance" }
144
+ }
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ module BulkActionTable
6
+ # Row action buttons component
7
+ # Renders Edit and Delete buttons for each table row
8
+ #
9
+ # @param edit_path [String, nil] URL to edit page
10
+ # @param delete_path [String, nil] URL to delete action
11
+ # @param delete_confirm [String] Confirmation message for delete
12
+ # @param require_confirm [Boolean] Whether to require confirmation (default: true)
13
+ # @param turbo_frame [String, nil] Turbo Frame ID for updates
14
+ # @param controller_name [String] Stimulus controller identifier
15
+ class BulkActionTableActions < BaseComponent
16
+ def initialize(
17
+ delete_path:,
18
+ item_id:,
19
+ edit_path: nil,
20
+ delete_confirm: "Are you sure you want to delete this item?",
21
+ require_confirm: true,
22
+ turbo_frame: nil,
23
+ controller_name: "ruby-cms--bulk-action-table"
24
+ )
25
+ super
26
+ @edit_path = edit_path
27
+ @delete_path = delete_path
28
+ @item_id = item_id
29
+ @delete_confirm = delete_confirm
30
+ @require_confirm = require_confirm
31
+ @turbo_frame = turbo_frame
32
+ @controller_name = controller_name
33
+ end
34
+
35
+ def view_template
36
+ div(class: "flex items-center justify-end gap-1") do
37
+ render_edit_button if @edit_path
38
+
39
+ render_delete_button if @delete_path
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def render_edit_button
46
+ link_options = {
47
+ href: @edit_path,
48
+ class: "inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 " \
49
+ "hover:bg-gray-100 hover:text-gray-900 transition-colors"
50
+ }
51
+ link_options[:data] = { turbo_frame: @turbo_frame } if @turbo_frame
52
+
53
+ a(**link_options) do
54
+ svg(
55
+ xmlns: "http://www.w3.org/2000/svg",
56
+ viewBox: "0 0 20 20",
57
+ fill: "currentColor",
58
+ class: "h-4 w-4"
59
+ ) do |s|
60
+ s.path(
61
+ d: edit_icon_path_d
62
+ )
63
+ s.path(
64
+ d: edit_icon_path_d2
65
+ )
66
+ end
67
+ end
68
+ end
69
+
70
+ def render_delete_button
71
+ item_id = @item_id || extract_item_id_from_path
72
+ button(
73
+ type: "button",
74
+ class: "inline-flex h-8 w-8 items-center justify-center rounded-md text-rose-600 " \
75
+ "hover:bg-rose-50 hover:text-rose-700 transition-colors",
76
+ data: {
77
+ action: "click->#{@controller_name}#showIndividualDeleteDialog",
78
+ "#{@controller_name}-item-id-param": item_id,
79
+ delete_path: @delete_path,
80
+ require_confirm: @require_confirm
81
+ }
82
+ ) do
83
+ svg(
84
+ xmlns: "http://www.w3.org/2000/svg",
85
+ viewBox: "0 0 20 20",
86
+ fill: "currentColor",
87
+ class: "h-4 w-4"
88
+ ) do |s|
89
+ s.path(
90
+ fill_rule: "evenodd",
91
+ d: delete_icon_path_d,
92
+ clip_rule: "evenodd"
93
+ )
94
+ end
95
+ end
96
+ end
97
+
98
+ def edit_icon_path_d
99
+ "M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3" \
100
+ "l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
101
+ end
102
+
103
+ def edit_icon_path_d2
104
+ "M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75" \
105
+ "v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0" \
106
+ "v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"
107
+ end
108
+
109
+ def delete_icon_path_d
110
+ "M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10" \
111
+ ".23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 00" \
112
+ "2.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193" \
113
+ "V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69" \
114
+ "-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 " \
115
+ "10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34" \
116
+ ".06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
117
+ end
118
+
119
+ def extract_item_id_from_path
120
+ # Try to extract item ID from delete_path (e.g., "/admin/users/123" -> "123")
121
+ # This is a fallback - ideally item_id should be passed explicitly
122
+ @delete_path.to_s.split("/").last
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ module BulkActionTable
6
+ # Table body component
7
+ # Simple wrapper for <tbody> content
8
+ class BulkActionTableBody < BaseComponent
9
+ def view_template(&)
10
+ tbody(class: "divide-y divide-gray-100", &)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ module BulkActionTable
6
+ # Checkbox cell component
7
+ # Renders checkbox in table row for bulk selection
8
+ #
9
+ # @param item_id [Integer, String] Item ID for checkbox value
10
+ # @param controller_name [String] Stimulus controller identifier
11
+ class BulkActionTableCheckboxCell < BaseComponent
12
+ def initialize(
13
+ item_id:,
14
+ controller_name: "ruby-cms--bulk-action-table"
15
+ )
16
+ super
17
+ @item_id = item_id
18
+ @controller_name = controller_name
19
+ end
20
+
21
+ def view_template
22
+ td(class: "w-12 px-6 py-3",
23
+ data: { action: "click->#{@controller_name}#stopPropagation" }) do
24
+ input(
25
+ type: "checkbox",
26
+ role: "checkbox",
27
+ value: @item_id,
28
+ class: "h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-200",
29
+ data: {
30
+ "#{@controller_name}-target": "itemCheckbox",
31
+ item_id: @item_id,
32
+ action: "change->#{@controller_name}#updateSelection"
33
+ },
34
+ aria_label: "Select row"
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ module BulkActionTable
6
+ # Checkbox header cell component
7
+ # Renders select-all checkbox in table header
8
+ #
9
+ # @param controller_name [String] Stimulus controller identifier
10
+ class BulkActionTableCheckboxHead < BaseComponent
11
+ def initialize(controller_name: "ruby-cms--bulk-action-table")
12
+ super()
13
+ @controller_name = controller_name
14
+ end
15
+
16
+ def view_template
17
+ th(class: "w-12 px-6 py-3") do
18
+ input(
19
+ type: "checkbox",
20
+ role: "checkbox",
21
+ class: "h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-200",
22
+ data: {
23
+ "#{@controller_name}-target": "selectAllCheckbox",
24
+ action: "change->#{@controller_name}#toggleSelectAll"
25
+ },
26
+ aria_label: "Select all"
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ module BulkActionTable
6
+ # Delete confirmation modal component
7
+ # Shows confirmation dialog for bulk delete actions
8
+ #
9
+ # @param controller_name [String] Stimulus controller identifier
10
+ class BulkActionTableDeleteModal < BaseComponent
11
+ def initialize(controller_name: "ruby-cms--bulk-action-table")
12
+ super
13
+ @controller_name = controller_name
14
+ end
15
+
16
+ def view_template
17
+ div(**dialog_overlay_attributes) do
18
+ render_backdrop
19
+ render_dialog_container
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def dialog_overlay_attributes
26
+ {
27
+ class: "fixed inset-0 z-50 hidden",
28
+ data: {
29
+ "#{@controller_name}-target": "dialogOverlay",
30
+ action: "keydown->#{@controller_name}#handleKeydown"
31
+ },
32
+ role: "dialog",
33
+ aria_modal: "true",
34
+ aria_labelledby: "dialog-title"
35
+ }
36
+ end
37
+
38
+ def render_backdrop
39
+ div(
40
+ class: "absolute inset-0 bg-black/50",
41
+ data: {
42
+ action: "click->#{@controller_name}#closeDialog"
43
+ }
44
+ )
45
+ end
46
+
47
+ def render_dialog_container
48
+ div(class: "relative flex min-h-full items-center justify-center p-4") do
49
+ div(**dialog_content_attributes) do
50
+ render_header_with_close
51
+ render_message
52
+ render_footer
53
+ end
54
+ end
55
+ end
56
+
57
+ def render_header_with_close
58
+ div(class: "flex items-start justify-between gap-4") do
59
+ render_header
60
+ render_close_button
61
+ end
62
+ end
63
+
64
+ def dialog_content_attributes
65
+ {
66
+ class: "w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-lg",
67
+ data: {
68
+ "#{@controller_name}-target": "dialogContent"
69
+ },
70
+ tabindex: "-1"
71
+ }
72
+ end
73
+
74
+ def render_close_button
75
+ button(
76
+ type: "button",
77
+ class: "inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 " \
78
+ "hover:bg-gray-100 hover:text-gray-900 transition-colors",
79
+ data: {
80
+ action: "click->#{@controller_name}#closeDialog"
81
+ },
82
+ aria_label: "Close"
83
+ ) do
84
+ render_close_icon
85
+ span(class: "sr-only") { "Close" }
86
+ end
87
+ end
88
+
89
+ def render_close_icon
90
+ svg(
91
+ width: "15",
92
+ height: "15",
93
+ viewBox: "0 0 15 15",
94
+ fill: "none",
95
+ xmlns: "http://www.w3.org/2000/svg",
96
+ class: "h-4 w-4"
97
+ ) do |s|
98
+ s.path(
99
+ d: close_icon_path_d,
100
+ fill: "currentColor",
101
+ fill_rule: "evenodd",
102
+ clip_rule: "evenodd"
103
+ )
104
+ end
105
+ end
106
+
107
+ def close_icon_path_d
108
+ "M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 " \
109
+ "2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 " \
110
+ "3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 " \
111
+ "3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 " \
112
+ "10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 " \
113
+ "12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 " \
114
+ "11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 " \
115
+ "11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 " \
116
+ "4.03157Z"
117
+ end
118
+
119
+ def render_header
120
+ h3(
121
+ id: "dialog-title",
122
+ class: "text-base font-semibold text-gray-900",
123
+ data: {
124
+ "#{@controller_name}-target": "dialogTitle"
125
+ }
126
+ ) { "Delete Selected Items" }
127
+ end
128
+
129
+ def render_message
130
+ div(
131
+ class: "mt-3 text-sm text-gray-600 space-y-1",
132
+ data: {
133
+ "#{@controller_name}-target": "dialogMessage"
134
+ }
135
+ ) do
136
+ p { "Are you sure you want to delete the selected items?" }
137
+ p { "This action cannot be undone." }
138
+ end
139
+ end
140
+
141
+ def render_footer
142
+ div(class: "mt-6 flex items-center justify-end gap-2") do
143
+ render_cancel_button
144
+ render_confirm_button
145
+ end
146
+ end
147
+
148
+ def render_cancel_button
149
+ button(
150
+ type: "button",
151
+ class: "inline-flex h-9 items-center justify-center rounded-md border " \
152
+ "border-gray-200 bg-white px-4 text-sm font-medium text-gray-900 " \
153
+ "shadow-sm hover:bg-gray-50 transition-colors",
154
+ data: {
155
+ action: "click->#{@controller_name}#closeDialog"
156
+ }
157
+ ) { "Cancel" }
158
+ end
159
+
160
+ def render_confirm_button
161
+ button(
162
+ type: "button",
163
+ class: "inline-flex h-9 items-center justify-center rounded-md bg-rose-600 px-4 " \
164
+ "text-sm font-medium text-white shadow-sm hover:bg-rose-700 transition-colors",
165
+ data: {
166
+ "#{@controller_name}-target": "dialogConfirmButton",
167
+ action: "click->#{@controller_name}#confirmAction"
168
+ }
169
+ ) { "Delete Selected" }
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ module BulkActionTable
6
+ # Table header component
7
+ # Renders <thead> with column headers and optional select-all checkbox
8
+ #
9
+ # @param headers [Array<String>] Array of header text
10
+ # @param bulk_actions_enabled [Boolean] Whether to show select-all checkbox
11
+ # @param controller_name [String] Stimulus controller identifier
12
+ class BulkActionTableHeader < BaseComponent
13
+ def initialize(
14
+ headers: [],
15
+ bulk_actions_enabled: true,
16
+ controller_name: "ruby-cms--bulk-action-table"
17
+ )
18
+ super
19
+ @headers = headers
20
+ @bulk_actions_enabled = bulk_actions_enabled
21
+ @controller_name = controller_name
22
+ end
23
+
24
+ def view_template
25
+ thead(class: "bg-gray-50") do
26
+ tr do
27
+ render_bulk_checkbox_header
28
+ render_table_headers
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def render_bulk_checkbox_header
36
+ return unless @bulk_actions_enabled
37
+
38
+ render BulkActionTableCheckboxHead.new(controller_name: @controller_name)
39
+ end
40
+
41
+ def render_table_headers
42
+ Array(@headers).each do |header|
43
+ if header.kind_of?(Hash)
44
+ th(class: build_classes(th_base_classes, header[:class])) do
45
+ header[:text] || header[:label]
46
+ end
47
+ else
48
+ th(class: th_base_classes) { header }
49
+ end
50
+ end
51
+ end
52
+
53
+ def th_base_classes
54
+ "px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end