panda-core 0.2.4 → 0.6.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/tailwind/application.css +199 -7
  3. data/app/assets/tailwind/tailwind.config.js +8 -0
  4. data/app/components/panda/core/UI/badge.rb +107 -0
  5. data/app/components/panda/core/UI/button.rb +110 -0
  6. data/app/components/panda/core/UI/card.rb +88 -0
  7. data/app/components/panda/core/admin/breadcrumb_component.rb +133 -0
  8. data/app/components/panda/core/admin/button_component.rb +46 -28
  9. data/app/components/panda/core/admin/container_component.rb +75 -4
  10. data/app/components/panda/core/admin/file_gallery_component.rb +157 -0
  11. data/app/components/panda/core/admin/flash_message_component.rb +98 -15
  12. data/app/components/panda/core/admin/form_error_component.rb +48 -0
  13. data/app/components/panda/core/admin/form_input_component.rb +50 -0
  14. data/app/components/panda/core/admin/form_select_component.rb +68 -0
  15. data/app/components/panda/core/admin/heading_component.rb +53 -24
  16. data/app/components/panda/core/admin/page_header_component.rb +107 -0
  17. data/app/components/panda/core/admin/panel_component.rb +33 -4
  18. data/app/components/panda/core/admin/slideover_component.rb +66 -4
  19. data/app/components/panda/core/admin/statistics_component.rb +19 -0
  20. data/app/components/panda/core/admin/tab_bar_component.rb +101 -0
  21. data/app/components/panda/core/admin/table_component.rb +92 -11
  22. data/app/components/panda/core/admin/tag_component.rb +58 -16
  23. data/app/components/panda/core/admin/user_activity_component.rb +43 -0
  24. data/app/components/panda/core/admin/user_display_component.rb +77 -0
  25. data/app/components/panda/core/base.rb +122 -0
  26. data/app/controllers/panda/core/admin/base_controller.rb +68 -0
  27. data/app/controllers/panda/core/admin/dashboard_controller.rb +5 -3
  28. data/app/controllers/panda/core/admin/my_profile_controller.rb +4 -4
  29. data/app/controllers/panda/core/admin/sessions_controller.rb +15 -8
  30. data/app/controllers/panda/core/admin/test_sessions_controller.rb +60 -0
  31. data/app/helpers/panda/core/asset_helper.rb +31 -5
  32. data/app/helpers/panda/core/sessions_helper.rb +27 -2
  33. data/app/javascript/panda/core/application.js +8 -1
  34. data/app/javascript/panda/core/controllers/alert_controller.js +38 -0
  35. data/app/javascript/panda/core/controllers/index.js +3 -3
  36. data/app/javascript/panda/core/controllers/toggle_controller.js +41 -0
  37. data/app/javascript/panda/core/tailwindplus-elements.js +31 -0
  38. data/app/javascript/panda/core/vendor/@hotwired--stimulus.js +4 -0
  39. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +160 -0
  40. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +4 -0
  41. data/app/models/panda/core/user.rb +61 -14
  42. data/app/services/panda/core/attach_avatar_service.rb +67 -0
  43. data/app/views/layouts/panda/core/admin.html.erb +40 -3
  44. data/app/views/layouts/panda/core/admin_simple.html.erb +6 -0
  45. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +4 -4
  46. data/app/views/panda/core/admin/dashboard/show.html.erb +2 -2
  47. data/app/views/panda/core/admin/my_profile/edit.html.erb +36 -25
  48. data/app/views/panda/core/admin/sessions/new.html.erb +9 -10
  49. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +27 -34
  50. data/app/views/panda/core/admin/shared/_flash.html.erb +4 -30
  51. data/app/views/panda/core/admin/shared/_sidebar.html.erb +41 -20
  52. data/app/views/panda/core/shared/_header.html.erb +13 -5
  53. data/config/importmap.rb +19 -6
  54. data/config/routes.rb +10 -3
  55. data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
  56. data/db/migrate/20250811120000_add_oauth_avatar_url_to_panda_core_users.rb +7 -0
  57. data/lib/panda/core/asset_loader.rb +23 -8
  58. data/lib/panda/core/configuration.rb +12 -9
  59. data/lib/panda/core/debug.rb +47 -0
  60. data/lib/panda/core/engine.rb +55 -9
  61. data/lib/panda/core/services/base_service.rb +19 -4
  62. data/lib/panda/core/version.rb +1 -1
  63. data/lib/panda/core.rb +2 -0
  64. data/lib/tasks/panda_core_users.rake +158 -0
  65. metadata +103 -14
  66. data/app/components/panda/core/admin/container_component.html.erb +0 -12
  67. data/app/components/panda/core/admin/flash_message_component.html.erb +0 -31
  68. data/app/components/panda/core/admin/panel_component.html.erb +0 -7
  69. data/app/components/panda/core/admin/slideover_component.html.erb +0 -9
  70. data/app/components/panda/core/admin/table_component.html.erb +0 -29
  71. data/app/controllers/panda/core/admin_controller.rb +0 -30
