panda-core 0.2.4 → 0.4.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/tailwind/application.css +104 -7
  3. data/app/components/panda/core/UI/badge.rb +107 -0
  4. data/app/components/panda/core/UI/button.rb +89 -0
  5. data/app/components/panda/core/UI/card.rb +88 -0
  6. data/app/components/panda/core/admin/button_component.rb +46 -28
  7. data/app/components/panda/core/admin/container_component.rb +52 -4
  8. data/app/components/panda/core/admin/flash_message_component.rb +74 -9
  9. data/app/components/panda/core/admin/form_error_component.rb +48 -0
  10. data/app/components/panda/core/admin/form_input_component.rb +50 -0
  11. data/app/components/panda/core/admin/form_select_component.rb +68 -0
  12. data/app/components/panda/core/admin/heading_component.rb +52 -24
  13. data/app/components/panda/core/admin/panel_component.rb +33 -4
  14. data/app/components/panda/core/admin/slideover_component.rb +8 -4
  15. data/app/components/panda/core/admin/statistics_component.rb +19 -0
  16. data/app/components/panda/core/admin/tab_bar_component.rb +101 -0
  17. data/app/components/panda/core/admin/table_component.rb +90 -9
  18. data/app/components/panda/core/admin/tag_component.rb +21 -16
  19. data/app/components/panda/core/admin/user_activity_component.rb +43 -0
  20. data/app/components/panda/core/admin/user_display_component.rb +78 -0
  21. data/app/components/panda/core/base.rb +122 -0
  22. data/app/controllers/panda/core/admin/base_controller.rb +68 -0
  23. data/app/controllers/panda/core/admin/dashboard_controller.rb +5 -3
  24. data/app/controllers/panda/core/admin/my_profile_controller.rb +3 -3
  25. data/app/controllers/panda/core/admin/sessions_controller.rb +9 -6
  26. data/app/helpers/panda/core/sessions_helper.rb +1 -1
  27. data/app/javascript/panda/core/vendor/@hotwired--stimulus.js +4 -0
  28. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +160 -0
  29. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +4 -0
  30. data/app/models/panda/core/user.rb +17 -13
  31. data/app/views/layouts/panda/core/admin.html.erb +40 -3
  32. data/app/views/layouts/panda/core/admin_simple.html.erb +5 -0
  33. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +4 -4
  34. data/app/views/panda/core/admin/dashboard/show.html.erb +2 -2
  35. data/app/views/panda/core/admin/my_profile/edit.html.erb +13 -27
  36. data/app/views/panda/core/admin/sessions/new.html.erb +7 -7
  37. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +27 -34
  38. data/app/views/panda/core/admin/shared/_flash.html.erb +4 -30
  39. data/app/views/panda/core/admin/shared/_sidebar.html.erb +36 -20
  40. data/app/views/panda/core/shared/_header.html.erb +13 -5
  41. data/config/importmap.rb +11 -6
  42. data/config/routes.rb +2 -4
  43. data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
  44. data/lib/panda/core/asset_loader.rb +23 -8
  45. data/lib/panda/core/configuration.rb +12 -9
  46. data/lib/panda/core/debug.rb +47 -0
  47. data/lib/panda/core/engine.rb +43 -6
  48. data/lib/panda/core/version.rb +1 -1
  49. data/lib/panda/core.rb +1 -0
  50. metadata +93 -14
  51. data/app/components/panda/core/admin/container_component.html.erb +0 -12
  52. data/app/components/panda/core/admin/flash_message_component.html.erb +0 -31
  53. data/app/components/panda/core/admin/panel_component.html.erb +0 -7
  54. data/app/components/panda/core/admin/slideover_component.html.erb +0 -9
  55. data/app/components/panda/core/admin/table_component.html.erb +0 -29
  56. data/app/controllers/panda/core/admin_controller.rb +0 -30
@@ -1,49 +1,35 @@
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>
14
- <% end %>
15
- </div>
16
- </div>
17
- <% end %>
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) %>
18
10
 
19
11
  <div class="space-y-4">
20
12
  <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" %>
23
- </div>
24
-
25
- <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" %>
13
+ <%= f.label :name %>
14
+ <%= f.text_field :name %>
28
15
  </div>
29
16
 
30
17
  <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" %>
18
+ <%= f.label :email %>
19
+ <%= f.email_field :email %>
33
20
  </div>
34
21
 
35
22
  <div class="field">
36
- <%= f.label :current_theme, "Theme", class: "block text-sm font-medium text-gray-700" %>
23
+ <%= f.label :current_theme, "Theme" %>
37
24
  <%= f.select :current_theme,
38
- options_for_select(Panda::Core.configuration.available_themes || [["Default", "default"], ["Sky", "sky"]], user.current_theme),
25
+ options_for_select(Panda::Core.config.available_themes || [["Default", "default"], ["Sky", "sky"]], user.current_theme),
39
26
  {},
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
27
  data: { action: "change->theme-form#updateTheme" } %>
42
28
  </div>
43
29
  </div>
