ruby_cms 0.2.0.9 → 0.2.1.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +20 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +11 -1
- data/app/controllers/ruby_cms/admin/dashboard_controller.rb +2 -3
- data/app/helpers/ruby_cms/application_helper.rb +8 -0
- data/app/javascript/controllers/ruby_cms/index.js +0 -1
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +171 -53
- data/app/views/ruby_cms/_tailwind_safelist.html.erb +2 -0
- data/app/views/ruby_cms/admin/analytics/index.html.erb +8 -8
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +306 -51
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +296 -48
- data/app/views/ruby_cms/admin/dashboard/blocks/_analytics_overview.html.erb +63 -36
- data/app/views/ruby_cms/admin/dashboard/blocks/_quick_actions.html.erb +2 -2
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +3 -12
- data/lib/generators/ruby_cms/install_generator.rb +83 -4
- data/lib/ruby_cms/engine/navigation_registration.rb +17 -16
- data/lib/ruby_cms/version.rb +1 -1
- data/lib/ruby_cms.rb +83 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7267ef45796f4fa3970c9737eaa93ae97acf466e312d471e111a30a2c8161a2d
|
|
4
|
+
data.tar.gz: 1a38b722589679fd1ea1884580f8e05d597676aa3d4b8cf92611c68a5303d705
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c082f14f3b10272bb767b5ac5a4f8b3d9f2c89ddfbdba13bd85b8c4589516b13745a21c4363f093aa103ff5ae11afd534bd4babe2be1d5a9b16b3be003ee538
|
|
7
|
+
data.tar.gz: 50e24617dbe4ad914040fdd2260643425b0a935523fc50ae239ae41f53e002aa127a2d2887915885a5203e2c39ab8380d03d1a16dab69514cdfd504d0e987bf8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.1.1] - 2026-04-14
|
|
4
|
+
- Navigation: add `RubyCms.nav_group(...)` to define sidebar accordion groups in config (optional group page + child pages).
|
|
5
|
+
|
|
6
|
+
## [0.2.1.0] - 2026-04-10
|
|
7
|
+
|
|
8
|
+
- Analytics rework and dashboard changes
|
|
9
|
+
|
|
3
10
|
## [0.2.0.8] - 2026-04-09
|
|
4
11
|
|
|
5
12
|
- Analytics performance: migration adds `ahoy_events (name, time)`, `ahoy_events (visit_id, time)`, `ahoy_visits (started_at)`, `ahoy_visits (visitor_token)` indexes
|
data/README.md
CHANGED
|
@@ -229,6 +229,26 @@ RubyCms.nav_register(
|
|
|
229
229
|
)
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
+
### nav_group (accordion)
|
|
233
|
+
|
|
234
|
+
Define a sidebar accordion group in the same config file as your pages:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
RubyCms.nav_group(
|
|
238
|
+
key: :operations,
|
|
239
|
+
label: "Operations",
|
|
240
|
+
icon: :folder,
|
|
241
|
+
section: "main",
|
|
242
|
+
order: 20,
|
|
243
|
+
# Optional: the group can have its own page
|
|
244
|
+
path: ->(view) { view.main_app.admin_operations_path },
|
|
245
|
+
# Child pages are referenced by the keys you used in register_page/nav_register
|
|
246
|
+
children: %i[backups reports]
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Groups are hidden automatically when they have no visible children and no `path`.
|
|
251
|
+
|
|
232
252
|
### Path Options
|
|
233
253
|
|
|
234
254
|
| Format | Example | Behavior |
|
|
@@ -117,7 +117,7 @@ module RubyCms
|
|
|
117
117
|
nil
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
helper_method :format_chart_date, :format_chart_date_short
|
|
120
|
+
helper_method :format_chart_date, :format_chart_date_short, :format_chart_date_axis
|
|
121
121
|
|
|
122
122
|
def format_chart_date(date_string)
|
|
123
123
|
format_chart_date_by_granularity(date_string, long: true)
|
|
@@ -131,6 +131,16 @@ module RubyCms
|
|
|
131
131
|
date_string.to_s
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
# Compact x-axis tick for daily chart (month view: day number only to avoid crowded labels)
|
|
135
|
+
def format_chart_date_axis(date_string)
|
|
136
|
+
date = Date.parse(date_string.to_s)
|
|
137
|
+
return date.day.to_s if @period == "month"
|
|
138
|
+
|
|
139
|
+
format_chart_date_short(date_string)
|
|
140
|
+
rescue Date::Error
|
|
141
|
+
date_string.to_s
|
|
142
|
+
end
|
|
143
|
+
|
|
134
144
|
def format_daily_date(date_string)
|
|
135
145
|
date = Date.parse(date_string.to_s)
|
|
136
146
|
if @period == "month"
|
|
@@ -26,9 +26,8 @@ module RubyCms
|
|
|
26
26
|
|
|
27
27
|
def assign_dashboard_blocks
|
|
28
28
|
visible = RubyCms.visible_dashboard_blocks(user: current_user_cms)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.map {|b| prepare_dashboard_block(b) }
|
|
29
|
+
# Top :stats row (pages, users, permissions, errors) is not rendered on the dashboard layout.
|
|
30
|
+
@stats_blocks = []
|
|
32
31
|
main = visible
|
|
33
32
|
.select {|b| b[:section] == :main }
|
|
34
33
|
.map {|b| prepare_dashboard_block(b) }
|
|
@@ -25,6 +25,14 @@ module RubyCms
|
|
|
25
25
|
)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def ruby_cms_nav_sidebar_rows(section: :main)
|
|
29
|
+
RubyCms.visible_nav_sidebar_rows(
|
|
30
|
+
section:,
|
|
31
|
+
view_context: self,
|
|
32
|
+
user: (current_user_cms if respond_to?(:current_user_cms))
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
28
36
|
# Render an SVG fragment (typically <path ...>) safely.
|
|
29
37
|
# Used for nav icons which may come from host app configuration.
|
|
30
38
|
def ruby_cms_safe_svg_fragment(fragment)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%# Admin Sidebar Navigation %>
|
|
2
|
-
<aside class="
|
|
3
|
-
<div class="
|
|
2
|
+
<aside class="flex h-full min-h-0 w-56 max-w-56 shrink-0 flex-col overflow-hidden bg-[#FAF9F5]" aria-label="Admin" data-ruby-cms--mobile-menu-target="sidebar">
|
|
3
|
+
<div class="pl-3 pr-4 py-5 flex-shrink-0 bg-[#FAF9F5]">
|
|
4
4
|
<div class="flex items-center gap-3" data-ruby-cms--mobile-menu-target="sidebarContent">
|
|
5
5
|
<% if respond_to?(:image_tag, true) %>
|
|
6
6
|
<div class="w-9 h-9 flex-shrink-0 rounded-md bg-white border border-gray-200 overflow-hidden">
|
|
@@ -14,60 +14,178 @@
|
|
|
14
14
|
</div>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
-
<nav class="flex-1
|
|
18
|
-
<%
|
|
19
|
-
|
|
20
|
-
nav_bottom = "Settings"
|
|
21
|
-
entries = ruby_cms_nav_entries
|
|
22
|
-
by_section = entries.group_by { |e| s = e[:section].to_s.presence || nav_main; s == "custom" ? nav_bottom : s }
|
|
23
|
-
main_items = by_section[nav_main] || []
|
|
24
|
-
bottom_items = by_section[nav_bottom] || []
|
|
25
|
-
%>
|
|
17
|
+
<nav class="flex-1 min-w-0 pl-2 pr-2.5 py-5 overflow-y-auto overflow-x-hidden" aria-label="Admin" data-ruby-cms--mobile-menu-target="sidebarContent">
|
|
18
|
+
<% main_rows = ruby_cms_nav_sidebar_rows(section: :main) %>
|
|
19
|
+
<% bottom_rows = ruby_cms_nav_sidebar_rows(section: :settings) %>
|
|
26
20
|
<%# Main nav: top, scrollable %>
|
|
27
|
-
<div class="flex-1
|
|
28
|
-
<%
|
|
29
|
-
<%
|
|
30
|
-
<%
|
|
31
|
-
<%
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<%
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
<div class="min-h-0 flex-1 space-y-1 overflow-y-auto overflow-x-hidden">
|
|
22
|
+
<% main_rows.each do |row| %>
|
|
23
|
+
<% if row[:type] == :link %>
|
|
24
|
+
<% e = row[:entry] %>
|
|
25
|
+
<% begin %>
|
|
26
|
+
<% path = e[:path].respond_to?(:call) ? e[:path].call(self) : e[:path] %>
|
|
27
|
+
<% next if path.blank? %>
|
|
28
|
+
<% path_str = path.respond_to?(:to_str) ? path.to_str : path.to_s %>
|
|
29
|
+
<% active = (path_str == "/admin" || path_str == "/admin/") ? request.path.match?(%r{\A/admin/?\z}) : request.path.start_with?(path_str) %>
|
|
30
|
+
<%= link_to path, class: "flex min-w-0 items-center gap-2 px-2 py-2 text-xs font-medium leading-5 rounded-md mb-0.5 no-underline transition-colors #{ active ? 'text-blue-600 font-semibold' : 'text-gray-600 hover:bg-blue-500 hover:text-white' }" do %>
|
|
31
|
+
<% if e[:icon].present? %>
|
|
32
|
+
<svg class="w-4 h-4 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
33
|
+
<%= ruby_cms_safe_svg_fragment(e[:icon]) %>
|
|
34
|
+
</svg>
|
|
35
|
+
<% end %>
|
|
36
|
+
<span class="min-w-0 flex-1 truncate"><%= e[:label] %></span>
|
|
39
37
|
<% end %>
|
|
40
|
-
|
|
38
|
+
<% rescue => ex %>
|
|
39
|
+
<% Rails.logger.error "RubyCMS nav error for #{e[:key]}: #{ex.message}" if defined?(Rails.logger) %>
|
|
41
40
|
<% end %>
|
|
42
|
-
<%
|
|
43
|
-
<%
|
|
41
|
+
<% else %>
|
|
42
|
+
<% g = row[:group] %>
|
|
43
|
+
<% children = row[:children] || [] %>
|
|
44
|
+
<% g_icon = g[:icon] %>
|
|
45
|
+
<% details_open = g.fetch(:default_open, true) %>
|
|
46
|
+
<details class="mb-0.5" <%= "open" if details_open %>>
|
|
47
|
+
<summary class="flex min-w-0 items-center justify-between gap-2 px-2 py-2 text-xs font-medium leading-5 rounded-md text-gray-600 cursor-pointer select-none hover:bg-blue-500 hover:text-white list-none [&::-webkit-details-marker]:hidden">
|
|
48
|
+
<span class="flex min-w-0 flex-1 items-center gap-2">
|
|
49
|
+
<% if g_icon.present? %>
|
|
50
|
+
<svg class="w-4 h-4 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
51
|
+
<%= ruby_cms_safe_svg_fragment(g_icon) %>
|
|
52
|
+
</svg>
|
|
53
|
+
<% end %>
|
|
54
|
+
<span class="truncate"><%= g[:label] %></span>
|
|
55
|
+
</span>
|
|
56
|
+
<svg class="w-3.5 h-3.5 flex-shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
57
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
58
|
+
</svg>
|
|
59
|
+
</summary>
|
|
60
|
+
<div class="mt-0.5 space-y-0.5 ml-2 pl-2 py-1">
|
|
61
|
+
<% if g[:path].present? %>
|
|
62
|
+
<% begin %>
|
|
63
|
+
<% gpath = g[:path].respond_to?(:call) ? g[:path].call(self) : g[:path] %>
|
|
64
|
+
<% if gpath.present? %>
|
|
65
|
+
<% gpath_str = gpath.respond_to?(:to_str) ? gpath.to_str : gpath.to_s %>
|
|
66
|
+
<% active = (gpath_str == "/admin" || gpath_str == "/admin/") ? request.path.match?(%r{\A/admin/?\z}) : request.path.start_with?(gpath_str) %>
|
|
67
|
+
<%= link_to gpath, class: "flex min-w-0 items-center gap-1.5 px-2 py-1.5 text-xs font-medium leading-5 rounded-md no-underline transition-colors #{ active ? 'text-blue-600 font-semibold' : 'text-gray-600 hover:bg-blue-500 hover:text-white' }" do %>
|
|
68
|
+
<% if g_icon.present? %>
|
|
69
|
+
<svg class="w-3.5 h-3.5 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
70
|
+
<%= ruby_cms_safe_svg_fragment(g_icon) %>
|
|
71
|
+
</svg>
|
|
72
|
+
<% end %>
|
|
73
|
+
<span class="min-w-0 flex-1 truncate"><%= g[:label] %></span>
|
|
74
|
+
<% end %>
|
|
75
|
+
<% end %>
|
|
76
|
+
<% rescue => ex %>
|
|
77
|
+
<% Rails.logger.error "RubyCMS nav group link #{g[:key]}: #{ex.message}" if defined?(Rails.logger) %>
|
|
78
|
+
<% end %>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% children.each do |e| %>
|
|
81
|
+
<% begin %>
|
|
82
|
+
<% path = e[:path].respond_to?(:call) ? e[:path].call(self) : e[:path] %>
|
|
83
|
+
<% next if path.blank? %>
|
|
84
|
+
<% path_str = path.respond_to?(:to_str) ? path.to_str : path.to_s %>
|
|
85
|
+
<% active = (path_str == "/admin" || path_str == "/admin/") ? request.path.match?(%r{\A/admin/?\z}) : request.path.start_with?(path_str) %>
|
|
86
|
+
<%= link_to path, class: "flex min-w-0 items-center gap-1.5 px-2 py-1.5 text-xs font-medium leading-5 rounded-md no-underline transition-colors #{ active ? 'text-blue-600 font-semibold' : 'text-gray-600 hover:bg-blue-500 hover:text-white' }" do %>
|
|
87
|
+
<% if e[:icon].present? %>
|
|
88
|
+
<svg class="w-3.5 h-3.5 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
89
|
+
<%= ruby_cms_safe_svg_fragment(e[:icon]) %>
|
|
90
|
+
</svg>
|
|
91
|
+
<% end %>
|
|
92
|
+
<span class="min-w-0 flex-1 truncate"><%= e[:label] %></span>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% rescue => ex %>
|
|
95
|
+
<% Rails.logger.error "RubyCMS nav group child #{e[:key]}: #{ex.message}" if defined?(Rails.logger) %>
|
|
96
|
+
<% end %>
|
|
97
|
+
<% end %>
|
|
98
|
+
</div>
|
|
99
|
+
</details>
|
|
44
100
|
<% end %>
|
|
45
101
|
<% end %>
|
|
46
102
|
</div>
|
|
47
103
|
|
|
48
104
|
<%# Spacer so bottom section sits above footer %>
|
|
49
105
|
<div class="flex-shrink-0 pt-4 mt-4">
|
|
50
|
-
<% if
|
|
51
|
-
<div class="text-xs font-semibold uppercase tracking-
|
|
106
|
+
<% if bottom_rows.any? %>
|
|
107
|
+
<div class="px-1.5 pb-1.5 text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
52
108
|
<%= t("ruby_cms.nav.settings", default: "Settings") %>
|
|
53
109
|
</div>
|
|
54
110
|
<div class="space-y-1">
|
|
55
|
-
<%
|
|
56
|
-
<%
|
|
57
|
-
<%
|
|
58
|
-
<%
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<%
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
111
|
+
<% bottom_rows.each do |row| %>
|
|
112
|
+
<% if row[:type] == :link %>
|
|
113
|
+
<% e = row[:entry] %>
|
|
114
|
+
<% begin %>
|
|
115
|
+
<% path = e[:path].respond_to?(:call) ? e[:path].call(self) : e[:path] %>
|
|
116
|
+
<% next if path.blank? %>
|
|
117
|
+
<% path_str = path.respond_to?(:to_str) ? path.to_str : path.to_s %>
|
|
118
|
+
<% active = request.path.start_with?(path_str) %>
|
|
119
|
+
<%= link_to path, class: "flex min-w-0 items-center gap-2 px-2 py-2 text-xs font-medium leading-5 rounded-md mb-0.5 no-underline transition-colors #{ active ? 'text-blue-600 font-semibold' : 'text-gray-600 hover:bg-blue-500 hover:text-white' }" do %>
|
|
120
|
+
<% if e[:icon].present? %>
|
|
121
|
+
<svg class="w-4 h-4 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
122
|
+
<%= ruby_cms_safe_svg_fragment(e[:icon]) %>
|
|
123
|
+
</svg>
|
|
124
|
+
<% end %>
|
|
125
|
+
<span class="min-w-0 flex-1 truncate"><%= e[:label] %></span>
|
|
66
126
|
<% end %>
|
|
67
|
-
|
|
127
|
+
<% rescue => ex %>
|
|
128
|
+
<% Rails.logger.error "RubyCMS nav error for #{e[:key]}: #{ex.message}" if defined?(Rails.logger) %>
|
|
68
129
|
<% end %>
|
|
69
|
-
<%
|
|
70
|
-
<%
|
|
130
|
+
<% else %>
|
|
131
|
+
<% g = row[:group] %>
|
|
132
|
+
<% children = row[:children] || [] %>
|
|
133
|
+
<% g_icon = g[:icon] %>
|
|
134
|
+
<% details_open = g.fetch(:default_open, true) %>
|
|
135
|
+
<details class="mb-0.5" <%= "open" if details_open %>>
|
|
136
|
+
<summary class="flex min-w-0 items-center justify-between gap-2 px-2 py-2 text-xs font-medium leading-5 rounded-md text-gray-600 cursor-pointer select-none hover:bg-blue-500 hover:text-white list-none [&::-webkit-details-marker]:hidden">
|
|
137
|
+
<span class="flex min-w-0 flex-1 items-center gap-2">
|
|
138
|
+
<% if g_icon.present? %>
|
|
139
|
+
<svg class="w-4 h-4 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
140
|
+
<%= ruby_cms_safe_svg_fragment(g_icon) %>
|
|
141
|
+
</svg>
|
|
142
|
+
<% end %>
|
|
143
|
+
<span class="truncate"><%= g[:label] %></span>
|
|
144
|
+
</span>
|
|
145
|
+
<svg class="w-3.5 h-3.5 flex-shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
146
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
147
|
+
</svg>
|
|
148
|
+
</summary>
|
|
149
|
+
<div class="mt-0.5 space-y-0.5 ml-2 pl-2 py-1">
|
|
150
|
+
<% if g[:path].present? %>
|
|
151
|
+
<% begin %>
|
|
152
|
+
<% gpath = g[:path].respond_to?(:call) ? g[:path].call(self) : g[:path] %>
|
|
153
|
+
<% if gpath.present? %>
|
|
154
|
+
<% gpath_str = gpath.respond_to?(:to_str) ? gpath.to_str : gpath.to_s %>
|
|
155
|
+
<% active = request.path.start_with?(gpath_str) %>
|
|
156
|
+
<%= link_to gpath, class: "flex min-w-0 items-center gap-1.5 px-2 py-1.5 text-xs font-medium leading-5 rounded-md no-underline transition-colors #{ active ? 'text-blue-600 font-semibold' : 'text-gray-600 hover:bg-blue-500 hover:text-white' }" do %>
|
|
157
|
+
<% if g_icon.present? %>
|
|
158
|
+
<svg class="w-3.5 h-3.5 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
159
|
+
<%= ruby_cms_safe_svg_fragment(g_icon) %>
|
|
160
|
+
</svg>
|
|
161
|
+
<% end %>
|
|
162
|
+
<span class="min-w-0 flex-1 truncate"><%= g[:label] %></span>
|
|
163
|
+
<% end %>
|
|
164
|
+
<% end %>
|
|
165
|
+
<% rescue => ex %>
|
|
166
|
+
<% Rails.logger.error "RubyCMS nav group link #{g[:key]}: #{ex.message}" if defined?(Rails.logger) %>
|
|
167
|
+
<% end %>
|
|
168
|
+
<% end %>
|
|
169
|
+
<% children.each do |e| %>
|
|
170
|
+
<% begin %>
|
|
171
|
+
<% path = e[:path].respond_to?(:call) ? e[:path].call(self) : e[:path] %>
|
|
172
|
+
<% next if path.blank? %>
|
|
173
|
+
<% path_str = path.respond_to?(:to_str) ? path.to_str : path.to_s %>
|
|
174
|
+
<% active = request.path.start_with?(path_str) %>
|
|
175
|
+
<%= link_to path, class: "flex min-w-0 items-center gap-1.5 px-2 py-1.5 text-xs font-medium leading-5 rounded-md no-underline transition-colors #{ active ? 'text-blue-600 font-semibold' : 'text-gray-600 hover:bg-blue-500 hover:text-white' }" do %>
|
|
176
|
+
<% if e[:icon].present? %>
|
|
177
|
+
<svg class="w-3.5 h-3.5 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
178
|
+
<%= ruby_cms_safe_svg_fragment(e[:icon]) %>
|
|
179
|
+
</svg>
|
|
180
|
+
<% end %>
|
|
181
|
+
<span class="min-w-0 flex-1 truncate"><%= e[:label] %></span>
|
|
182
|
+
<% end %>
|
|
183
|
+
<% rescue => ex %>
|
|
184
|
+
<% Rails.logger.error "RubyCMS nav group child #{e[:key]}: #{ex.message}" if defined?(Rails.logger) %>
|
|
185
|
+
<% end %>
|
|
186
|
+
<% end %>
|
|
187
|
+
</div>
|
|
188
|
+
</details>
|
|
71
189
|
<% end %>
|
|
72
190
|
<% end %>
|
|
73
191
|
</div>
|
|
@@ -75,16 +193,16 @@
|
|
|
75
193
|
</div>
|
|
76
194
|
</nav>
|
|
77
195
|
|
|
78
|
-
<div class="
|
|
196
|
+
<div class="min-w-0 shrink-0 bg-[#FAF9F5] py-2.5 pl-2 pr-2.5" data-ruby-cms--mobile-menu-target="sidebarContent">
|
|
79
197
|
<div class="mb-2">
|
|
80
198
|
<%= form_with url: ruby_cms_admin_locale_path, method: :patch, scope: nil, data: { turbo: false } do |form| %>
|
|
81
199
|
<div class="relative">
|
|
82
|
-
<div class="flex items-center gap-
|
|
83
|
-
<svg class="w-
|
|
200
|
+
<div class="flex w-full min-w-0 items-center gap-2 px-2 py-2 text-xs font-medium leading-5 text-gray-600 rounded-md no-underline transition-colors hover:bg-blue-500 hover:text-white">
|
|
201
|
+
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
84
202
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 21a9 9 0 100-18 9 9 0 000 18zM3.6 9h16.8M3.6 15h16.8M11 3c-2.2 2.2-3.5 5.2-3.5 9s1.3 6.8 3.5 9m2 0c2.2-2.2 3.5-5.2 3.5-9S15.2 5.2 13 3"/>
|
|
85
203
|
</svg>
|
|
86
|
-
<span class="flex-1 text-left"><%= ruby_cms_locale_display_name(I18n.locale) %></span>
|
|
87
|
-
<svg class="w-
|
|
204
|
+
<span class="min-w-0 flex-1 truncate text-left"><%= ruby_cms_locale_display_name(I18n.locale) %></span>
|
|
205
|
+
<svg class="w-3.5 h-3.5 flex-shrink-0 opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
88
206
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10l4-4 4 4M16 14l-4 4-4-4"/>
|
|
89
207
|
</svg>
|
|
90
208
|
</div>
|
|
@@ -102,20 +220,20 @@
|
|
|
102
220
|
</div>
|
|
103
221
|
|
|
104
222
|
<% if respond_to?(:main_app, true) && main_app.respond_to?(:root_path) %>
|
|
105
|
-
<%= link_to main_app.root_path, target: "_blank", class: "flex items-center gap-
|
|
106
|
-
<svg class="w-
|
|
223
|
+
<%= link_to main_app.root_path, target: "_blank", class: "flex w-full min-w-0 items-center gap-2 px-2 py-2 text-xs font-medium leading-5 text-gray-600 rounded-md no-underline transition-colors hover:bg-blue-500 hover:text-white" do %>
|
|
224
|
+
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
107
225
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
|
108
226
|
</svg>
|
|
109
|
-
<span class="flex-1 text-left"><%= t("ruby_cms.nav.view_site", default: "View site") %></span>
|
|
227
|
+
<span class="min-w-0 flex-1 truncate text-left"><%= t("ruby_cms.nav.view_site", default: "View site") %></span>
|
|
110
228
|
<% end %>
|
|
111
229
|
<% end %>
|
|
112
230
|
|
|
113
231
|
<% if respond_to?(:main_app, true) && main_app.respond_to?(:session_path) %>
|
|
114
|
-
<%= button_to main_app.session_path, method: :delete, class: "flex items-center gap-
|
|
115
|
-
<svg class="w-
|
|
232
|
+
<%= button_to main_app.session_path, method: :delete, class: "flex w-full min-w-0 items-center gap-2 px-2 py-2 text-xs font-medium leading-5 text-gray-600 rounded-md border-0 bg-transparent cursor-pointer transition-colors hover:bg-blue-500 hover:text-white", data: { turbo_confirm: t("ruby_cms.nav.logout_confirm", default: "Are you sure you want to logout?") } do %>
|
|
233
|
+
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
116
234
|
<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"></path>
|
|
117
235
|
</svg>
|
|
118
|
-
<span class="flex-1 text-left"><%= t("ruby_cms.nav.logout", default: "Logout") %></span>
|
|
236
|
+
<span class="min-w-0 flex-1 truncate text-left"><%= t("ruby_cms.nav.logout", default: "Logout") %></span>
|
|
119
237
|
<% end %>
|
|
120
238
|
<% end %>
|
|
121
239
|
</div>
|
|
@@ -37,23 +37,23 @@
|
|
|
37
37
|
<% end %>
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
|
-
<%# ── Activity charts (3/4) + KPI summary card (1/4) ── %>
|
|
41
|
-
<div class="grid gap-4 items-
|
|
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;">
|
|
42
42
|
|
|
43
43
|
<%# Left: activity charts stacked %>
|
|
44
|
-
<div class="
|
|
44
|
+
<div class="flex min-h-0 min-w-0 flex-col gap-4">
|
|
45
45
|
<%= render "ruby_cms/admin/analytics/partials/daily_activity_chart" %>
|
|
46
46
|
<%= render "ruby_cms/admin/analytics/partials/hourly_activity_chart" %>
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
|
-
<%# Right:
|
|
50
|
-
<div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
|
|
51
|
-
<div class="px-4 py-3 border-b border-border/60">
|
|
49
|
+
<%# Right: KPI summary — stretches to match combined chart column height %>
|
|
50
|
+
<div class="flex h-full min-h-0 min-w-0 flex-col rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
|
|
51
|
+
<div class="shrink-0 px-4 py-3 border-b border-border/60">
|
|
52
52
|
<p class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Overview</p>
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
55
|
<%# Active users now %>
|
|
56
|
-
<div class="px-4 py-3 bg-emerald-50/60 border-b border-emerald-100">
|
|
56
|
+
<div class="shrink-0 px-4 py-3 bg-emerald-50/60 border-b border-emerald-100">
|
|
57
57
|
<div class="flex items-center justify-between">
|
|
58
58
|
<div>
|
|
59
59
|
<p class="text-xs font-medium text-emerald-700">Nu actief</p>
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
</div>
|
|
74
74
|
</div>
|
|
75
75
|
|
|
76
|
-
<div class="divide-y divide-border/50">
|
|
76
|
+
<div class="min-h-0 flex-1 divide-y divide-border/50 overflow-y-auto">
|
|
77
77
|
|
|
78
78
|
<%# Page Views %>
|
|
79
79
|
<div class="px-4 py-3">
|