@@ -1,5 +1,42 @@
1
- <%= render "panda/core/shared/header", html_class: "h-full", body_class: "bg-gradient-admin" %>
2
- <div class="flex flex-col items-center justify-center min-h-screen px-4">
3
- <%= yield %>
1
+ <%= render "panda/core/shared/header", html_class: "h-full bg-white" %>
2
+ <div class="flex h-full" id="panda-container">
3
+ <div class="absolute top-0 w-full lg:flex lg:fixed lg:inset-y-0 lg:z-50 lg:flex-col lg:w-72">
4
+ <div class="flex overflow-y-auto flex-col gap-y-5 px-4 pb-4 max-h-16 bg-gradient-admin lg:max-h-full grow" data-transition-enter="transition-all ease-in-out duration-300" data-transition-enter-from="m-h-16" data-transition-enter-to="max-h-full" data-transition-leave="transition-all ease-in-out duration-300" data-transition-leave-from="max-h-full" data-transition-leave-to="max-h-16">
5
+ <%= render "panda/core/admin/shared/sidebar" %>
6
+ </div>
7
+ </div>
8
+ <div class="flex flex-col flex-1 mt-16 ml-0 lg:mt-0 lg:ml-72" id="panda-inner-container" <% if content_for :sidebar %> data-controller="toggle" data-action="keydown.esc->modal#close" tabindex="-1"<% end %>>
9
+ <section id="panda-main" class="flex flex-row h-full">
10
+ <div class="flex-1 h-full" id="panda-primary-content">
11
+ <%= render "panda/core/admin/shared/breadcrumbs" %>
12
+ <%= render "panda/core/admin/shared/flash" %>
13
+ <%= yield %>
14
+ </div>
15
+ <% if content_for :sidebar %>
16
+ <div data-toggle-target="toggleable" class="hidden flex absolute right-0 flex-col h-full bg-white divide-y divide-gray-200 shadow-xl basis-3/12"
17
+ data-transition-enter="transform transition ease-in-out duration-500"
18
+ data-transition-enter-from="translate-x-full"
19
+ data-transition-enter-to="translate-x-0"
20
+ data-transition-leave="transform transition ease-in-out duration-500"
21
+ data-transition-leave-from="translate-x-full"
22
+ data-transition-leave-to="translate-x-0"
23
+ id="slideover">
24
+ <div class="overflow-y-auto flex-1 h-0">
25
+ <div class="py-3 px-4 mb-4 bg-black">
26
+ <div class="flex justify-between items-center">
27
+ <h2 class="text-base font-semibold leading-6 text-white" id="slideover-title"><i class="mr-2 fa-solid fa-gear"></i> <%= yield :sidebar_title %> </h2>
28
+ <button type="button" data-action="click->toggle#toggle touch->toggle#toggle"><i class="font-bold text-white fa-solid fa-xmark right"></i></button>
29
+ </div>
30
+ </div>
31
+ <div class="flex flex-col flex-1 justify-between">
32
+ <div class="px-4 space-y-6">
33
+ <%= yield :sidebar %>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ <% end %>
39
+ </section>
40
+ </div>
4
41
  </div>
5
42
  <%= render "panda/core/shared/footer" %>
@@ -0,0 +1,6 @@
1
+ <%= render "panda/core/shared/header", html_class: "h-full", body_class: "bg-gradient-admin" %>
2
+ <%= render "panda/core/admin/shared/flash" %>
3
+ <div class="flex flex-col items-center justify-center min-h-screen px-4">
4
+ <%= yield %>
5
+ </div>
6
+ <%= render "panda/core/shared/footer" %>
@@ -10,7 +10,7 @@
10
10
  <div class="px-4 py-5 sm:p-6">
11
11
  <div class="flex items-center">
12
12
  <div class="flex-shrink-0">
13
- <i class="fa-regular fa-file-lines text-3xl text-gray-400"></i>
13
+ <i class="fa-solid fa-file-lines text-3xl text-gray-400"></i>
14
14
  </div>
15
15
  <div class="ml-5 w-0 flex-1">
16
16
  <dt class="text-sm font-medium text-gray-500 truncate">Content Management</dt>
@@ -46,8 +46,8 @@
46
46
  </div>