44
30
 
45
31
  <%= f.submit "Update Profile",
46
- class: "btn btn-primary mt-6",
32
+ class: "btn btn-primary mt-4",
47
33
  data: { disable_with: "Saving..." } %>
48
34
  <% end %>
49
35
  <% end %>
@@ -1,20 +1,20 @@
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] %>
13
+ <% provider_config = Panda::Core.config.authentication_providers[provider] %>
14
14
  <% provider_name = provider_config&.dig(:name) || provider.to_s.humanize %>
15
15
  <% provider_path = provider_config&.dig(:path_name) || provider %>
16
16
  <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 %>
17
+ <%= form_tag "#{Panda::Core.config.admin_path}/auth/#{provider_path}", method: "post", data: {turbo: false} do %>
18
18
  <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
19
  <i class="fa-brands fa-<%= oauth_provider_icon(provider) %> text-xl mr-1"></i>
20
20
  Sign in with <%= provider_name %>
@@ -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"></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 fa-regular fa-chevron-right font-light <%= breadcrumb_styles %> py-5 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" data-controller="toggle">
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,43 @@
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
+ <% Panda::Core.config.admin_navigation_items&.call(current_user)&.each do |item| %>
5
+ <li>
6
+ <%
7
+ # Exact match for dashboard, starts_with for others
8
+ # Check if current path matches this nav item
9
+ is_active = if request.path == item[:path]
10
+ true
11
+ elsif request.path.starts_with?(item[:path] + "/")
12
+ true
13
+ else
14
+ false
15
+ end
16
+ %>
17
+ <%= 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 %>
18
+ <span class="text-center w-6"><i class="<%= item[:icon] %> text-xl fa-fw"></i></span>
19
+ <span><%= item[:label] %></span>
12
20
  <% end %>
13
- </ul>
21
+ </li>
22
+ <% end %>
23
+ <li>
24
+ <%= 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 %>
25
+ <span class="text-center w-6"><i class="text-xl fa-solid fa-door-open fa-fw"></i></span>
26
+ <span>Logout</span>
27
+ <% end %>
14
28
  </li>
15
-
16
29
  <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 %>
30
+ <%= 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 %>
31
+ <% if !current_user.image_url.to_s.empty? %>
32
+ <span class="text-center w-6"><img src="<%= current_user.image_url %>" class="w-auto h-7 rounded-full"></span>
33
+ <% else %>
34
+ <span class="text-center w-6"><i class="text-xl fa-regular fa-circle-user fa-fw"></i></span>
23
35
  <% end %>
24
- </div>
36
+ <span><%= current_user.name %></span>
37
+ <% end %>
38
+ </li>
39
+ <li class="px-2 py-3">
40
+ <span class="text-xs text-white">Panda Core v<%= Panda::Core::VERSION %></span>
25
41
  </li>
26
42
  </ul>
27
- </nav>
43
+ </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,15 @@
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
+ pin "panda/core/application", to: "panda/core/application.js"
5
+ pin "panda/core/controllers/index", to: "panda/core/controllers/index.js"
6
+ pin_all_from Panda::Core::Engine.root.join("app/javascript/panda/core/controllers"), under: "panda/core/controllers"
8
7
 
9
- # Font Awesome icons
8
+ # Base JavaScript dependencies for Panda Core (vendored for reliability)
9
+ pin "@hotwired/stimulus", to: "panda/core/vendor/@hotwired--stimulus.js", preload: true # @3.2.2
10
+ pin "@hotwired/turbo", to: "panda/core/vendor/@hotwired--turbo.js", preload: true # @8.0.18
11
+ pin "@rails/actioncable/src", to: "panda/core/vendor/@rails--actioncable--src.js", preload: true # @8.0.201
12
+ pin "tailwindcss-stimulus-components", to: "panda/core/tailwindcss-stimulus-components.js" # @6.1.3
13
+
14
+ # Font Awesome icons (from CDN)
10
15
  pin "@fortawesome/fontawesome-free", to: "https://ga.jspm.io/npm:@fortawesome/fontawesome-free@7.1.0/js/all.js"
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,6 @@ 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"
22
- end
20
+ resource :my_profile, only: %i[edit update], controller: "admin/my_profile", path: "my_profile"
23
21
  end
24
22
  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
