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,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module SettingsRegistry
5
+ Entry = Struct.new(
6
+ :key,
7
+ :type,
8
+ :default,
9
+ :category,
10
+ :description
11
+ )
12
+
13
+ mattr_accessor :entries, default: {}
14
+
15
+ class << self
16
+ def register(key:, type:, default:, category: "general", description: nil)
17
+ k = key.to_s
18
+ entries[k] = Entry.new(
19
+ key: k,
20
+ type: normalize_type(type),
21
+ default: default,
22
+ category: category.to_s,
23
+ description: description.to_s
24
+ )
25
+ end
26
+
27
+ def fetch(key)
28
+ entries[key.to_s]
29
+ end
30
+
31
+ def each(&)
32
+ entries.each_value(&)
33
+ end
34
+
35
+ def defaults_hash
36
+ seed_defaults! if entries.empty?
37
+
38
+ entries.values.to_h do |entry|
39
+ [
40
+ entry.key.to_sym,
41
+ {
42
+ value: entry.default,
43
+ type: value_type_for(entry.type),
44
+ description: entry.description,
45
+ category: entry.category
46
+ }
47
+ ]
48
+ end
49
+ end
50
+
51
+ def seed_defaults!
52
+ return unless entries.empty?
53
+
54
+ # Pagination
55
+ register(
56
+ key: :visitor_errors_per_page,
57
+ type: :integer,
58
+ default: 25,
59
+ category: :pagination,
60
+ description: "Number of visitor errors to show per page"
61
+ )
62
+ register(
63
+ key: :content_blocks_per_page,
64
+ type: :integer,
65
+ default: 50,
66
+ category: :pagination,
67
+ description: "Number of content blocks to show per page"
68
+ )
69
+ register(
70
+ key: :users_per_page,
71
+ type: :integer,
72
+ default: 50,
73
+ category: :pagination,
74
+ description: "Number of users to show per page"
75
+ )
76
+ register(
77
+ key: :permissions_per_page,
78
+ type: :integer,
79
+ default: 50,
80
+ category: :pagination,
81
+ description: "Number of permissions to show per page"
82
+ )
83
+ register(
84
+ key: :pagination_min_per_page,
85
+ type: :integer,
86
+ default: 5,
87
+ category: :pagination,
88
+ description: "Minimum allowed per-page size"
89
+ )
90
+ register(
91
+ key: :pagination_max_per_page,
92
+ type: :integer,
93
+ default: 200,
94
+ category: :pagination,
95
+ description: "Maximum allowed per-page size"
96
+ )
97
+
98
+ # Analytics
99
+ register(
100
+ key: :analytics_high_volume_threshold,
101
+ type: :integer,
102
+ default: 1000,
103
+ category: :analytics,
104
+ description: "Threshold of visits per IP in selected period to flag high volume traffic"
105
+ )
106
+ register(
107
+ key: :analytics_rapid_request_threshold,
108
+ type: :integer,
109
+ default: 50,
110
+ category: :analytics,
111
+ description: "Threshold of visits from an IP per minute to flag rapid requests"
112
+ )
113
+ register(
114
+ key: :analytics_default_period,
115
+ type: :string,
116
+ default: "week",
117
+ category: :analytics,
118
+ description: "Default analytics period (day/week/month/year)"
119
+ )
120
+ register(
121
+ key: :analytics_max_date_range_days,
122
+ type: :integer,
123
+ default: 365,
124
+ category: :analytics,
125
+ description: "Maximum date range for analytics filters"
126
+ )
127
+ register(
128
+ key: :analytics_max_popular_pages,
129
+ type: :integer,
130
+ default: 10,
131
+ category: :analytics,
132
+ description: "Maximum number of popular pages to show"
133
+ )
134
+ register(
135
+ key: :analytics_max_top_visitors,
136
+ type: :integer,
137
+ default: 10,
138
+ category: :analytics,
139
+ description: "Maximum number of top visitors to show"
140
+ )
141
+ register(
142
+ key: :analytics_cache_duration_seconds,
143
+ type: :integer,
144
+ default: 600,
145
+ category: :analytics,
146
+ description: "Analytics cache duration in seconds"
147
+ )
148
+ register(
149
+ key: :analytics_recent_page_views_limit,
150
+ type: :integer,
151
+ default: 25,
152
+ category: :analytics,
153
+ description: "Recent page views shown on analytics dashboard"
154
+ )
155
+ register(
156
+ key: :analytics_page_details_limit,
157
+ type: :integer,
158
+ default: 100,
159
+ category: :analytics,
160
+ description: "Events shown on page details screen"
161
+ )
162
+ register(
163
+ key: :analytics_visitor_details_limit,
164
+ type: :integer,
165
+ default: 100,
166
+ category: :analytics,
167
+ description: "Events shown on visitor details screen"
168
+ )
169
+
170
+ # Dashboard
171
+ register(
172
+ key: :dashboard_recent_errors_limit,
173
+ type: :integer,
174
+ default: 5,
175
+ category: :dashboard,
176
+ description: "Recent visitor errors shown on dashboard"
177
+ )
178
+ register(
179
+ key: :dashboard_recent_content_blocks_limit,
180
+ type: :integer,
181
+ default: 5,
182
+ category: :dashboard,
183
+ description: "Recent content blocks shown on dashboard"
184
+ )
185
+
186
+ # Navigation visibility
187
+ register(
188
+ key: :nav_show_dashboard,
189
+ type: :boolean,
190
+ default: true,
191
+ category: :navigation,
192
+ description: "Show Dashboard in navigation"
193
+ )
194
+ register(
195
+ key: :nav_show_visual_editor,
196
+ type: :boolean,
197
+ default: true,
198
+ category: :navigation,
199
+ description: "Show Visual Editor in navigation"
200
+ )
201
+ register(
202
+ key: :nav_show_content_blocks,
203
+ type: :boolean,
204
+ default: true,
205
+ category: :navigation,
206
+ description: "Show Content Blocks in navigation"
207
+ )
208
+ register(
209
+ key: :nav_show_settings,
210
+ type: :boolean,
211
+ default: true,
212
+ category: :navigation,
213
+ description: "Show Settings in navigation"
214
+ )
215
+ register(
216
+ key: :nav_show_visitor_errors,
217
+ type: :boolean,
218
+ default: true,
219
+ category: :navigation,
220
+ description: "Show Visitor Errors in navigation"
221
+ )
222
+ register(
223
+ key: :nav_show_permissions,
224
+ type: :boolean,
225
+ default: true,
226
+ category: :navigation,
227
+ description: "Show Permissions in navigation"
228
+ )
229
+ register(
230
+ key: :nav_show_users,
231
+ type: :boolean,
232
+ default: true,
233
+ category: :navigation,
234
+ description: "Show Users in navigation"
235
+ )
236
+ register(
237
+ key: :nav_show_analytics,
238
+ type: :boolean,
239
+ default: true,
240
+ category: :navigation,
241
+ description: "Show Analytics in navigation"
242
+ )
243
+ register(
244
+ key: :nav_order,
245
+ type: :json,
246
+ default: [],
247
+ category: :navigation,
248
+ description: "Order of nav items (set via Settings → Navigation drag-and-drop)"
249
+ )
250
+
251
+ # Content/image constraints
252
+ register(
253
+ key: :reserved_key_prefixes,
254
+ type: :json,
255
+ default: ["admin_"],
256
+ category: :content,
257
+ description: "Reserved prefixes blocked for content block keys"
258
+ )
259
+ register(
260
+ key: :image_content_types,
261
+ type: :json,
262
+ default: ["image/png", "image/jpeg", "image/gif", "image/webp"],
263
+ category: :content,
264
+ description: "Allowed MIME types for content block images"
265
+ )
266
+ register(
267
+ key: :image_max_size,
268
+ type: :integer,
269
+ default: 5 * 1024 * 1024,
270
+ category: :content,
271
+ description: "Max image attachment size in bytes"
272
+ )
273
+ end
274
+
275
+ private
276
+
277
+ def normalize_type(type)
278
+ t = type.to_sym
279
+ return t if %i[string integer boolean json].include?(t)
280
+
281
+ :string
282
+ end
283
+
284
+ def value_type_for(type)
285
+ normalize_type(type).to_s
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ VERSION = "0.1.0"
5
+ end
data/lib/ruby_cms.rb ADDED
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ruby_cms/version"
4
+ require_relative "ruby_cms/css_compiler"
5
+ require_relative "ruby_cms/settings_registry"
6
+ require_relative "ruby_cms/settings"
7
+ require_relative "ruby_cms/engine"
8
+ require_relative "ruby_cms/app_integration"
9
+ require_relative "ruby_cms/content_blocks_sync"
10
+ require_relative "ruby_cms/content_blocks_grouping"
11
+
12
+ module RubyCms
13
+ class Error < StandardError; end
14
+
15
+ mattr_accessor :nav_registry
16
+ self.nav_registry = []
17
+
18
+ # Navigation section keys. Order in sidebar: main, then Settings (bottom).
19
+ # User can add items to either via nav_register with section: NAV_SECTION_MAIN (order 10+)
20
+ # or section: NAV_SECTION_BOTTOM (order 10+).
21
+ NAV_SECTION_MAIN = "main"
22
+ NAV_SECTION_BOTTOM = "Settings"
23
+
24
+ ANALYTICS_ICON_PATH = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
25
+ 'd="M3 3v18h18"></path>' \
26
+ '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
27
+ 'd="M7 13l3-3 3 2 4-5"></path>'
28
+
29
+ def self.configure
30
+ yield(Rails.application.config.ruby_cms)
31
+ end
32
+
33
+ def self.setting(key, default: nil)
34
+ RubyCms::Settings.get(key, default:)
35
+ end
36
+
37
+ # Register a link for admin navigation.
38
+ # Sections: main (top), Settings (bottom). Add to main with section: "main", order: 10+;
39
+ # add to bottom with section: "Settings", order: 10+. Order can be changed via Settings → Navigation drag-and-drop.
40
+ # Options:
41
+ # - key: required Symbol/String
42
+ # - label: required String
43
+ # - path: required String or callable(view_context) -> path
44
+ # - icon: optional SVG path fragment
45
+ # - section: "main" | "Settings" (or nil => main)
46
+ # - order: optional Integer for sorting within section
47
+ # - permission: optional permission key (e.g., :manage_analytics)
48
+ # - default_visible: optional Boolean (default true)
49
+ # - if: optional callable for custom visibility gate
50
+ def self.nav_register(key:, label:, path:, icon: nil, section: nil, order: nil, permission: nil, default_visible: true, **options)
51
+ normalized_key = key.to_sym
52
+ normalized_section = section.presence || NAV_SECTION_MAIN
53
+ entry = {
54
+ key: normalized_key,
55
+ label: label.to_s,
56
+ path: path,
57
+ icon: icon,
58
+ section: normalized_section,
59
+ order: order,
60
+ permission: permission&.to_s,
61
+ default_visible: default_visible ? true : false,
62
+ if: options[:if]
63
+ }
64
+
65
+ self.nav_registry = nav_registry.reject {|e| e[:key] == normalized_key }
66
+ self.nav_registry += [entry]
67
+
68
+ register_navigation_setting!(entry)
69
+
70
+ entry
71
+ end
72
+
73
+ # Returns only entries that pass settings + permissions + conditional checks,
74
+ # sorted by saved nav_order (Settings → Navigation drag-and-drop) when set, else by section + order.
75
+ def self.visible_nav_registry(view_context: nil, user: nil)
76
+ list = nav_registry
77
+ .select {|item| nav_entry_visible?(item, view_context:, user:) }
78
+ .sort_by {|item| nav_sort_tuple(item) }
79
+ apply_nav_order(list)
80
+ rescue StandardError => e
81
+ Rails.logger.error("[RubyCMS] Error filtering navigation: #{e.message}") if defined?(Rails.logger)
82
+ nav_registry
83
+ end
84
+
85
+ module Nav
86
+ def self.register(key:, label:, path:, icon: nil, section: nil, order: nil, permission: nil, default_visible: true, **)
87
+ RubyCms.nav_register(
88
+ key:,
89
+ label:,
90
+ path:,
91
+ icon:,
92
+ section:,
93
+ order:,
94
+ permission:,
95
+ default_visible:,
96
+ **
97
+ )
98
+ end
99
+ end
100
+
101
+ class << self
102
+ private
103
+
104
+ def nav_sort_tuple(item)
105
+ section = item[:section].to_s.presence || NAV_SECTION_MAIN
106
+ priority = nav_section_priority(section)
107
+ [priority, item[:order] || 1000, item[:label].to_s]
108
+ end
109
+
110
+ def nav_section_priority(section)
111
+ section.to_s == NAV_SECTION_MAIN ? 0 : 2
112
+ end
113
+
114
+ def apply_nav_order(list)
115
+ # Read directly from DB so order is always from Settings, not Settings.get (which can return registry default).
116
+ pref = RubyCms::Preference.find_by(key: "nav_order")
117
+ return list unless pref&.value_type == "json" && pref.value.present?
118
+
119
+ saved = JSON.parse(pref.value)
120
+ return list unless saved.kind_of?(Array) && saved.any?
121
+
122
+ order_map = saved.each_with_index.to_h {|k, i| [k.to_s, i] }
123
+ list.sort_by {|item| order_map.fetch(item[:key].to_s, 9999) }
124
+ rescue StandardError
125
+ list
126
+ end
127
+
128
+ def nav_entry_visible?(item, view_context:, user:)
129
+ return false unless setting_enabled_for_nav_item?(item)
130
+ return false unless permission_allows_nav_item?(item, view_context:, user:)
131
+ return false unless condition_allows_nav_item?(item, view_context:)
132
+
133
+ true
134
+ end
135
+
136
+ def setting_enabled_for_nav_item?(item)
137
+ pref_key = :"nav_show_#{item[:key]}"
138
+ RubyCms::Settings.get(pref_key, default: item.fetch(:default_visible, true))
139
+ rescue StandardError
140
+ item.fetch(:default_visible, true)
141
+ end
142
+
143
+ def permission_allows_nav_item?(item, view_context:, user:)
144
+ permission_key = item[:permission]
145
+ return true if permission_key.blank?
146
+
147
+ current_user = user || user_from_view_context(view_context)
148
+ return false if current_user.nil?
149
+ return true unless current_user.respond_to?(:can?)
150
+
151
+ current_user.can?(permission_key.to_sym)
152
+ rescue StandardError
153
+ false
154
+ end
155
+
156
+ def condition_allows_nav_item?(item, view_context:)
157
+ condition = item[:if]
158
+ return true unless condition.respond_to?(:call)
159
+
160
+ case condition.arity
161
+ when 1
162
+ condition.call(view_context)
163
+ else
164
+ condition.call
165
+ end
166
+ rescue StandardError
167
+ false
168
+ end
169
+
170
+ def user_from_view_context(view_context)
171
+ return nil unless view_context
172
+
173
+ return view_context.current_user_cms if view_context.respond_to?(:current_user_cms)
174
+ return view_context.current_user if view_context.respond_to?(:current_user)
175
+
176
+ nil
177
+ end
178
+
179
+ def register_navigation_setting!(entry)
180
+ key = :"nav_show_#{entry[:key]}"
181
+
182
+ RubyCms::SettingsRegistry.register(
183
+ key: key,
184
+ type: :boolean,
185
+ default: entry.fetch(:default_visible, true),
186
+ category: :navigation,
187
+ description: "Show #{entry[:label]} in navigation"
188
+ )
189
+ rescue StandardError => e
190
+ Rails.logger.warn("[RubyCMS] Failed to register nav setting for #{entry[:key]}: #{e.message}") if defined?(Rails.logger)
191
+ end
192
+ end
193
+ end
194
+
195
+ RubyCms::SettingsRegistry.seed_defaults!
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ruby_cms do
4
+ namespace :css do
5
+ desc "Compile RubyCMS admin.css from component files (for gem development)"
6
+ task compile_gem: :environment do
7
+ require "ruby_cms/engine" unless defined?(RubyCms::Engine)
8
+
9
+ dest = RubyCms::Engine.root.join("app/assets/stylesheets/ruby_cms/admin.css")
10
+ RubyCms::Engine.compile_admin_css(dest)
11
+ puts "✓ Compiled admin.css in gem"
12
+ end
13
+
14
+ desc "Compile RubyCMS CSS to host app (combines component files)"
15
+ task compile: :environment do
16
+ require "fileutils"
17
+ require "ruby_cms/engine" unless defined?(RubyCms::Engine)
18
+
19
+ dest_dir = Rails.root.join("app/assets/stylesheets/ruby_cms")
20
+ FileUtils.mkdir_p(dest_dir)
21
+ dest = dest_dir.join("admin.css")
22
+ RubyCms::Engine.compile_admin_css(dest)
23
+ puts "✓ Compiled admin.css to #{dest}"
24
+ puts "✓ RubyCMS CSS compilation complete!"
25
+ end
26
+ end
27
+ end