47
47
 
48
48
  <% # Hook for additional dashboard cards %>
49
- <% if Panda::Core.configuration.respond_to?(:admin_dashboard_cards) %>
50
- <% cards = Panda::Core.configuration.admin_dashboard_cards&.call(current_user) %>
49
+ <% if Panda::Core.config.respond_to?(:admin_dashboard_cards) %>
50
+ <% cards = Panda::Core.config.admin_dashboard_cards&.call(current_user) %>
51
51
  <% cards&.each do |card| %>
52
52
  <div class="bg-white overflow-hidden shadow rounded-lg">
53
53
  <div class="px-4 py-5 sm:p-6">
@@ -70,4 +70,4 @@
70
70
  <% end %>
71
71
  <% end %>
72
72
  </div>
73
- </div>
73
+ </div>
@@ -3,8 +3,8 @@
3
3
  <% container.with_heading(text: "Dashboard", level: 1) %>
4
4
 
5
5
  <% # Hook for dashboard widgets %>
6
- <% if Panda::Core.configuration.admin_dashboard_widgets %>
7
- <% widgets = Panda::Core.configuration.admin_dashboard_widgets.call(current_user) %>
6
+ <% if Panda::Core.config.admin_dashboard_widgets %>
7
+ <% widgets = Panda::Core.config.admin_dashboard_widgets.call(current_user) %>
8
8
  <% if widgets && widgets.any? %>
9
9
  <div class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
10
10
  <% widgets.each do |widget| %>
@@ -1,49 +1,60 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "My Profile", level: 1) %>
2
+ <% component.heading(text: "My Profile", level: 1) %>
3
3
 
4
4
  <%= form_with model: user,
5
- url: admin_my_profile_path,
6
- method: :patch,
7
- local: true,
8
- data: { controller: "theme-form" } do |f| %>
9
- <% if user.errors.any? %>
10
- <div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
11
- <div class="text-sm text-red-600">
12
- <% user.errors.full_messages.each do |message| %>
13
- <p><%= message %></p>
5
+ url: admin_my_profile_path,
6
+ method: :patch,
7
+ local: true,
8
+ data: { controller: "theme-form" } do |f| %>
9
+ <%= render Panda::Core::Admin::FormErrorComponent.new(model: user) %>
10
+
11
+ <div class="space-y-6">
12
+ <!-- Avatar Section -->
13
+ <div class="col-span-full">
14
+ <%= f.label :avatar, "Profile Picture", class: "block text-sm/6 font-medium text-gray-900 dark:text-white" %>
15
+ <div class="mt-2 flex items-center gap-x-3">
16
+ <% if user.avatar.attached? %>
17
+ <%= image_tag main_app.url_for(user.avatar), alt: user.name, class: "w-48 h-48 max-w-48 rounded-full object-cover" %>
18
+ <% else %>
19
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="w-48 h-48 text-gray-300 dark:text-gray-500">
20
+ <path d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" clip-rule="evenodd" fill-rule="evenodd" />
21
+ </svg>
14
22
  <% end %>
23
+ <label for="<%= f.field_id(:avatar) %>" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 cursor-pointer dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20">
24
+ Change
25
+ </label>
26
+ <%= f.file_field :avatar,
27
+ accept: "image/png,image/jpeg,image/jpg,image/gif,image/webp",
28
+ class: "sr-only" %>
15
29
  </div>
16
- </div>
17
- <% end %>
18
-
19
- <div class="space-y-4">
20
- <div class="field">
21
- <%= f.label :firstname, "First Name", class: "block text-sm font-medium text-gray-700" %>
22
- <%= f.text_field :firstname, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" %>
30
+ <% if user.avatar.attached? %>
31
+ <p class="mt-2 text-xs text-gray-600 dark:text-gray-400">
32
+ Current: <%= user.avatar.filename %> (<%= number_to_human_size(user.avatar.byte_size) %>)
33
+ </p>
34
+ <% end %>
23
35
  </div>
24
36
 
25
37
  <div class="field">
26
- <%= f.label :lastname, "Last Name", class: "block text-sm font-medium text-gray-700" %>
27
- <%= f.text_field :lastname, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" %>
38
+ <%= f.label :name %>
39
+ <%= f.text_field :name %>
28
40
  </div>
29
41
 
30
42
  <div class="field">
31
- <%= f.label :email, class: "block text-sm font-medium text-gray-700" %>
32
- <%= f.email_field :email, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" %>
43
+ <%= f.label :email %>
44
+ <%= f.email_field :email %>
33
45
  </div>
34
46
 
35
47
  <div class="field">