@@ -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
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Debug
6
+ class << self
7
+ # Check if debug mode is enabled via PANDA_DEBUG environment variable
8
+ def enabled?
9
+ ENV["PANDA_DEBUG"].to_s.downcase == "true" || ENV["PANDA_DEBUG"] == "1"
10
+ end
11
+
12
+ # Log a debug message if debug mode is enabled
13
+ def log(message, prefix: "PANDA")
14
+ return unless enabled?
15
+
16
+ timestamp = Time.current.strftime("%Y-%m-%d %H:%M:%S")
17
+ puts "[#{prefix} DEBUG #{timestamp}] #{message}"
18
+ end
19
+
20
+ # Log an object with pretty printing (using awesome_print if available)
21
+ def inspect(object, label: nil, prefix: "PANDA")
22
+ return unless enabled?
23
+
24
+ timestamp = Time.current.strftime("%Y-%m-%d %H:%M:%S")
25
+ header = label ? "#{label}: " : ""
26
+
27
+ puts "\n[#{prefix} DEBUG #{timestamp}] #{header}"
28
+ if defined?(AwesomePrint)
29
+ ap object
30
+ else
31
+ pp object
32
+ end
33
+ puts
34
+ end
35
+
36
+ # Enable HTTP debugging for Net::HTTP requests
37
+ def enable_http_debug!
38
+ return unless enabled? || ENV["DEBUG_HTTP"].to_s.downcase == "true"
39
+
40
+ require "net/http"
41
+ Net::HTTP.set_debug_output($stdout)
42
+ log("HTTP debugging enabled - all HTTP requests will be logged")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -13,6 +13,8 @@ module Panda
13
13
  config.eager_load_namespaces << Panda::Core::Engine
14
14
 
15
15
  # Add engine's app directories to autoload paths
16
+ # Note: Only add the root directories, not nested subdirectories
17
+ # Zeitwerk will automatically discover nested modules from these roots
16
18
  config.autoload_paths += Dir[root.join("app", "models")]
17
19
  config.autoload_paths += Dir[root.join("app", "controllers")]
18
20
  config.autoload_paths += Dir[root.join("app", "builders")]
@@ -43,7 +45,7 @@ module Panda
43
45
  end
44
46
  end
45
47
 
46
- initializer "panda_core.configuration" do |app|
48
+ initializer "panda_core.config" do |app|
47
49
  # Configuration is already initialized with defaults in Configuration class
48
50
  end
49
51
 
@@ -64,18 +66,18 @@ module Panda
64
66
  end
65
67
  end
66
68
 
67
-
68
69
  initializer "panda_core.omniauth" do |app|
69
70
  # Mount OmniAuth at configurable admin path
70
71
  app.middleware.use OmniAuth::Builder do
71
72
  # Configure OmniAuth to use the configured admin path
72
73
  configure do |config|
73
- config.path_prefix = "#{Panda::Core.configuration.admin_path}/auth"
74
- # Allow POST requests for request phase (required for CSRF protection)
75
- config.allowed_request_methods = [:get, :post]
74
+ config.path_prefix = "#{Panda::Core.config.admin_path}/auth"
75
+ # POST-only for CSRF protection (CVE-2015-9284)
76
+ # All login forms use POST via form_tag method: "post"
77
+ config.allowed_request_methods = [:post]
76
78
  end
77
79
 
78
- Panda::Core.configuration.authentication_providers.each do |provider_name, settings|
80
+ Panda::Core.config.authentication_providers.each do |provider_name, settings|
79
81
  # Build provider options, allowing custom path name override
80
82
  provider_options = settings[:options] || {}
81
83
 
@@ -97,6 +99,41 @@ module Panda
97
99
  end
98
100
  end
99
101
  end
102
+
103
+ # Load Phlex base component after Rails application is initialized
104
+ # This ensures Rails.application.routes is available
105
+ initializer "panda_core.phlex_base", after: :load_config_initializers do
106
+ require "phlex"
107
+ require "phlex-rails"
108
+ require "literal"
109
+ require "tailwind_merge"
110
+
111
+ # Load the base component
112
+ require root.join("app/components/panda/core/base")
113
+ end
114
+
115
+ # Set up ViewComponent and Lookbook previews
116
+ initializer "panda_core.view_component" do |app|
117
+ app.config.view_component.preview_paths ||= []
118
+ app.config.view_component.preview_paths << root.join("spec/components/previews")
119
+
120
+ # Add preview directories to autoload paths in development
121
+ if Rails.env.development?
122
+ # Handle frozen autoload_paths array
123
+ if app.config.autoload_paths.frozen?
124
+ app.config.autoload_paths = app.config.autoload_paths.dup
125
+ end
126
+ app.config.autoload_paths << root.join("spec/components/previews")
127
+ end
128
+ end
129
+
130
+ # Create AdminController alias after controllers are loaded
131
+ # This allows other gems to inherit from Panda::Core::AdminController
132
+ initializer "panda_core.admin_controller_alias", after: :load_config_initializers do
133
+ ActiveSupport.on_load(:action_controller_base) do
134
+ Panda::Core.const_set(:AdminController, Panda::Core::Admin::BaseController) unless Panda::Core.const_defined?(:AdminController)
135
+ end
136
+ end
100
137
  end
101
138
  end
102
139
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Panda
4
4
  module Core
5
- VERSION = "0.2.4"
5
+ VERSION = "0.4.1"
6
6
  end
7
7
  end
data/lib/panda/core.rb CHANGED
@@ -13,4 +13,5 @@ end
13
13
  require_relative "core/version"
14
14
  require_relative "core/configuration"
15
15
  require_relative "core/asset_loader"
16
+ require_relative "core/debug"
16
17
  require_relative "core/engine" if defined?(Rails)