ruby_cms 0.2.1.0 → 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 +3 -0
- data/README.md +20 -0
- data/app/helpers/ruby_cms/application_helper.rb +8 -0
- 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/dashboard/blocks/_quick_actions.html.erb +2 -2
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +1 -1
- 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
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 |
|
|
@@ -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>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<div class="flex flex-col divide-y divide-gray-100">
|
|
10
10
|
<%= link_to new_ruby_cms_admin_content_block_path, class: "group flex items-center justify-between gap-2 px-5 py-3 hover:bg-gray-50 transition-colors" do %>
|
|
11
11
|
<div class="flex min-w-0 items-center gap-2.5">
|
|
12
|
-
<div class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-teal-
|
|
12
|
+
<div class="mr-2 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-teal-100 text-teal-800">
|
|
13
13
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
|
14
14
|
</div>
|
|
15
15
|
<div class="min-w-0">
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
<%= link_to ruby_cms_admin_visitor_errors_path, class: "group flex items-center justify-between gap-2 px-5 py-3 hover:bg-gray-50 transition-colors" do %>
|
|
37
37
|
<div class="flex min-w-0 items-center gap-2.5">
|
|
38
|
-
<div class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-rose-
|
|
38
|
+
<div class="mr-2 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-rose-100 text-rose-800">
|
|
39
39
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
40
40
|
</div>
|
|
41
41
|
<div class="min-w-0">
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
subtitle: "Welcome back! Here's an overview of your CMS.",
|
|
4
4
|
content_card: false
|
|
5
5
|
) do %>
|
|
6
|
-
<div class="space-y-
|
|
6
|
+
<div class="space-y-8 pb-8">
|
|
7
7
|
|
|
8
8
|
<%# Main row: three equal columns — quick actions | recent errors | analytics %>
|
|
9
9
|
<% if @primary_main_blocks.any? %>
|
|
@@ -492,8 +492,7 @@ module RubyCms
|
|
|
492
492
|
end
|
|
493
493
|
|
|
494
494
|
def configure_tailwind(tailwind_css)
|
|
495
|
-
|
|
496
|
-
remove_ruby_cms_tailwind_content_paths
|
|
495
|
+
ensure_ruby_cms_tailwind_safelist_partial
|
|
497
496
|
run "bin/rails tailwindcss:build" if File.exist?(tailwind_css)
|
|
498
497
|
# Importmap pins are provided by the engine via `ruby_cms/config/importmap.rb`.
|
|
499
498
|
add_importmap_pins
|
|
@@ -869,9 +868,89 @@ module RubyCms
|
|
|
869
868
|
end.join("\n")
|
|
870
869
|
end
|
|
871
870
|
|
|
871
|
+
# Keep Tailwind generation fast without scanning the gem path:
|
|
872
|
+
# generate a host-app safelist partial from RubyCMS classes.
|
|
873
|
+
def ensure_ruby_cms_tailwind_safelist_partial
|
|
874
|
+
classes = ruby_cms_tailwind_classes
|
|
875
|
+
return if classes.empty?
|
|
876
|
+
|
|
877
|
+
safelist_path = Rails.root.join("app/views/ruby_cms/_tailwind_safelist.html.erb")
|
|
878
|
+
FileUtils.mkdir_p(safelist_path.dirname)
|
|
879
|
+
File.write(safelist_path, build_tailwind_safelist_partial(classes))
|
|
880
|
+
say "✓ Task tailwind/safelist: Generated app/views/ruby_cms/_tailwind_safelist.html.erb.",
|
|
881
|
+
:green
|
|
882
|
+
rescue StandardError => e
|
|
883
|
+
say "⚠ Task tailwind/safelist: Could not generate safelist partial: #{e.message}.", :yellow
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
def ruby_cms_tailwind_classes
|
|
887
|
+
patterns = [
|
|
888
|
+
RubyCms::Engine.root.join("app/views/**/*.erb"),
|
|
889
|
+
RubyCms::Engine.root.join("app/components/**/*.rb"),
|
|
890
|
+
RubyCms::Engine.root.join("app/helpers/**/*.rb"),
|
|
891
|
+
RubyCms::Engine.root.join("app/javascript/**/*.js")
|
|
892
|
+
]
|
|
893
|
+
|
|
894
|
+
tokens = Set.new
|
|
895
|
+
patterns.each do |pattern|
|
|
896
|
+
Dir.glob(pattern.to_s).each do |path|
|
|
897
|
+
extract_tailwind_tokens_from(File.read(path), tokens)
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
tokens.to_a.sort
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def extract_tailwind_tokens_from(content, tokens)
|
|
905
|
+
class_strings(content).each do |raw|
|
|
906
|
+
add_tailwind_tokens(raw, tokens)
|
|
907
|
+
|
|
908
|
+
# Also include classes from interpolated branches:
|
|
909
|
+
# "#{active ? 'text-blue-600' : 'text-gray-600'}"
|
|
910
|
+
raw.scan(/['"]([^'"]+)['"]/).flatten.each do |inner|
|
|
911
|
+
add_tailwind_tokens(inner, tokens)
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
def class_strings(content)
|
|
917
|
+
regexes = [
|
|
918
|
+
/class\s*=\s*"([^"]+)"/m,
|
|
919
|
+
/class\s*=\s*'([^']+)'/m,
|
|
920
|
+
/class:\s*"([^"]+)"/m,
|
|
921
|
+
/class:\s*'([^']+)'/m
|
|
922
|
+
]
|
|
923
|
+
|
|
924
|
+
regexes.flat_map { |regex| content.scan(regex).flatten.compact }
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
def add_tailwind_tokens(value, tokens)
|
|
928
|
+
value
|
|
929
|
+
.gsub(/\#\{[^}]*\}/, " ")
|
|
930
|
+
.split(/\s+/)
|
|
931
|
+
.map(&:strip)
|
|
932
|
+
.reject(&:empty?)
|
|
933
|
+
.each do |token|
|
|
934
|
+
next unless token.match?(/\A[-!:\[\]\/.%a-zA-Z0-9_]+\z/)
|
|
935
|
+
next unless token.match?(/[A-Za-z]/)
|
|
936
|
+
next if token.start_with?("http", "/", "#")
|
|
937
|
+
|
|
938
|
+
tokens << token
|
|
939
|
+
end
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def build_tailwind_safelist_partial(classes)
|
|
943
|
+
grouped_classes = classes.each_slice(20).map { |group| group.join(" ") }.join("\n ")
|
|
944
|
+
|
|
945
|
+
<<~ERB
|
|
946
|
+
<%# RubyCMS Tailwind safelist (auto-generated by ruby_cms:install). %>
|
|
947
|
+
<%# Keep this file so Tailwind picks up classes used inside the RubyCMS gem. %>
|
|
948
|
+
<div class="hidden #{grouped_classes}"></div>
|
|
949
|
+
ERB
|
|
950
|
+
end
|
|
951
|
+
|
|
872
952
|
# RubyCMS admin styles are precompiled to app/assets/stylesheets/ruby_cms/admin.css.
|
|
873
|
-
#
|
|
874
|
-
# Not a generator task.
|
|
953
|
+
# Legacy cleanup helpers kept for backward compatibility.
|
|
875
954
|
def remove_ruby_cms_tailwind_source(tailwind_css_path)
|
|
876
955
|
return unless valid_tailwind_source_path?(tailwind_css_path)
|
|
877
956
|
|
|
@@ -39,15 +39,6 @@ module RubyCms
|
|
|
39
39
|
permission: :manage_analytics,
|
|
40
40
|
order: 1
|
|
41
41
|
)
|
|
42
|
-
RubyCms.register_page(
|
|
43
|
-
key: :permissions,
|
|
44
|
-
label: "Permissions",
|
|
45
|
-
path: lambda(&:ruby_cms_admin_permissions_path),
|
|
46
|
-
icon: :shield_check,
|
|
47
|
-
section: :settings,
|
|
48
|
-
permission: :manage_permissions,
|
|
49
|
-
order: 2
|
|
50
|
-
)
|
|
51
42
|
RubyCms.register_page(
|
|
52
43
|
key: :visitor_errors,
|
|
53
44
|
label: "Visitor errors",
|
|
@@ -55,7 +46,16 @@ module RubyCms
|
|
|
55
46
|
icon: :exclamation_triangle,
|
|
56
47
|
section: :settings,
|
|
57
48
|
permission: :manage_visitor_errors,
|
|
58
|
-
order:
|
|
49
|
+
order: 2
|
|
50
|
+
)
|
|
51
|
+
RubyCms.register_page(
|
|
52
|
+
key: :permissions,
|
|
53
|
+
label: "Permissions",
|
|
54
|
+
path: lambda(&:ruby_cms_admin_permissions_path),
|
|
55
|
+
icon: :shield_check,
|
|
56
|
+
section: :settings,
|
|
57
|
+
permission: :manage_permissions,
|
|
58
|
+
order: 10
|
|
59
59
|
)
|
|
60
60
|
RubyCms.register_page(
|
|
61
61
|
key: :users,
|
|
@@ -64,7 +64,7 @@ module RubyCms
|
|
|
64
64
|
icon: :user_group,
|
|
65
65
|
section: :settings,
|
|
66
66
|
permission: :manage_permissions,
|
|
67
|
-
order:
|
|
67
|
+
order: 20
|
|
68
68
|
)
|
|
69
69
|
RubyCms.register_page(
|
|
70
70
|
key: :commands,
|
|
@@ -73,16 +73,17 @@ module RubyCms
|
|
|
73
73
|
icon: :wrench,
|
|
74
74
|
section: :settings,
|
|
75
75
|
permission: :manage_admin,
|
|
76
|
-
order:
|
|
76
|
+
order: 30
|
|
77
77
|
)
|
|
78
|
-
RubyCms.
|
|
78
|
+
RubyCms.nav_group(
|
|
79
79
|
key: :settings,
|
|
80
80
|
label: "Settings",
|
|
81
81
|
path: lambda(&:ruby_cms_admin_settings_path),
|
|
82
82
|
icon: :cog_6_tooth,
|
|
83
|
-
section:
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
section: RubyCms::NAV_SECTION_BOTTOM,
|
|
84
|
+
children: %i[permissions users commands],
|
|
85
|
+
default_open: false,
|
|
86
|
+
order: 3
|
|
86
87
|
)
|
|
87
88
|
end
|
|
88
89
|
end
|
data/lib/ruby_cms/version.rb
CHANGED
data/lib/ruby_cms.rb
CHANGED
|
@@ -75,6 +75,7 @@ module RubyCms
|
|
|
75
75
|
resolved_path = path.kind_of?(Symbol) ? ->(v) { v.main_app.send(path) } : path
|
|
76
76
|
resolved_icon = icon.nil? ? nil : RubyCms::Icons.resolve(icon)
|
|
77
77
|
entry = {
|
|
78
|
+
type: :link,
|
|
78
79
|
key: normalized_key,
|
|
79
80
|
label: label.to_s,
|
|
80
81
|
path: resolved_path,
|
|
@@ -94,6 +95,49 @@ module RubyCms
|
|
|
94
95
|
entry
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
# Register a navigation group (accordion) in the sidebar.
|
|
99
|
+
#
|
|
100
|
+
# A group can optionally have its own page (`path:`) and can contain child pages
|
|
101
|
+
# (registered via `register_page` / `nav_register`) referenced by their keys.
|
|
102
|
+
#
|
|
103
|
+
# Notes:
|
|
104
|
+
# - `children:` is an array of nav keys (Symbol/String) that will be rendered under the group.
|
|
105
|
+
# - Children are filtered by the same visibility rules as regular nav links.
|
|
106
|
+
# - A group is hidden if it has no visible children and no `path`.
|
|
107
|
+
def self.nav_group(
|
|
108
|
+
key:, label:, children:,
|
|
109
|
+
path: nil, icon: nil, section: nil, order: nil,
|
|
110
|
+
permission: nil, default_visible: true, default_open: true, **options
|
|
111
|
+
)
|
|
112
|
+
normalized_key = key.to_sym
|
|
113
|
+
normalized_section = section.presence || NAV_SECTION_MAIN
|
|
114
|
+
resolved_path = path.nil? ? nil : (path.kind_of?(Symbol) ? ->(v) { v.main_app.send(path) } : path)
|
|
115
|
+
resolved_icon = icon.nil? ? nil : RubyCms::Icons.resolve(icon)
|
|
116
|
+
normalized_children = Array(children).map(&:to_s).map(&:strip).reject(&:blank?).map(&:to_sym)
|
|
117
|
+
|
|
118
|
+
entry = {
|
|
119
|
+
type: :group,
|
|
120
|
+
key: normalized_key,
|
|
121
|
+
label: label.to_s,
|
|
122
|
+
path: resolved_path,
|
|
123
|
+
icon: resolved_icon,
|
|
124
|
+
section: normalized_section,
|
|
125
|
+
order: order,
|
|
126
|
+
permission: permission&.to_s,
|
|
127
|
+
default_visible: default_visible ? true : false,
|
|
128
|
+
default_open: default_open ? true : false,
|
|
129
|
+
children: normalized_children,
|
|
130
|
+
if: options[:if]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
self.nav_registry = nav_registry.reject {|e| e[:key] == normalized_key }
|
|
134
|
+
self.nav_registry += [entry]
|
|
135
|
+
|
|
136
|
+
register_navigation_setting!(entry)
|
|
137
|
+
|
|
138
|
+
entry
|
|
139
|
+
end
|
|
140
|
+
|
|
97
141
|
VALID_PAGE_SECTIONS = %i[main settings].freeze
|
|
98
142
|
|
|
99
143
|
# Unified API to register an admin page: nav item + permission key in one call.
|
|
@@ -133,6 +177,45 @@ module RubyCms
|
|
|
133
177
|
[]
|
|
134
178
|
end
|
|
135
179
|
|
|
180
|
+
# Sidebar rows for a given section. Includes groups (accordions) and top-level links.
|
|
181
|
+
# Returns an array of:
|
|
182
|
+
# - { type: :link, entry: <nav entry hash> }
|
|
183
|
+
# - { type: :group, group: <group entry hash>, children: [<nav entry hash>...] }
|
|
184
|
+
def self.visible_nav_sidebar_rows(section:, view_context: nil, user: nil)
|
|
185
|
+
visible = visible_nav_registry(view_context:, user:)
|
|
186
|
+
links = visible.select {|e| e[:type].to_s == "link" }
|
|
187
|
+
groups = visible.select {|e| e[:type].to_s == "group" }
|
|
188
|
+
|
|
189
|
+
links_by_key = links.index_by {|e| e[:key].to_sym }
|
|
190
|
+
nested_keys = groups.flat_map {|g| Array(g[:children]) }.map(&:to_sym).to_set
|
|
191
|
+
|
|
192
|
+
section_key = section.to_s
|
|
193
|
+
section_name = (section_key == "settings" ? NAV_SECTION_BOTTOM : NAV_SECTION_MAIN)
|
|
194
|
+
|
|
195
|
+
# groups first; children order uses their own order/label.
|
|
196
|
+
group_rows = groups
|
|
197
|
+
.select {|g| g[:section].to_s == section_name }
|
|
198
|
+
.sort_by {|g| nav_sort_tuple(g) }
|
|
199
|
+
.filter_map do |g|
|
|
200
|
+
child_entries = Array(g[:children]).map {|k| links_by_key[k.to_sym] }.compact
|
|
201
|
+
child_entries = child_entries.sort_by {|c| [c[:order] || 1000, c[:label].to_s] }
|
|
202
|
+
next if child_entries.empty? && g[:path].blank?
|
|
203
|
+
{ type: :group, group: g, children: child_entries }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
link_rows = links
|
|
207
|
+
.select {|e| e[:section].to_s == section_name }
|
|
208
|
+
.reject {|e| nested_keys.include?(e[:key].to_sym) }
|
|
209
|
+
.sort_by {|e| nav_sort_tuple(e) }
|
|
210
|
+
.map {|e| { type: :link, entry: e } }
|
|
211
|
+
|
|
212
|
+
merged = (group_rows.map {|r| [r, nav_sort_tuple(r[:group])] } +
|
|
213
|
+
link_rows.map {|r| [r, nav_sort_tuple(r[:entry])] })
|
|
214
|
+
.sort_by {|(_, tuple)| tuple }
|
|
215
|
+
.map(&:first)
|
|
216
|
+
merged
|
|
217
|
+
end
|
|
218
|
+
|
|
136
219
|
module Nav
|
|
137
220
|
def self.register(key:, label:, path:, icon: nil, section: nil, order: nil, permission: nil, default_visible: true, **)
|
|
138
221
|
RubyCms.nav_register(
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_cms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.1.
|
|
4
|
+
version: 0.2.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Codebyjob
|
|
@@ -162,6 +162,7 @@ files:
|
|
|
162
162
|
- app/views/layouts/ruby_cms/_admin_sidebar.html.erb
|
|
163
163
|
- app/views/layouts/ruby_cms/admin.html.erb
|
|
164
164
|
- app/views/layouts/ruby_cms/minimal.html.erb
|
|
165
|
+
- app/views/ruby_cms/_tailwind_safelist.html.erb
|
|
165
166
|
- app/views/ruby_cms/admin/analytics/index.html.erb
|
|
166
167
|
- app/views/ruby_cms/admin/analytics/page_details.html.erb
|
|
167
168
|
- app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb
|