36
- <%= f.label :current_theme, "Theme", class: "block text-sm font-medium text-gray-700" %>
48
+ <%= f.label :current_theme, "Theme" %>
37
49
  <%= f.select :current_theme,
38
- options_for_select(Panda::Core.configuration.available_themes || [["Default", "default"], ["Sky", "sky"]], user.current_theme),
50
+ options_for_select(Panda::Core.config.available_themes || [["Default", "default"], ["Sky", "sky"]], user.current_theme),
39
51
  {},
40
- class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
41
52
  data: { action: "change->theme-form#updateTheme" } %>
42
53
  </div>
43
54
  </div>
44
55
 
45
56
  <%= f.submit "Update Profile",
46
- class: "btn btn-primary mt-6",
57
+ class: "btn btn-primary mt-4",
47
58
  data: { disable_with: "Saving..." } %>
48
59
  <% end %>
49
60
  <% end %>
@@ -1,23 +1,22 @@
1
1
  <div class="flex flex-col justify-center py-12 px-6 min-h-full text-center lg:px-8">
2
2
  <div class="text-center sm:mx-auto sm:w-full sm:max-w-sm">
3
- <% if Panda::Core.configuration.login_logo_path %>
4
- <img src="<%= Panda::Core.configuration.login_logo_path %>" class="py-2 mx-auto w-auto h-32">
3
+ <% if Panda::Core.config.login_logo_path %>
4
+ <img src="<%= Panda::Core.config.login_logo_path %>" class="py-2 mx-auto w-auto h-32">
5
5
  <% end %>
6
6
  <h2 class="mt-10 mb-6 text-2xl font-bold text-center text-white">
7
- <%= Panda::Core.configuration.login_page_title || "Sign in to your account" %>
7
+ <%= Panda::Core.config.login_page_title || "Sign in to your account" %>
8
8
  </h2>
9
9
  </div>
10
- <% if @providers&.any? || Panda::Core.configuration.authentication_providers.any? %>
11
- <% providers = @providers || Panda::Core.configuration.authentication_providers.keys %>
10
+ <% if @providers&.any? || Panda::Core.config.authentication_providers.any? %>
11
+ <% providers = @providers || Panda::Core.config.authentication_providers.keys %>
12
12
  <% providers.each do |provider| %>
13
- <% provider_config = Panda::Core.configuration.authentication_providers[provider] %>
14
- <% provider_name = provider_config&.dig(:name) || provider.to_s.humanize %>
13
+ <% provider_config = Panda::Core.config.authentication_providers[provider] %>
15
14
  <% provider_path = provider_config&.dig(:path_name) || provider %>
16
15
  <div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
17
- <%= form_tag "#{Panda::Core.configuration.admin_path}/auth/#{provider_path}", method: "post", data: {turbo: false} do %>
16
+ <%= form_tag "#{Panda::Core.config.admin_path}/auth/#{provider_path}", method: "post", data: {turbo: false} do %>
18
17
  <button type="submit" id="button-sign-in-<%= provider_path %>" class="inline-flex gap-x-2 items-center py-2.5 px-3.5 mx-auto mb-4 bg-white text-gray-900 rounded-md border min-w-56 border-neutral-400 hover:bg-gray-50">
19
- <i class="fa-brands fa-<%= oauth_provider_icon(provider) %> text-xl mr-1"></i>
20
- Sign in with <%= provider_name %>
18
+ <i class="<%= oauth_provider_non_brand?(provider) ? 'fa-solid' : 'fa-brands' %> fa-<%= oauth_provider_icon(provider) %> text-xl mr-1"></i>
19
+ Sign in with <%= oauth_provider_name(provider, provider_config) %>
21
20
  </button>
22
21
  <% end %>
23
22
  </div>
