admin_suite 0.2.1 → 0.2.2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/admin_suite.css +128 -0
  3. data/app/controllers/admin_suite/application_controller.rb +31 -0
  4. data/app/controllers/admin_suite/dashboard_controller.rb +59 -226
  5. data/app/helpers/admin_suite/base_helper.rb +108 -108
  6. data/app/helpers/admin_suite/panels_helper.rb +1 -1
  7. data/app/javascript/controllers/admin_suite/file_upload_controller.js +9 -9
  8. data/app/javascript/controllers/admin_suite/json_editor_controller.js +8 -8
  9. data/app/javascript/controllers/admin_suite/searchable_select_controller.js +2 -2
  10. data/app/javascript/controllers/admin_suite/tag_select_controller.js +1 -1
  11. data/app/javascript/controllers/admin_suite/toggle_switch_controller.js +1 -1
  12. data/app/views/admin_suite/dashboard/index.html.erb +6 -15
  13. data/app/views/admin_suite/panels/_cards.html.erb +6 -6
  14. data/app/views/admin_suite/panels/_chart.html.erb +12 -12
  15. data/app/views/admin_suite/panels/_health.html.erb +14 -14
  16. data/app/views/admin_suite/panels/_recent.html.erb +11 -11
  17. data/app/views/admin_suite/panels/_stat.html.erb +24 -24
  18. data/app/views/admin_suite/panels/_table.html.erb +10 -10
  19. data/app/views/admin_suite/portals/show.html.erb +1 -1
  20. data/app/views/admin_suite/resources/_form.html.erb +1 -1
  21. data/app/views/admin_suite/resources/edit.html.erb +4 -4
  22. data/app/views/admin_suite/resources/index.html.erb +23 -23
  23. data/app/views/admin_suite/resources/new.html.erb +4 -4
  24. data/app/views/admin_suite/resources/show.html.erb +17 -17
  25. data/app/views/admin_suite/shared/_form.html.erb +8 -8
  26. data/app/views/admin_suite/shared/_json_editor_field.html.erb +4 -4
  27. data/app/views/admin_suite/shared/_sidebar.html.erb +4 -4
  28. data/app/views/admin_suite/shared/_topbar.html.erb +1 -1
  29. data/app/views/layouts/admin_suite/application.html.erb +4 -4
  30. data/docs/configuration.md +25 -0
  31. data/docs/portals.md +42 -0
  32. data/lib/admin_suite/configuration.rb +10 -0
  33. data/lib/admin_suite/engine.rb +9 -0
  34. data/lib/admin_suite/ui/field_renderer_registry.rb +2 -2
  35. data/lib/admin_suite/ui/form_field_renderer.rb +2 -2
  36. data/lib/admin_suite/ui/show_formatter_registry.rb +5 -5
  37. data/lib/admin_suite/ui/show_value_formatter.rb +1 -1
  38. data/lib/admin_suite/version.rb +1 -1
  39. data/lib/admin_suite.rb +31 -0
  40. data/test/dummy/log/test.log +1328 -440
  41. data/test/dummy/tmp/local_secret.txt +1 -1
  42. data/test/integration/dashboard_test.rb +57 -1
  43. metadata +1 -1
@@ -56,12 +56,12 @@
56
56
  <% end %>
57
57
  <% end %>
58
58
 
59
- <div class="flex flex-col lg:flex-row gap-6">
59
+ <div class="admin-suite-two-col flex flex-col md:flex-row gap-6">
60
60
  <!-- Filters Sidebar -->
61
61
  <% if resource_config.index_config&.filters_list&.any? || resource_config.index_config&.searchable_fields&.any? %>
62
- <div class="lg:w-64 flex-shrink-0">
62
+ <div class="admin-suite-two-col__sidebar admin-suite-two-col__sidebar--filters md:w-64 flex-shrink-0">
63
63
  <div class="bg-white rounded-xl border border-slate-200 p-4">
64
- <h3 class="font-medium text-slate-900 dark:text-white mb-4">Filters</h3>
64
+ <h3 class="font-medium text-slate-900 mb-4">Filters</h3>
65
65
  <%= form_with url: url_for(action: :index), method: :get,