@@ -1,35 +1,28 @@
1
- <% if defined?(@breadcrumbs) && @breadcrumbs.any? %>
2
- <div class="flex">
3
- <nav class="flex-1 px-4 py-3 text-gray-700 border-b border-gray-200 bg-gray-50" aria-label="Breadcrumb" id="panda-breadcrumbs">
4
- <ol class="inline-flex items-center space-x-1 md:space-x-3">
5
- <% @breadcrumbs.each_with_index do |breadcrumb, index| %>
6
- <li class="inline-flex items-center">
7
- <% if index > 0 %>
8
- <svg class="w-3 h-3 text-gray-400 mx-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
9
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
10
- </svg>
11
- <% end %>
12
-
13
- <% if breadcrumb.path && index < @breadcrumbs.length - 1 %>
14
- <%= link_to breadcrumb.label, breadcrumb.path, class: "inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600" %>
15
- <% else %>
16
- <span class="text-sm font-medium text-gray-500"><%= breadcrumb.label %></span>
17
- <% end %>
18
- </li>
19
- <% end %>
20
- </ol>
21
- </nav>
22
-
23
- <% if content_for?(:sidebar) %>
24
- <div class="px-4 py-3 border-b border-gray-200 bg-gray-50 text-gray-700" tabindex="-1" data-controller="toggle">
25
- <a href="#" id="slideover-toggle" data-action="click->toggle#toggle touch->toggle#toggle" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600">
26
- <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
27
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
28
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
29
- </svg>
30
- <%= yield(:sidebar_title) || "Settings" %>
1
+ <% breadcrumb_styles = "text-black/60 hover:text-black/80" %>
2
+
3
+ <div class="flex mt-1 -mb-1">
4
+ <nav aria-label="Breadcrumb" id="panda-breadcrumbs" class="grow">
5
+ <ol role="list" class="px-4 w-full sm:px-6">
6
+ <li class="inline-block">
7
+ <a href="<%= Panda::Core.config.admin_path %>" class="<%= breadcrumb_styles %>">
8
+ <i class="fa fa-solid fa-house fa-fw inline-block w-4 text-center"></i>
9
+ <span class="sr-only">Home</span>
31
10
  </a>
32
- </div>
33
- <% end %>
34
- </div>
35
- <% end %>
11
+ </li>
12
+ <% breadcrumbs.each do |crumb| %>
13
+ <li class="inline-block">
14
+ <i class="fa-solid fa-chevron-right fa-fw inline-block w-4 text-center font-light <%= breadcrumb_styles %> px-2"></i>
15
+ <a href="<%= crumb.path %>" class="text-sm font-normal <%= breadcrumb_styles %>"><%= crumb.name %></a>
16
+ </li>
17
+ <% end %>
18
+ </ol>
19
+ </nav>
20
+
21
+ <% if content_for :sidebar %>
22
+ <div class="pt-4 pr-8 text-black/80" tabindex="-1">
23
+ <button type="button" id="slideover-toggle" data-action="click->toggle#toggle touch->toggle#toggle" class="text-sm font-light">
24
+ <i class="mr-1 fa-light fa-gear"></i> <%= yield :sidebar_title %>
25
+ </button>
26
+ </div>
27
+ <% end %>
28
+ </div>
@@ -1,31 +1,5 @@
1
1
  <% if flash.any? %>
2
- <div class="fixed top-4 right-4 z-50 space-y-2">
3
- <% flash.each do |type, message| %>
4
- <div class="bg-white rounded-lg shadow-lg p-4 max-w-sm" data-controller="flash" data-flash-delay-value="5000">
5
- <div class="flex items-start">
6
- <div class="flex-shrink-0">
7
- <% if type == "success" %>
8
- <i class="fa-regular fa-check-circle text-green-500"></i>
9
- <% elsif type == "error" %>
10
- <i class="fa-regular fa-times-circle text-red-500"></i>
11
- <% elsif type == "warning" %>
12
- <i class="fa-regular fa-exclamation-triangle text-yellow-500"></i>
13
- <% else %>
14
- <i class="fa-regular fa-info-circle text-blue-500"></i>
15
- <% end %>
16
- </div>
17
- <div class="ml-3 w-0 flex-1">
18
- <p class="text-sm font-medium text-gray-900">
19
- <%= message %>
20
- </p>
21
- </div>
22
- <div class="ml-4 flex-shrink-0 flex">
23
- <button type="button" class="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500" data-action="click->flash#close">
24
- <i class="fa-regular fa-times"></i>
25
- </button>
26
- </div>
27
- </div>
28
- </div>
29
- <% end %>
30
- </div>
31
- <% end %>
2
+ <% flash.each do |kind, message| %>
3
+ <%= render Panda::Core::Admin::FlashMessageComponent.new(kind: kind.to_sym, message: message) %>
4
+ <% end %>
5
+ <% end %>
@@ -1,27 +1,48 @@
1
1
  <nav class="flex flex-col flex-1">
2
- <ul role="list" class="flex flex-col flex-1 gap-y-7">
3
- <li>
4
- <ul role="list" class="-mx-2 space-y-1">
5
- <% Panda::Core.configuration.admin_navigation_items&.call(current_user)&.each do |item| %>
6
- <li>
7
- <%= link_to item[:path], class: "group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold text-gray-200 hover:text-white hover:bg-gray-700" do %>
8
- <i class="<%= item[:icon] %> text-gray-200 group-hover:text-white"></i>
9
- <%= item[:label] %>
10
- <% end %>
11
- </li>
2
+ <ul role="list" class="flex flex-col flex-1">
3
+ <a class="block p-0 mt-4 mb-4 ml-2 text-xl font-medium text-white"><%= Panda::Core.config.admin_title || "Panda Admin" %></a>
4
+ <%
5
+ # Get navigation items in original order for display
6
+ nav_items = Panda::Core.config.admin_navigation_items&.call(current_user) || []
7
+
8
+ # Find the most specific matching path by checking longest paths first
9
+ active_path = nav_items
10
+ .sort_by { |item| -item[:path].length }
11
+ .find { |item| request.path == item[:path] || request.path.starts_with?(item[:path] + "/") }
12
+ &.dig(:path)
13
+ %>
14
+ <% nav_items.each do |item| %>
15
+ <li>
16
+ <%
17
+ # Check if this item is the active one
18
+ is_active = item[:path] == active_path
19
+ %>
20
+ <%= link_to item[:path], class: "#{is_active ? "bg-mid text-white relative flex items-center transition-all py-3 px-2 mb-2 rounded-md group gap-x-3 text-base leading-6 font-normal" : "text-white hover:bg-mid/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal"}" do %>
21
+ <span class="text-center w-6"><i class="<%= item[:icon] %> text-xl fa-fw"></i></span>
22
+ <span><%= item[:label] %></span>
12
23
  <% end %>
13
- </ul>
24
+ </li>
25
+ <% end %>
26
+ <li>
27
+ <%= button_to panda_core.admin_logout_path, method: :delete, id: "logout-link", data: { turbo: false }, class: "text-white hover:bg-mid/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal w-full" do %>
28
+ <span class="text-center w-6"><i class="text-xl fa-solid fa-door-open fa-fw"></i></span>
29
+ <span>Logout</span>
30
+ <% end %>
14
31
  </li>
15
-
16
32
  <li class="mt-auto">
17
- <div class="flex items-center gap-x-4 px-6 py-3 text-sm font-semibold leading-6 text-gray-200">
18
- <% if current_user %>
19
- <span class="flex-1"><%= "#{current_user.firstname} #{current_user.lastname}" %></span>
20
- <%= link_to panda_core.admin_logout_path, method: :delete, class: "text-gray-200 hover:text-white" do %>
21
- <i class="fa-regular fa-sign-out"></i>
22
- <% end %>
33
+ <%= link_to panda_core.edit_admin_my_profile_path, class: "text-white hover:bg-mid/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal w-full", title: "Edit my Profile" do %>
34
+ <% if current_user.avatar.attached? %>
35
+ <span class="text-center w-6"><%= image_tag main_app.url_for(current_user.avatar), alt: current_user.name, class: "w-auto h-7 rounded-full object-cover" %></span>
36
+ <% elsif !current_user.image_url.to_s.empty? %>
37
+ <span class="text-center w-6"><img src="<%= current_user.image_url %>" class="w-auto h-7 rounded-full"></span>
38
+ <% else %>
39
+ <span class="text-center w-6"><i class="text-xl fa-solid fa-circle-user fa-fw"></i></span>
23
40
  <% end %>
24
- </div>
41
+ <span><%= current_user.name %></span>
42
+ <% end %>
43
+ </li>
44
+ <li class="px-2 py-3">
45
+ <span class="text-xs text-white">Panda Core v<%= Panda::Core::VERSION %></span>
25
46
  </li>
26
47
  </ul>
27
- </nav>
48
+ </nav>
@@ -1,11 +1,19 @@
1
1
  <!DOCTYPE html>
2
- <html data-theme="<%= Panda::Core::Current&.user&.current_theme || Panda::Core.configuration.default_theme %>" class="<%= local_assigns[:html_class] || "" %>">
2
+ <html data-theme="<%= Panda::Core::Current&.user&.current_theme || Panda::Core.config.default_theme %>" class="<%= local_assigns[:html_class] || "" %>">
3
3
  <head>
4
- <title><%= content_for?(:title) ? yield(:title) : (Panda::Core.configuration.admin_title || "Panda Admin") %></title>
4
+ <title><%= content_for?(:title) ? yield(:title) : (Panda::Core.config.admin_title || "Panda Admin") %></title>
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
- <script src="https://kit.fontawesome.com/7835d81e75.js" defer="true" crossorigin="anonymous"></script>
8
- <link rel="stylesheet" href="/panda-core-assets/panda-core.css">
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.1.0/css/all.min.css">
8
+ <%= panda_core_stylesheet %>
9
+ <%= panda_core_javascript %>
10
+
11
+ <% if defined?(Panda::CMS) && controller.class.name.start_with?("Panda::CMS") %>
12
+ <!-- CMS Assets -->
13
+ <%= panda_cms_javascript %>
14
+ <%= render "panda/cms/shared/favicons" %>
15
+ <% end %>
16
+
9
17
  <%= yield :head %>