66
66
  data: {
67
67
  turbo_frame: "resource_results",
@@ -136,7 +136,7 @@
136
136
  <% end %>
137
137
 
138
138
  <!-- Main Content -->
139
- <div class="flex-1">
139
+ <div class="flex-1 min-w-0">
140
140
  <%= turbo_frame_tag "resource_results", data: { turbo_action: "advance" } do %>
141
141
  <div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
142
142
  <% if @collection.any? %>
@@ -216,25 +216,25 @@
216
216
 
217
217
  <!-- Pagination -->
218
218
  <% if @pagy.pages > 1 %>
219
- <div class="px-6 py-4 border-t border-slate-200 dark:border-slate-700">
219
+ <div class="px-6 py-4 border-t border-slate-200">
220
220
  <div class="flex items-center justify-between">
221
- <div class="text-sm text-slate-500 dark:text-slate-400">
222
- Showing <span class="font-medium text-slate-700 dark:text-slate-300"><%= @pagy.from %></span> to
223
- <span class="font-medium text-slate-700 dark:text-slate-300"><%= @pagy.to %></span> of
224
- <span class="font-medium text-slate-700 dark:text-slate-300"><%= number_with_delimiter(@pagy.count) %></span> results
221
+ <div class="text-sm text-slate-500">
222
+ Showing <span class="font-medium text-slate-700"><%= @pagy.from %></span> to
223
+ <span class="font-medium text-slate-700"><%= @pagy.to %></span> of
224
+ <span class="font-medium text-slate-700"><%= number_with_delimiter(@pagy.count) %></span> results
225
225
  </div>
226
226
 
227
227
  <nav class="flex items-center gap-1">
228
228
  <% if @pagy.prev %>
229
229
  <%= link_to url_for(params.permit!.merge(page: @pagy.prev)),
230
- class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors" do %>
230
+ class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors" do %>
231
231
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
232
232
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
233
233
  </svg>
234
234
  Prev
235
235
  <% end %>
236
236
  <% else %>
237
- <span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 dark:text-slate-500 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg cursor-not-allowed">
237
+ <span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed">
238
238
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
239
239
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
240
240
  </svg>
@@ -245,10 +245,10 @@
245
245
  <div class="hidden sm:flex items-center gap-1 mx-2">
246
246
  <% @pagy.series.each do |item| %>
247
247
  <% if item == :gap %>
248
- <span class="px-2 py-1 text-sm text-slate-400 dark:text-slate-500">…</span>
248
+ <span class="px-2 py-1 text-sm text-slate-400">…</span>
249
249
  <% elsif item.is_a?(Integer) %>
250
250
  <%= link_to item, url_for(params.permit!.merge(page: item)),
251
- class: "px-3 py-1.5 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors" %>
251
+ class: "px-3 py-1.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors" %>
252
252
  <% elsif item.is_a?(String) %>
253
253
  <span class="px-3 py-1.5 text-sm font-medium text-white rounded-lg" style="background: var(--admin-suite-primary); border: 1px solid var(--admin-suite-primary);">
254
254
  <%= item %>
@@ -257,20 +257,20 @@
257
257
  <% end %>
258
258
  </div>
259
259
 
260
- <span class="sm:hidden px-3 py-1.5 text-sm text-slate-500 dark:text-slate-400">
260
+ <span class="sm:hidden px-3 py-1.5 text-sm text-slate-500">
261
261
  Page <%= @pagy.page %> of <%= @pagy.pages %>
262
262
  </span>
263
263
 
264
264
  <% if @pagy.next %>
265
265
  <%= link_to url_for(params.permit!.merge(page: @pagy.next)),
266
- class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors" do %>
266
+ class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors" do %>
267
267
  Next
268
268
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
269
269
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
270
270
  </svg>
271
271
  <% end %>
272
272
  <% else %>
273
- <span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 dark:text-slate-500 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg cursor-not-allowed">
273
+ <span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed">
274
274
  Next
275
275
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
276
276
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
@@ -281,22 +281,22 @@
281
281
  </div>
282
282
  </div>
283
283
  <% else %>
284
- <div class="px-6 py-3 border-t border-slate-200 dark:border-slate-700">
285
- <div class="text-sm text-slate-500 dark:text-slate-400">
286
- Showing <span class="font-medium text-slate-700 dark:text-slate-300"><%= @pagy.count %></span> results
284
+ <div class="px-6 py-3 border-t border-slate-200">
285
+ <div class="text-sm text-slate-500">
286
+ Showing <span class="font-medium text-slate-700"><%= @pagy.count %></span> results
287
287
  </div>
288
288
  </div>
289
289
  <% end %>
290
290
  <% else %>
291
291
  <div class="p-12 text-center">
292
- <div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-900 mb-4">
292
+ <div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-slate-100 mb-4">
293
293
  <svg class="w-6 h-6 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
294
294
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
295
295
  </svg>
296
296
  </div>
297
- <h3 class="text-lg font-medium text-slate-900 dark:text-white">No <%= resource_config.human_name_plural.downcase %> found</h3>
297
+ <h3 class="text-lg font-medium text-slate-900">No <%= resource_config.human_name_plural.downcase %> found</h3>
298
298
  <% if has_new_route %>
299
- <p class="text-slate-500 dark:text-slate-400 mt-1">Get started by creating your first one.</p>
299
+ <p class="text-slate-500 mt-1">Get started by creating your first one.</p>
300
300
  <%= link_to url_for(action: :new),
301
301
  class: theme_btn_primary_class + " mt-4",
302
302
  data: { turbo_frame: "_top" } do %>
@@ -304,7 +304,7 @@
304
304
  New <%= resource_config.human_name %>
305
305
  <% end %>
306
306
  <% else %>
307
- <p class="text-slate-500 dark:text-slate-400 mt-1">No records have been created yet.</p>
307
+ <p class="text-slate-500 mt-1">No records have been created yet.</p>
308
308
  <% end %>
309
309
  </div>
310
310
  <% end %>
@@ -3,18 +3,18 @@
3
3
  <div class="px-4 sm:px-6 lg:px-8 max-w-3xl mx-auto">
4
4
  <!-- Header -->
5
5
  <div class="mb-6">
6
- <nav class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 mb-2">
6
+ <nav class="flex items-center gap-2 text-sm text-slate-500 mb-2">
7
7
  <%= link_to "Dashboard", root_path, class: theme_link_hover_text_class %>
8
8
  <%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
9
9
  <%= link_to resource_config.human_name_plural, url_for(action: :index), class: theme_link_hover_text_class %>
10
10
  <%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
11
- <span class="text-slate-900 dark:text-white">New</span>
11
+ <span class="text-slate-900">New</span>
12
12
  </nav>
13
- <h1 class="text-2xl font-bold text-slate-900 dark:text-white">New <%= resource_config.human_name %></h1>
13
+ <h1 class="text-2xl font-bold text-slate-900">New <%= resource_config.human_name %></h1>
14
14
  </div>
15
15
 
16
16
  <!-- Form -->
17
- <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
17
+ <div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
18
18
  <div class="p-6">
19
19
  <%= render "admin_suite/shared/form", resource: @resource %>
20
20
  </div>
@@ -26,23 +26,23 @@
26
26
  <div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
27
27
  <!-- Header -->
28
28
  <div class="mb-6">
29
- <nav class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 mb-2">
29
+ <nav class="flex items-center gap-2 text-sm text-slate-500 mb-2">
30
30
  <%= link_to "Dashboard", root_path, class: theme_link_hover_text_class %>
31
31
  <%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
32
32
  <%= link_to resource_config.human_name_plural, url_for(action: :index), class: theme_link_hover_text_class %>
33
33
  <%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
34
- <span class="text-slate-900 dark:text-white">
34
+ <span class="text-slate-900">
35
35
  <%= resource.respond_to?(:name) ? resource.name.to_s.truncate(30) : "##{resource.id}" %>
36
36
  </span>
37
37
  </nav>
38
38
 
39
39
  <div class="flex items-center justify-between">
40
- <h1 class="text-2xl font-bold text-slate-900 dark:text-white">
40
+ <h1 class="text-2xl font-bold text-slate-900">
41
41
  <%= resource.respond_to?(:name) ? resource.name : "#{resource_config.human_name} ##{resource.id}" %>
42
42
  </h1>
43
43
  <div class="flex items-center gap-2">
44
44
  <% if has_edit_route %>
45
- <%= link_to url_for(action: :edit, id: resource.to_param), class: "inline-flex items-center gap-2 px-4 py-2 bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-900 dark:text-white text-sm font-medium rounded-lg transition-colors" do %>
45
+ <%= link_to url_for(action: :edit, id: resource.to_param), class: "inline-flex items-center gap-2 px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-900 text-sm font-medium rounded-lg transition-colors" do %>
46
46
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
47
47
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
48
48
  </svg>
@@ -128,24 +128,24 @@
128
128
  <% end %>
129
129
 
130
130
  <!-- Two Column Layout -->
131
- <div class="flex flex-col lg:flex-row gap-6">
131
+ <div class="admin-suite-two-col flex flex-col md:flex-row gap-6">
132
132
  <!-- Sidebar (Left Column) -->
133
133
  <% if sidebar_sections.any? %>
134
- <div class="lg:w-80 flex-shrink-0 space-y-6">
134
+ <div class="admin-suite-two-col__sidebar admin-suite-two-col__sidebar--details md:w-80 flex-shrink-0 space-y-6">
135
135
  <% sidebar_sections.each do |section| %>
136
136
  <%= render_show_section(resource, section, :sidebar) %>
137
137
  <% end %>
138
138
  </div>
139
139
  <% else %>
140
- <div class="lg:w-80 flex-shrink-0 space-y-6">
141
- <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
142
- <div class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50">
143
- <h3 class="font-medium text-slate-900 dark:text-white text-sm">Details</h3>
140
+ <div class="admin-suite-two-col__sidebar admin-suite-two-col__sidebar--details md:w-80 flex-shrink-0 space-y-6">
141
+ <div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
142
+ <div class="px-4 py-3 border-b border-slate-200 bg-slate-50">
143
+ <h3 class="font-medium text-slate-900 text-sm">Details</h3>
144
144
  </div>
145
145
  <div class="p-4 space-y-3">
146
146
  <div class="flex justify-between items-start">
147
- <span class="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">ID</span>
148
- <span class="text-sm font-mono text-slate-900 dark:text-white"><%= resource.id %></span>
147
+ <span class="text-xs font-medium text-slate-500 uppercase tracking-wider">ID</span>
148
+ <span class="text-sm font-mono text-slate-900"><%= resource.id %></span>
149
149
  </div>
150
150
  </div>
151
151
  </div>
@@ -159,18 +159,18 @@
159
159
  <%= render_show_section(resource, section, :main) %>
160
160
  <% end %>
161
161
  <% else %>
162
- <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
163
- <div class="px-6 py-4 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50">
164
- <h3 class="font-medium text-slate-900 dark:text-white">All Attributes</h3>
162
+ <div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
163
+ <div class="px-6 py-4 border-b border-slate-200 bg-slate-50">
164
+ <h3 class="font-medium text-slate-900">All Attributes</h3>
165
165
  </div>
166
166
  <div class="p-6">
167
167
  <dl class="space-y-6">
168
168
  <% resource.attributes.except("id", "created_at", "updated_at").each do |attr, _value| %>
169
169
  <div>
170
- <dt class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">
170
+ <dt class="text-sm font-medium text-slate-500 mb-2">
171
171
  <%= attr.humanize %>
172
172
  </dt>
173
- <dd class="text-sm text-slate-900 dark:text-white">
173
+ <dd class="text-sm text-slate-900">
174
174
  <%= format_show_value(resource, attr) %>
175
175
  </dd>
176
176
  </div>
@@ -2,16 +2,16 @@
2
2
 
3
3
  <%= form_with model: resource, url: resource.new_record? ? url_for(action: :create) : url_for(action: :update, id: resource.to_param), local: true, class: "space-y-6" do |f| %>
4
4
  <% if resource.errors.any? %>
5
- <div class="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
5
+ <div class="p-4 bg-red-50 border border-red-200 rounded-lg">
6
6
  <div class="flex items-start gap-3">
7
7
  <svg class="w-5 h-5 text-red-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
8
8
  <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"/>
9
9
  </svg>
10
10
  <div>
11
- <h3 class="text-sm font-medium text-red-800 dark:text-red-200">
11
+ <h3 class="text-sm font-medium text-red-800">
12
12
  <%= pluralize(resource.errors.count, "error") %> prohibited this record from being saved:
13
13
  </h3>
14
- <ul class="mt-2 text-sm text-red-700 dark:text-red-300 list-disc list-inside">
14
+ <ul class="mt-2 text-sm text-red-700 list-disc list-inside">
15
15
  <% resource.errors.full_messages.each do |message| %>
16
16
  <li><%= message %></li>
17
17
  <% end %>
@@ -24,10 +24,10 @@
24
24
  <% resource_config.form_config&.fields_list&.each do |field| %>
25
25
  <% case field %>
26
26
  <% when Admin::Base::Resource::SectionDefinition %>
27
- <div class="border-t border-slate-200 dark:border-slate-700 pt-6 mt-6">
28
- <h3 class="text-lg font-medium text-slate-900 dark:text-white mb-1"><%= field.title %></h3>
27
+ <div class="border-t border-slate-200 pt-6 mt-6">
28
+ <h3 class="text-lg font-medium text-slate-900 mb-1"><%= field.title %></h3>
29
29
  <% if field.description.present? %>
30
- <p class="text-sm text-slate-500 dark:text-slate-400 mb-4"><%= field.description %></p>
30
+ <p class="text-sm text-slate-500 mb-4"><%= field.description %></p>
31
31
  <% end %>
32
32
  </div>
33
33
  <% when Admin::Base::Resource::SectionEnd %>
@@ -51,8 +51,8 @@
51
51
  <% end %>
52
52
  <% end %>
53
53
 
54
- <div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-700">
55
- <%= link_to "Cancel", resource.new_record? ? url_for(action: :index) : url_for(action: :show, id: resource.to_param), class: "px-4 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors" %>
54
+ <div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200">
55
+ <%= link_to "Cancel", resource.new_record? ? url_for(action: :index) : url_for(action: :show, id: resource.to_param), class: "px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 rounded-lg transition-colors" %>
56
56
  <%= f.button class: theme_btn_primary_small_class, data: { disable_with: "Saving..." } do %>
57
57
  <%= resource.new_record? ? "Create #{resource_config.human_name}" : "Save Changes" %>
58
58
  <% end %>
@@ -18,9 +18,9 @@
18
18
 
19
19
  <div data-controller="admin-suite--json-editor">
20
20
  <% if field.help.present? %>
21
- <p class="text-sm text-slate-500 dark:text-slate-400 mb-2"><%= field.help %></p>
21
+ <p class="text-sm text-slate-500 mb-2"><%= field.help %></p>
22
22
  <% else %>
23
- <p class="text-sm text-slate-500 dark:text-slate-400 mb-2">Enter valid JSON. Use the Format button to pretty-print.</p>
23
+ <p class="text-sm text-slate-500 mb-2">Enter valid JSON. Use the Format button to pretty-print.</p>
24
24
  <% end %>
25
25
 
26
26
  <div class="relative">
@@ -38,7 +38,7 @@
38
38
  <button
39
39
  type="button"
40
40
  data-action="click->admin-suite--json-editor#format"
41
- class="px-3 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 rounded transition-colors"
41
+ class="px-3 py-1.5 text-xs font-medium text-slate-700 bg-slate-100 hover:bg-slate-200 rounded transition-colors"
42
42
  >
43
43
  Format JSON
44
44
  </button>
@@ -47,6 +47,6 @@
47
47
 
48
48
  <div
49
49
  data-admin-suite--json-editor-target="error"
50
- class="hidden mt-2 text-sm text-red-600 dark:text-red-400"
50
+ class="hidden mt-2 text-sm text-red-600"
51
51
  ></div>
52
52
  </div>
@@ -44,13 +44,13 @@
44
44
  <!-- Portal Navigation -->
45
45
  <nav class="flex-1 overflow-y-auto px-3 py-4 space-y-2">
46
46
  <% sorted_portals.each do |portal_key, portal| %>
47
- <% portal_home = portal_path(portal: portal_key) %>
48
- <% portal_prefix = File.join(request.script_name.to_s, portal_key.to_s) %>
49
- <% active = request.path.start_with?(portal_prefix) %>
47
+ <% portal_home = portal_path(portal: portal_key).to_s %>
48
+ <% portal_prefix = portal_home.sub(%r{/*\z}, "") %>
49
+ <% active = portal_prefix.present? && (request.path == portal_prefix || request.path.start_with?("#{portal_prefix}/")) %>
50
50
  <% color = portal_color(portal_key) %>
51
51
 
52
52
  <div>
53
- <%= link_to (portal_home || root_path),
53
+ <%= link_to (portal_prefix.presence || root_path),
54
54
  class: "flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors #{active ? "bg-white/20 text-white" : "text-white/90 hover:bg-white/10 hover:text-white"}" do %>
55
55
  <div class="admin-suite-portal-accent admin-suite-portal-accent--<%= color %> admin-suite-portal-chip flex items-center justify-center w-8 h-8 rounded-lg <%= active ? "" : "bg-white/10" %>">
56
56
  <%= portal_icon(portal_key, class: "admin-suite-portal-icon w-4 h-4") %>
@@ -6,7 +6,7 @@
6
6
  <div class="flex items-center gap-4">
7
7
  <!-- Mobile menu button -->
8
8
  <button type="button"
9
- class="lg:hidden p-2 rounded-lg text-slate-500 hover:bg-slate-100"
9
+ class="admin-suite-mobile-menu-button lg:hidden p-2 rounded-lg text-slate-500 hover:bg-slate-100"
10
10
  data-action="click->admin-suite--sidebar#toggle">
11
11
  <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12
12
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
@@ -41,24 +41,24 @@
41
41
  <body class="admin-suite bg-slate-50">
42
42
  <div class="flex h-screen overflow-hidden" data-controller="admin-suite--sidebar">
43
43
  <!-- Sidebar -->
44
- <div class="hidden lg:flex lg:flex-shrink-0">
44
+ <div class="admin-suite-desktop-sidebar lg:flex lg:flex-shrink-0">
45
45
  <%= render "admin_suite/shared/sidebar" %>
46
46
  </div>
47
47
 
48
48
  <!-- Mobile sidebar overlay -->
49
49
  <div data-admin-suite--sidebar-target="overlay"
50
- class="hidden fixed inset-0 z-40 bg-slate-600 bg-opacity-75 lg:hidden"
50
+ class="admin-suite-mobile-overlay hidden fixed inset-0 z-40 bg-slate-600 bg-opacity-75 lg:hidden"
51
51
  data-action="click->admin-suite--sidebar#close">
52
52
  </div>
53
53
 
54
54
  <!-- Mobile sidebar -->
55
55
  <div data-admin-suite--sidebar-target="mobileSidebar"
56
- class="hidden fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out lg:hidden">
56
+ class="admin-suite-mobile-sidebar hidden fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out lg:hidden">
57
57
  <%= render "admin_suite/shared/sidebar" %>
58
58
  </div>
59
59
 
60
60
  <!-- Main content area -->
61
- <div class="flex flex-col flex-1 overflow-hidden">
61
+ <div class="admin-suite-main flex flex-col flex-1 overflow-hidden">
62
62
  <!-- Top bar -->
63
63
  <%= render "admin_suite/shared/topbar" %>
64
64
 
@@ -39,6 +39,11 @@ These are the defaults in `AdminSuite::Configuration` / `AdminSuite::Engine`:
39
39
  - `Rails.root/config/admin_suite/portals/*.rb`
40
40
  - `Rails.root/app/admin/portals/*.rb`
41
41
  - `Rails.root/app/admin_suite/portals/*.rb`
42
+ - `dashboard_globs`: defaults to:
43
+ - `Rails.root/config/admin_suite/dashboard.rb`
44
+ - `Rails.root/config/admin_suite/dashboard/*.rb`
45
+ - `Rails.root/app/admin_suite/dashboard.rb`
46
+ - `Rails.root/app/admin_suite/dashboard/*.rb`
42
47
 
43
48
  Note: AdminSuite definition files (resources, actions, portals) often don't follow
44
49
  Zeitwerk's path-to-constant naming conventions. To prevent eager-load `Zeitwerk::NameError`s
@@ -126,6 +131,26 @@ Where AdminSuite should load portal definition files from (files typically call
126
131
 
127
132
  - **Type**: `Array<String>`
128
133
 
134
+ ### `dashboard_globs`
135
+
136
+ Where AdminSuite should load the root dashboard definition file(s) from (files typically call `AdminSuite.root_dashboard { ... }`).
137
+
138
+ - **Type**: `Array<String>`
139
+
140
+ ### `root_dashboard_title`
141
+
142
+ Optional title shown on the root dashboard.
143
+
144
+ - **Type**: `String`, `Proc`, or `nil`
145
+ - **Proc signature**: `->(controller) { "Admin Suite" }`
146
+
147
+ ### `root_dashboard_description`
148
+
149
+ Optional description shown on the root dashboard.
150
+
151
+ - **Type**: `String`, `Proc`, or `nil`
152
+ - **Proc signature**: `->(controller) { "..." }`
153
+
129
154
  ### `portals`
130
155
 
131
156
  Portal metadata used for navigation (label/icon/color/order). This is separate from the portal DSL and can be used alone.
data/docs/portals.md CHANGED
@@ -96,3 +96,45 @@ Example:
96
96
  - `/internal/admin/ops`
97
97
  - `/internal/admin/ai`
98
98
 
99
+ ## Root dashboard (`AdminSuite.root_dashboard`)
100
+
101
+ The engine root (`/`, relative to the mount path) renders a default dashboard (portal cards + basic stats).
102
+
103
+ To customize it, create a dashboard definition file (loaded from `AdminSuite.config.dashboard_globs`, which defaults to paths like `config/admin_suite/dashboard.rb` and `app/admin_suite/dashboard.rb`):
104
+
105
+ ```ruby
106
+ # config/admin_suite/dashboard.rb
107
+ AdminSuite.configure do |config|
108
+ config.root_dashboard_title = "Developer Portal"
109
+ config.root_dashboard_description = "Internal tools for managing application resources."
110
+ end
111
+
112
+ AdminSuite.root_dashboard do
113
+ row do
114
+ cards_panel "Portals",
115
+ span: 12,
116
+ variant: :portals,
117
+ resources: ->(view) do
118
+ view.navigation_items
119
+ .sort_by { |(_k, meta)| (meta[:order] || 100).to_i }
120
+ .map do |portal_key, portal|
121
+ {
122
+ key: portal_key,
123
+ label: portal[:label] || portal_key.to_s.humanize,
124
+ description: portal[:description],
125
+ color: view.portal_color(portal_key),
126
+ icon: portal[:icon],
127
+ path: view.portal_path(portal: portal_key),
128
+ count: (portal[:sections] || {}).values.sum { |s| Array(s[:items]).size }
129
+ }
130
+ end
131
+ end
132
+ end
133
+
134
+ row do
135
+ stat_panel "Portals", ->(view) { view.navigation_items.keys.count }, span: 6, variant: :mini, color: :slate
136
+ stat_panel "Resources", -> { Admin::Base::Resource.registered_resources.count }, span: 6, variant: :mini, color: :slate
137
+ end
138
+ end
139
+ ```
140
+
@@ -9,6 +9,7 @@ module AdminSuite
9
9
  :resource_globs,
10
10
  :action_globs,
11
11
  :portal_globs,
12
+ :dashboard_globs,
12
13
  :portals,
13
14
  :custom_renderers,
14
15
  :icon_renderer,
@@ -18,6 +19,10 @@ module AdminSuite
18
19
  :theme,
19
20
  :host_stylesheet,
20
21
  :tailwind_cdn,
22
+ :root_dashboard_title,
23
+ :root_dashboard_description,
24
+ :root_dashboard_definition,
25
+ :root_dashboard_loaded,
21
26
  :on_action_executed,
22
27
  :resolve_action_handler
23
28
 
@@ -28,6 +33,7 @@ module AdminSuite
28
33
  @resource_globs = []
29
34
  @action_globs = []
30
35
  @portal_globs = []
36
+ @dashboard_globs = []
31
37
  @portals = {}
32
38
  @custom_renderers = {}
33
39
  @icon_renderer = nil
@@ -37,6 +43,10 @@ module AdminSuite
37
43
  @theme = { primary: :indigo, secondary: :purple }
38
44
  @host_stylesheet = nil
39
45
  @tailwind_cdn = true
46
+ @root_dashboard_title = nil
47
+ @root_dashboard_description = nil
48
+ @root_dashboard_definition = nil
49
+ @root_dashboard_loaded = false
40
50
  @on_action_executed = nil
41
51
  @resolve_action_handler = nil
42
52
  end
@@ -141,6 +141,15 @@ module AdminSuite
141
141
  ]
142
142
  end
143
143
 
144
+ if config.dashboard_globs.blank?
145
+ config.dashboard_globs = [
146
+ Rails.root.join("config/admin_suite/dashboard.rb").to_s,
147
+ Rails.root.join("config/admin_suite/dashboard/*.rb").to_s,
148
+ Rails.root.join("app/admin_suite/dashboard.rb").to_s,
149
+ Rails.root.join("app/admin_suite/dashboard/*.rb").to_s
150
+ ]
151
+ end
152
+
144
153
  config.portals = {
145
154
  ops: { label: "Ops Portal", icon: "settings", color: :amber, order: 10 },
146
155
  email: { label: "Email Portal", icon: "inbox", color: :emerald, order: 20 },
@@ -75,11 +75,11 @@ AdminSuite::UI::FieldRendererRegistry.register(:attachment) do |view, f, field,
75
75
  end
76
76
 
77
77
  AdminSuite::UI::FieldRendererRegistry.register(:trix) do |_view, f, field, resource, _field_class|
78
- f.rich_text_area(field.name, class: "prose dark:prose-invert max-w-none")
78
+ f.rich_text_area(field.name, class: "prose max-w-none")
79
79
  end
80
80
 
81
81
  AdminSuite::UI::FieldRendererRegistry.register(:rich_text) do |_view, f, field, resource, _field_class|
82
- f.rich_text_area(field.name, class: "prose dark:prose-invert max-w-none")
82
+ f.rich_text_area(field.name, class: "prose max-w-none")
83
83
  end
84
84
 
85
85
  AdminSuite::UI::FieldRendererRegistry.register(:markdown) do |_view, f, field, resource, field_class|
@@ -38,8 +38,8 @@ module AdminSuite
38
38
 
39
39
  concat(field_html)
40
40
 
41
- concat(content_tag(:p, field.help, class: "mt-1 text-sm text-slate-500 dark:text-slate-400")) if field.help.present?
42
- concat(content_tag(:p, resource.errors[field.name].first, class: "mt-1 text-sm text-red-600 dark:text-red-400")) if resource.errors[field.name].any?
41
+ concat(content_tag(:p, field.help, class: "mt-1 text-sm text-slate-500")) if field.help.present?
42
+ concat(content_tag(:p, resource.errors[field.name].first, class: "mt-1 text-sm text-red-600")) if resource.errors[field.name].any?
43
43
  end)
44
44
  end
45
45
  end
@@ -40,7 +40,7 @@ end
40
40
  AdminSuite::UI::ShowFormatterRegistry.register_class(TrueClass) do |_value, view, _record, _field|
41
41
  view.content_tag(:span, class: "inline-flex items-center gap-1") do
42
42
  view.concat(view.admin_suite_icon("check-circle-2", class: "w-4 h-4 text-green-500"))
43
- view.concat(view.content_tag(:span, "Yes", class: "text-green-600 dark:text-green-400 font-medium"))
43
+ view.concat(view.content_tag(:span, "Yes", class: "text-green-600 font-medium"))
44
44
  end
45
45
  end
46
46
 
@@ -54,14 +54,14 @@ end
54
54
  AdminSuite::UI::ShowFormatterRegistry.register_class(Time) do |value, view, _record, _field|
55
55
  view.content_tag(:span, class: "inline-flex items-center gap-2") do
56
56
  view.concat(view.content_tag(:span, value.strftime("%B %d, %Y at %H:%M"), class: "font-medium"))
57
- view.concat(view.content_tag(:span, "(#{view.time_ago_in_words(value)} ago)", class: "text-slate-500 dark:text-slate-400 text-xs"))
57
+ view.concat(view.content_tag(:span, "(#{view.time_ago_in_words(value)} ago)", class: "text-slate-500 text-xs"))
58
58
  end
59
59
  end
60
60
 
61
61
  AdminSuite::UI::ShowFormatterRegistry.register_class(DateTime) do |value, view, _record, _field|
62
62
  view.content_tag(:span, class: "inline-flex items-center gap-2") do
63
63
  view.concat(view.content_tag(:span, value.strftime("%B %d, %Y at %H:%M"), class: "font-medium"))
64
- view.concat(view.content_tag(:span, "(#{view.time_ago_in_words(value)} ago)", class: "text-slate-500 dark:text-slate-400 text-xs"))
64
+ view.concat(view.content_tag(:span, "(#{view.time_ago_in_words(value)} ago)", class: "text-slate-500 text-xs"))
65
65
  end
66
66
  end
67
67
 
@@ -72,7 +72,7 @@ end
72
72
  if defined?(ActiveRecord::Base)
73
73
  AdminSuite::UI::ShowFormatterRegistry.register_class(ActiveRecord::Base) do |value, view, _record, _field|
74
74
  link_text = value.respond_to?(:name) ? value.name : "#{value.class.name} ##{value.id}"
75
- view.content_tag(:span, link_text, class: "text-indigo-600 dark:text-indigo-400")
75
+ view.content_tag(:span, link_text, class: "text-indigo-600")
76
76
  end
77
77
  end
78
78
 
@@ -88,7 +88,7 @@ AdminSuite::UI::ShowFormatterRegistry.register_class(Array) do |value, view, _re
88
88
  else
89
89
  view.content_tag(:div, class: "flex flex-wrap gap-1") do
90
90
  value.each do |item|
91
- view.concat(view.content_tag(:span, item.to_s, class: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300"))
91
+ view.concat(view.content_tag(:span, item.to_s, class: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-700"))
92
92
  end
93
93
  end
94
94
  end
@@ -19,7 +19,7 @@ module AdminSuite
19
19
  else
20
20
  simple_format(value.to_s)
21
21
  end
22
- return content_tag(:div, rendered, class: "prose dark:prose-invert max-w-none")
22
+ return content_tag(:div, rendered, class: "prose max-w-none")
23
23
  when :json
24
24
  begin
25
25
  parsed =
@@ -2,7 +2,7 @@
2
2
 
3
3
  module AdminSuite
4
4
  module Version
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
 
8
8
  # Backward-compatible constant.