10
18
  </head>
11
- <body class="h-full <%= local_assigns[:body_class] || "" %>" data-environment="<%= Rails.env %>">
19
+ <body class="overflow-hidden h-full <%= local_assigns[:body_class] || "" %>" data-environment="<%= Rails.env %>">
data/config/importmap.rb CHANGED
@@ -1,10 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Base JavaScript dependencies for Panda Core
4
- pin "@hotwired/turbo", to: "@hotwired--turbo.js", preload: true # @8.0.18
5
- pin "@rails/actioncable/src", to: "@rails--actioncable--src.js", preload: true # @8.0.201
6
- pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
7
- pin "tailwindcss-stimulus-components" # @6.1.3
3
+ # Panda Core application and controllers
4
+ # Served via Rack::Static middleware from app/javascript
5
+ pin "panda/core/application", to: "/panda/core/application.js"
6
+ pin "panda/core/controllers/index", to: "/panda/core/controllers/index.js"
7
+ pin "panda/core/controllers/toggle_controller", to: "/panda/core/controllers/toggle_controller.js"
8
+ pin "panda/core/controllers/theme_form_controller", to: "/panda/core/controllers/theme_form_controller.js"
9
+ pin "panda/core/tailwindplus-elements", to: "/panda/core/tailwindplus-elements.js"
8
10
 
9
- # Font Awesome icons
11
+ # Base JavaScript dependencies for Panda Core (vendored for reliability)
12
+ pin "@hotwired/stimulus", to: "/panda/core/vendor/@hotwired--stimulus.js", preload: true # @3.2.2
13
+ pin "@hotwired/turbo", to: "/panda/core/vendor/@hotwired--turbo.js", preload: true # @8.0.18
14
+ pin "@rails/actioncable/src", to: "/panda/core/vendor/@rails--actioncable--src.js", preload: true # @8.0.201
15
+ pin "tailwindcss-stimulus-components", to: "/panda/core/tailwindcss-stimulus-components.js" # @6.1.3
16
+
17
+ # Font Awesome icons (from CDN)
10
18
  pin "@fortawesome/fontawesome-free", to: "https://ga.jspm.io/npm:@fortawesome/fontawesome-free@7.1.0/js/all.js"
19
+
20
+ # Tailwind Plus Elements - Vanilla JS interactive components (from CDN)
21
+ # Provides: Autocomplete, Command palette, Dialog, Disclosure, Dropdown menu, Popover, Select, Tabs
22
+ # Note: Using esm.sh instead of jsdelivr for better ES module support
23
+ pin "@tailwindplus/elements", to: "https://esm.sh/@tailwindplus/elements@1", preload: false
data/config/routes.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  Panda::Core::Engine.routes.draw do
2
2
  # Get admin_path from configuration
3
3
  # Default to "/admin" if not yet configured
4
- admin_path = (Panda::Core.configuration.admin_path || "/admin").delete_prefix("/")
4
+ admin_path = (Panda::Core.config.admin_path || "/admin").delete_prefix("/")
5
5
 
6
6
  scope path: admin_path, as: "admin" do
7
7
  get "/login", to: "admin/sessions#new", as: :login
@@ -17,8 +17,15 @@ Panda::Core::Engine.routes.draw do
17
17
  get "/", to: "admin/dashboard#show", as: :root
18
18
 
19
19
  # Profile management
20
- constraints Panda::Core::AdminConstraint.new do
21
- resource :my_profile, only: %i[edit update], controller: "admin/my_profile", path: "my_profile"
20
+ resource :my_profile, only: %i[edit update], controller: "admin/my_profile", path: "my_profile"
21
+
22
+ # Test-only authentication endpoint (available in development and test environments)
23
+ # This bypasses OAuth for faster, more reliable test execution
24
+ # Development: Used by Capybara system tests which run Rails server in development mode
25
+ # Test: Used by controller/request tests
26
+ unless Rails.env.production?
27
+ get "/test_login/:user_id", to: "admin/test_sessions#create", as: :test_login
28
+ post "/test_sessions", to: "admin/test_sessions#create", as: :test_sessions
22
29
  end
23
30
  end
24
31
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCurrentThemeToPandaCoreUsers < ActiveRecord::Migration[7.1]
4
+ def change
5
+ add_column :panda_core_users, :current_theme, :string unless column_exists?(:panda_core_users, :current_theme)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddOauthAvatarUrlToPandaCoreUsers < ActiveRecord::Migration[8.0]
4
+ def change
5
+ add_column :panda_core_users, :oauth_avatar_url, :string
6
+ end
7
+ end
@@ -105,9 +105,19 @@ module Panda
105
105
  end
106
106
 
107
107
  else
108
- # Development mode - use importmap
108
+ # Development mode - use importmap for JS and static CSS
109
109
  tags = []
110
110
  tags << javascript_include_tag("panda/core/application", type: "module")
111
+
112
+ # Add CSS if available
113
+ css_path = development_css_url
114
+ if css_path
115
+ css_attrs = {
116
+ rel: "stylesheet",
117
+ href: css_path
118
+ }
119
+ tags << tag(:link, css_attrs)
120
+ end
111
121
  end
112
122
  tags.join("\n").html_safe
113
123
  end
@@ -144,11 +154,16 @@ module Panda
144
154
  end
145
155
 
146
156
  def development_css_url
147
- return unless compiled_assets_available?
148
-
157
+ # Try versioned file first
149
158
  version = asset_version
150
- css_file = "/panda-core-assets/panda-core-#{version}.css"
151
- File.exist?(Rails.root.join("public#{css_file}")) ? css_file : nil
159
+ versioned_file = "/panda-core-assets/panda-core-#{version}.css"
160
+ return versioned_file if File.exist?(Rails.public_path.join("panda-core-assets", "panda-core-#{version}.css"))
161
+
162
+ # Fall back to unversioned file (always available from engine's public directory)
163
+ unversioned_file = "/panda-core-assets/panda-core.css"
164
+ return unversioned_file if File.exist?(Panda::Core::Engine.root.join("public", "panda-core-assets", "panda-core.css"))
165
+
166
+ nil
152
167
  end
153
168
 
154
169
  def asset_version
@@ -193,9 +208,9 @@ module Panda
193
208
  end.compact.join(" ")
194
209
 
195
210
  if content || block_given?
196
- "<#{name}#{attrs.present? ? " #{attrs}" : ""}>#{content || (block_given? ? yield : "")}</#{name}>"
211
+ "<#{name}#{" #{attrs}" if attrs.present?}>#{content || (block_given? ? yield : "")}</#{name}>"
197
212
  else
198
- "<#{name}#{attrs.present? ? " #{attrs}" : ""}><#{name}>"
213
+ "<#{name}#{" #{attrs}" if attrs.present?}><#{name}>"
199
214
  end
200
215
  end
201
216
 
@@ -208,7 +223,7 @@ module Panda
208
223
  end
209
224
  end.compact.join(" ")
210
225
 
211
- "<#{name}#{attrs.present? ? " #{attrs}" : ""} />"
226
+ "<#{name}#{" #{attrs}" if attrs.present?} />"
212
227
  end
213
228
 
214
229
  def javascript_include_tag(source, options = {})
@@ -46,7 +46,7 @@ module Panda
46
46
  {
47
47
  label: "Dashboard",
48
48
  path: @admin_path,
49
- icon: "fa-regular fa-house"
49
+ icon: "fa-solid fa-house"
50
50
  }
51
51
  ]
52
52
 
@@ -55,14 +55,14 @@ module Panda
55
55
  items << {
56
56
  label: "Content",
57
57
  path: "#{@admin_path}/cms",
58
- icon: "fa-regular fa-file-lines"
58
+ icon: "fa-solid fa-file-lines"
59
59
  }
60
60
  end
61
61
 
62
62
  items << {
63
63
  label: "My Profile",
64
64
  path: "#{@admin_path}/my_profile/edit",
65
- icon: "fa-regular fa-user"
65
+ icon: "fa-solid fa-user"
66
66
  }
67
67
 
68
68
  items
@@ -84,19 +84,22 @@ module Panda
84
84
  end
85
85
 
86
86
  class << self
87
- attr_writer :configuration
87
+ attr_writer :config
88
88
 
89
- def configuration
90
- @configuration ||= Configuration.new
89
+ def config
90
+ @config ||= Configuration.new
91
91
  end
92
92
 
93
93
  def configure
94
- yield(configuration)
94
+ yield(config)
95
95
  end
96
96
 
97
- def reset_configuration!
98
- @configuration = Configuration.new
97
+ def reset_config!
98
+ @config = Configuration.new
99
99
  end
100
+
101
+ # Alias for backward compatibility with test expectations
102
+ alias_method :reset_configuration!, :reset_config!
100
103
  end
101
104
  end
102
105
  end