panda-core 0.1.15 → 0.2.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -16
  3. data/Rakefile +3 -0
  4. data/app/builders/panda/core/form_builder.rb +225 -0
  5. data/app/components/panda/core/admin/button_component.rb +70 -0
  6. data/app/components/panda/core/admin/container_component.html.erb +12 -0
  7. data/app/components/panda/core/admin/container_component.rb +13 -0
  8. data/app/components/panda/core/admin/flash_message_component.html.erb +31 -0
  9. data/app/components/panda/core/admin/flash_message_component.rb +47 -0
  10. data/app/components/panda/core/admin/heading_component.rb +46 -0
  11. data/app/components/panda/core/admin/panel_component.html.erb +7 -0
  12. data/app/components/panda/core/admin/panel_component.rb +13 -0
  13. data/app/components/panda/core/admin/slideover_component.html.erb +9 -0
  14. data/app/components/panda/core/admin/slideover_component.rb +15 -0
  15. data/app/components/panda/core/admin/table_component.html.erb +29 -0
  16. data/app/components/panda/core/admin/table_component.rb +46 -0
  17. data/app/components/panda/core/admin/tag_component.rb +35 -0
  18. data/app/constraints/panda/core/admin_constraint.rb +14 -0
  19. data/app/controllers/panda/core/admin/dashboard_controller.rb +22 -0
  20. data/app/controllers/panda/core/admin/my_profile_controller.rb +49 -0
  21. data/app/controllers/panda/core/admin/sessions_controller.rb +69 -0
  22. data/app/controllers/panda/core/admin_controller.rb +28 -0
  23. data/app/controllers/panda/core/application_controller.rb +59 -0
  24. data/app/helpers/panda/core/asset_helper.rb +32 -0
  25. data/app/javascript/panda/core/application.js +9 -0
  26. data/app/javascript/panda/core/controllers/index.js +20 -0
  27. data/app/javascript/panda/core/controllers/theme_form_controller.js +25 -0
  28. data/app/javascript/panda/core/tailwindcss-stimulus-components.js +3 -0
  29. data/app/models/panda/core/application_record.rb +9 -0
  30. data/app/models/panda/core/breadcrumb.rb +17 -0
  31. data/app/models/panda/core/current.rb +16 -0
  32. data/app/models/panda/core/user.rb +51 -0
  33. data/app/views/layouts/panda/core/admin.html.erb +59 -0
  34. data/app/views/panda/core/admin/dashboard/show.html.erb +27 -0
  35. data/app/views/panda/core/admin/my_profile/edit.html.erb +49 -0
  36. data/app/views/panda/core/admin/sessions/new.html.erb +38 -0
  37. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +35 -0
  38. data/app/views/panda/core/admin/shared/_flash.html.erb +31 -0
  39. data/app/views/panda/core/admin/shared/_sidebar.html.erb +27 -0
  40. data/app/views/panda/core/admin/shared/_slideover.html.erb +33 -0
  41. data/config/routes.rb +22 -0
  42. data/db/migrate/20241210000003_add_current_theme_to_panda_core_users.rb +7 -0
  43. data/db/migrate/20250809000001_create_panda_core_users.rb +16 -0
  44. data/lib/generators/panda/core/dev_tools/USAGE +24 -0
  45. data/lib/generators/panda/core/dev_tools/templates/lefthook.yml +13 -0
  46. data/lib/generators/panda/core/dev_tools/templates/spec_support_panda_core_helpers.rb +18 -0
  47. data/lib/generators/panda/core/dev_tools_generator.rb +143 -0
  48. data/lib/panda/core/asset_loader.rb +221 -0
  49. data/lib/panda/core/authentication.rb +36 -0
  50. data/lib/panda/core/component_registry.rb +37 -0
  51. data/lib/panda/core/configuration.rb +31 -1
  52. data/lib/panda/core/engine.rb +43 -7
  53. data/lib/panda/core/notifications.rb +40 -0
  54. data/lib/panda/core/rake_tasks.rb +16 -0
  55. data/lib/panda/core/subscribers/authentication_subscriber.rb +61 -0
  56. data/lib/panda/core/testing/capybara_config.rb +70 -0
  57. data/lib/panda/core/testing/omniauth_helpers.rb +52 -0
  58. data/lib/panda/core/testing/rspec_config.rb +72 -0
  59. data/lib/panda/core/version.rb +1 -1
  60. data/lib/panda/core.rb +2 -8
  61. data/lib/tasks/assets.rake +423 -0
  62. data/lib/tasks/panda/core/migrations.rake +13 -0
  63. data/lib/tasks/panda_core.rake +52 -0
  64. metadata +375 -10
  65. data/db/migrate/20250121012333_logidze_install.rb +0 -577
  66. data/db/migrate/20250121012334_enable_hstore.rb +0 -5
@@ -0,0 +1,35 @@
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" %>
31
+ </a>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ <% end %>
@@ -0,0 +1,31 @@
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 %>
@@ -0,0 +1,27 @@
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>
12
+ <% end %>
13
+ </ul>
14
+ </li>
15
+
16
+ <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 %>
23
+ <% end %>
24
+ </div>
25
+ </li>
26
+ </ul>
27
+ </nav>
@@ -0,0 +1,33 @@
1
+ <div class="fixed inset-0 z-50 lg:hidden" role="dialog" aria-modal="true" id="mobile-slideover">
2
+ <div class="fixed inset-0 bg-gray-600 bg-opacity-75"></div>
3
+ <div class="fixed inset-0 flex">
4
+ <div class="relative flex w-full max-w-xs flex-1 flex-col bg-gray-800">
5
+ <div class="absolute top-0 right-0 -mr-12 pt-2">
6
+ <button type="button" class="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
7
+ <span class="sr-only">Close sidebar</span>
8
+ <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
9
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
10
+ </svg>
11
+ </button>
12
+ </div>
13
+ <%= yield %>
14
+ </div>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="hidden lg:flex lg:flex-shrink-0" id="slideover" data-toggle-target="content">
19
+ <div class="flex w-96">
20
+ <div class="flex min-h-0 flex-1 flex-col bg-gray-50 border-l border-gray-200">
21
+ <% if content_for?(:sidebar_title) %>
22
+ <div class="flex h-16 flex-shrink-0 items-center border-b border-gray-200 px-6">
23
+ <h2 class="text-lg font-medium text-gray-900"><%= yield :sidebar_title %></h2>
24
+ </div>
25
+ <% end %>
26
+ <div class="flex flex-1 flex-col overflow-y-auto">
27
+ <div class="px-6 py-4">
28
+ <%= yield %>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,22 @@
1
+ Panda::Core::Engine.routes.draw do
2
+ # Use the configured admin path (defaults to "/admin")
3
+ admin_path = Panda::Core.configuration.admin_path.delete_prefix("/")
4
+
5
+ scope path: admin_path, as: "admin" do
6
+ get "/login", to: "admin/sessions#new", as: :login
7
+
8
+ # OmniAuth routes - middleware handles /admin/auth/:provider automatically
9
+ # We just need to define the callback routes
10
+ get "/auth/:provider/callback", to: "admin/sessions#create", as: :auth_callback
11
+ post "/auth/:provider/callback", to: "admin/sessions#create"
12
+ get "/auth/failure", to: "admin/sessions#failure", as: :auth_failure
13
+ delete "/logout", to: "admin/sessions#destroy", as: :logout
14
+
15
+ constraints Panda::Core::AdminConstraint.new do
16
+ get "/", to: "admin/dashboard#show", as: :root
17
+
18
+ # Profile management
19
+ resource :my_profile, only: %i[edit update], controller: "admin/my_profile", path: "my_profile"
20
+ end
21
+ end
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, default: "default"
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreatePandaCoreUsers < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :panda_core_users, id: :uuid do |t|
6
+ t.string :firstname, null: false
7
+ t.string :lastname, null: false
8
+ t.string :email, null: false
9
+ t.string :image_url
10
+ t.boolean :admin, default: false, null: false
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :panda_core_users, :email, unique: true
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ Description:
2
+ Sets up or updates Panda Core development tools in your gem or Rails application.
3
+ This includes linting configurations, testing helpers, and CI/CD workflows.
4
+
5
+ Example:
6
+ # Initial setup
7
+ rails generate panda:core:dev_tools
8
+
9
+ # Force update (overwrites existing files)
10
+ rails generate panda:core:dev_tools --force
11
+
12
+ # Skip adding dependencies to Gemfile
13
+ rails generate panda:core:dev_tools --skip-dependencies
14
+
15
+ This will:
16
+ - Copy .standard.yml, .yamllint, and .rspec configurations
17
+ - Set up GitHub Actions workflows for CI and releases
18
+ - Add development dependencies to your Gemfile
19
+ - Create testing helper configuration
20
+ - Add rake tasks for linting and security checks
21
+ - Track version for future updates
22
+
23
+ To check for updates later:
24
+ bundle exec rake panda:check_updates
@@ -0,0 +1,13 @@
1
+ ---
2
+ assert_lefthook_installed: true
3
+ colors: true
4
+ pre-commit:
5
+ parallel: true
6
+ jobs:
7
+ - name: bundle-audit
8
+ run: bundle exec bundle-audit --update
9
+ - name: bundle-outdated
10
+ run: bundle outdated --strict
11
+ - name: standardrb
12
+ run: bundle exec standardrb --fix {all_files}
13
+ glob: "**/*.rb"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "panda/core/testing/rspec_config"
4
+ require "panda/core/testing/omniauth_helpers"
5
+ require "panda/core/testing/capybara_config"
6
+
7
+ RSpec.configure do |config|
8
+ # Apply Panda Core RSpec configuration
9
+ Panda::Core::Testing::RSpecConfig.configure(config)
10
+ Panda::Core::Testing::RSpecConfig.setup_matchers
11
+
12
+ # Configure Capybara
13
+ Panda::Core::Testing::CapybaraConfig.configure
14
+
15
+ # Include helpers
16
+ config.include Panda::Core::Testing::OmniAuthHelpers, type: :system
17
+ config.include Panda::Core::Testing::CapybaraConfig::Helpers, type: :system
18
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Panda
6
+ module Core
7
+ module Generators
8
+ class DevToolsGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("dev_tools/templates", __dir__)
10
+
11
+ desc "Set up or update Panda Core development tools in your gem/application"
12
+
13
+ class_option :force, type: :boolean, default: false,
14
+ desc: "Overwrite existing files"
15
+ class_option :skip_dependencies, type: :boolean, default: false,
16
+ desc: "Skip adding dependencies to Gemfile"
17
+
18
+ VERSION_FILE = ".panda-dev-tools-version"
19
+ CURRENT_VERSION = "1.0.0"
20
+
21
+ def check_for_updates
22
+ if File.exist?(VERSION_FILE)
23
+ installed_version = File.read(VERSION_FILE).strip
24
+ if installed_version != CURRENT_VERSION
25
+ say "Updating Panda Core dev tools from #{installed_version} to #{CURRENT_VERSION}", :yellow
26
+ @updating = true
27
+ else
28
+ say "Panda Core dev tools are up to date (#{CURRENT_VERSION})", :green
29
+ unless options[:force]
30
+ say "Use --force to reinstall anyway"
31
+ nil
32
+ end
33
+ end
34
+ else
35
+ say "Installing Panda Core dev tools #{CURRENT_VERSION}", :green
36
+ @updating = false
37
+ end
38
+ end
39
+
40
+ def copy_linting_configs
41
+ say "Copying linting configurations..."
42
+ copy_file ".standard.yml", force: options[:force] || @updating
43
+ copy_file ".yamllint", force: options[:force] || @updating
44
+ copy_file ".rspec", force: options[:force] || @updating
45
+ copy_file "lefthook.yml", force: options[:force] || @updating
46
+ end
47
+
48
+ def copy_github_workflows
49
+ say "Copying GitHub Actions workflows..."
50
+ directory ".github", ".github", force: options[:force] || @updating
51
+ end
52
+
53
+ def create_version_file
54
+ create_file VERSION_FILE, CURRENT_VERSION, force: true
55
+ end
56
+
57
+ def add_development_dependencies
58
+ say "Adding development dependencies to gemspec..."
59
+
60
+ if File.exist?("Gemfile")
61
+ append_to_file "Gemfile" do
62
+ <<~RUBY
63
+
64
+ group :development, :test do
65
+ # Panda Core development tools
66
+ gem "standard"
67
+ gem "brakeman"
68
+ gem "bundler-audit"
69
+ gem "yamllint"
70
+ end
71
+ RUBY
72
+ end
73
+ end
74
+ end
75
+
76
+ def create_spec_helper
77
+ say "Creating spec helper with Panda Core testing configuration..."
78
+
79
+ create_file "spec/support/panda_core_helpers.rb" do
80
+ <<~RUBY
81
+ # frozen_string_literal: true
82
+
83
+ require 'panda/core/testing/rspec_config'
84
+ require 'panda/core/testing/omniauth_helpers'
85
+ require 'panda/core/testing/capybara_config'
86
+
87
+ RSpec.configure do |config|
88
+ # Apply Panda Core RSpec configuration
89
+ Panda::Core::Testing::RSpecConfig.configure(config)
90
+ Panda::Core::Testing::RSpecConfig.setup_matchers
91
+
92
+ # Configure Capybara
93
+ Panda::Core::Testing::CapybaraConfig.configure
94
+
95
+ # Include helpers
96
+ config.include Panda::Core::Testing::OmniAuthHelpers, type: :system
97
+ config.include Panda::Core::Testing::CapybaraConfig::Helpers, type: :system
98
+ end
99
+ RUBY
100
+ end
101
+ end
102
+
103
+ def add_rake_tasks
104
+ say "Adding Panda Core rake tasks..."
105
+
106
+ append_to_file "Rakefile" do
107
+ <<~RUBY
108
+
109
+ # Panda Core development tasks
110
+ namespace :panda do
111
+ desc "Run all linters"
112
+ task :lint do
113
+ sh "bundle exec standardrb"
114
+ sh "yamllint -c .yamllint ."
115
+ end
116
+
117
+ desc "Run security checks"
118
+ task :security do
119
+ sh "bundle exec brakeman --quiet"
120
+ sh "bundle exec bundle-audit --update"
121
+ end
122
+
123
+ desc "Run all quality checks"
124
+ task quality: [:lint, :security]
125
+ end
126
+ RUBY
127
+ end
128
+ end
129
+
130
+ def display_instructions
131
+ say "\n✅ Panda Core development tools have been set up!", :green
132
+ say "\nNext steps:"
133
+ say " 1. Run 'bundle install' to install new dependencies"
134
+ say " 2. Run 'bundle exec rake panda:quality' to check code quality"
135
+ say " 3. Customize .github/workflows for your gem's needs"
136
+ say " 4. Add 'require' statements to your spec_helper.rb or rails_helper.rb:"
137
+ say " require 'support/panda_core_helpers'"
138
+ say "\nFor more information, see: docs/development_tools.md"
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'fileutils'
5
+ require 'json'
6
+
7
+ module Panda
8
+ module Core
9
+ # AssetLoader handles loading compiled assets from GitHub releases
10
+ # Falls back to local development assets when GitHub assets unavailable
11
+ class AssetLoader
12
+ class << self
13
+ # Generate HTML tags for loading Panda Core assets
14
+ def asset_tags(options = {})
15
+ if use_github_assets?
16
+ github_asset_tags(options)
17
+ else
18
+ development_asset_tags(options)
19
+ end
20
+ end
21
+
22
+ # Get the JavaScript asset URL
23
+ def javascript_url
24
+ if use_github_assets?
25
+ github_javascript_url
26
+ else
27
+ development_javascript_url
28
+ end
29
+ end
30
+
31
+ # Get the CSS asset URL (if exists)
32
+ def css_url
33
+ if use_github_assets?
34
+ github_css_url
35
+ else
36
+ development_css_url
37
+ end
38
+ end
39
+
40
+ # Check if GitHub-hosted assets should be used
41
+ def use_github_assets?
42
+ # Use GitHub assets in production or when explicitly enabled
43
+ Rails.env.production? ||
44
+ ENV['PANDA_CORE_USE_GITHUB_ASSETS'] == 'true' ||
45
+ !development_assets_available? ||
46
+ ((Rails.env.test? || in_test_environment?) && compiled_assets_available?)
47
+ end
48
+
49
+ private
50
+
51
+ def github_asset_tags(_options = {})
52
+ version = asset_version
53
+ base_url = github_base_url(version)
54
+
55
+ tags = []
56
+
57
+ # JavaScript tag with integrity check
58
+ js_url = "#{base_url}panda-core-#{version}.js"
59
+
60
+ js_attrs = {
61
+ src: js_url
62
+ }
63
+ # In CI environment, don't use defer to ensure immediate execution
64
+ js_attrs[:defer] = true unless ENV['GITHUB_ACTIONS'] == 'true'
65
+
66
+ tags << content_tag(:script, '', js_attrs)
67
+
68
+ # CSS tag if CSS bundle exists
69
+ css_url = "#{base_url}panda-core-#{version}.css"
70
+ if github_asset_exists?(version, "panda-core-#{version}.css")
71
+ css_attrs = {
72
+ rel: 'stylesheet',
73
+ href: css_url
74
+ }
75
+ tags << tag(:link, css_attrs)
76
+ end
77
+
78
+ tags.join("\n").html_safe
79
+ end
80
+
81
+ def development_asset_tags(_options = {})
82
+ # In test environment with CI, always use compiled assets
83
+ if (Rails.env.test? || ENV['CI'].present?) && compiled_assets_available?
84
+ # Use the same logic as GitHub assets but with local paths
85
+ version = asset_version
86
+ js_url = "/panda-core-assets/panda-core-#{version}.js"
87
+ css_url = "/panda-core-assets/panda-core-#{version}.css"
88
+
89
+ tags = []
90
+
91
+ # JavaScript tag
92
+ js_attrs = {
93
+ src: js_url
94
+ }
95
+ js_attrs[:defer] = true unless ENV['GITHUB_ACTIONS'] == 'true'
96
+ tags << content_tag(:script, '', js_attrs)
97
+
98
+ # CSS tag if exists
99
+ if File.exist?(Rails.root.join('public', 'panda-core-assets', "panda-core-#{version}.css"))
100
+ css_attrs = {
101
+ rel: 'stylesheet',
102
+ href: css_url
103
+ }
104
+ tags << tag(:link, css_attrs)
105
+ end
106
+
107
+ else
108
+ # Development mode - use importmap
109
+ tags = []
110
+ tags << javascript_include_tag('panda/core/application', type: 'module')
111
+ end
112
+ tags.join("\n").html_safe
113
+ end
114
+
115
+ def github_javascript_url
116
+ version = asset_version
117
+ # In test environment with local compiled assets, use local URL
118
+ if Rails.env.test? && compiled_assets_available?
119
+ "/panda-core-assets/panda-core-#{version}.js"
120
+ else
121
+ "#{github_base_url(version)}panda-core-#{version}.js"
122
+ end
123
+ end
124
+
125
+ def github_css_url
126
+ version = asset_version
127
+ # In test environment with local compiled assets, use local URL
128
+ if Rails.env.test? && compiled_assets_available?
129
+ css_file = "/panda-core-assets/panda-core-#{version}.css"
130
+ File.exist?(Rails.root.join("public#{css_file}")) ? css_file : nil
131
+ else
132
+ "#{github_base_url(version)}panda-core-#{version}.css"
133
+ end
134
+ end
135
+
136
+ def development_javascript_url
137
+ if compiled_assets_available?
138
+ version = asset_version
139
+ "/panda-core-assets/panda-core-#{version}.js"
140
+ else
141
+ # Return importmap path
142
+ '/assets/panda/core/application.js'
143
+ end
144
+ end
145
+
146
+ def development_css_url
147
+ return unless compiled_assets_available?
148
+
149
+ 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
152
+ end
153
+
154
+ def asset_version
155
+ Panda::Core::VERSION
156
+ end
157
+
158
+ def github_base_url(version)
159
+ "https://github.com/tastybamboo/panda-core/releases/download/v#{version}/"
160
+ end
161
+
162
+ def github_asset_exists?(_version, _filename)
163
+ # For now, assume assets exist if we're using GitHub mode
164
+ # Could implement actual checking via GitHub API
165
+ true
166
+ end
167
+
168
+ def development_assets_available?
169
+ # Check if we're in a development environment with importmap available
170
+ Rails.env.development? && defined?(Importmap)
171
+ end
172
+
173
+ def compiled_assets_available?
174
+ # Check if compiled assets exist in test location
175
+ version = asset_version
176
+ js_file = Rails.public_path.join('panda-core-assets', "panda-core-#{version}.js")
177
+ js_file.exist?
178
+ end
179
+
180
+ def in_test_environment?
181
+ # Additional check for test environment indicators
182
+ ENV['CI'].present? || ENV['GITHUB_ACTIONS'].present? || ENV['RAILS_ENV'] == 'test'
183
+ end
184
+
185
+ # Helper methods to match ActionView helpers
186
+ def content_tag(name, content = nil, options = {})
187
+ attrs = options.map do |k, v|
188
+ if v == true
189
+ k.to_s
190
+ elsif v
191
+ "#{k}=\"#{v}\""
192
+ end
193
+ end.compact.join(' ')
194
+
195
+ if content || block_given?
196
+ "<#{name}#{attrs.present? ? " #{attrs}" : ''}>#{content || (block_given? ? yield : '')}</#{name}>"
197
+ else
198
+ "<#{name}#{attrs.present? ? " #{attrs}" : ''}><#{name}>"
199
+ end
200
+ end
201
+
202
+ def tag(name, options = {})
203
+ attrs = options.map do |k, v|
204
+ if v == true
205
+ k.to_s
206
+ elsif v
207
+ "#{k}=\"#{v}\""
208
+ end
209
+ end.compact.join(' ')
210
+
211
+ "<#{name}#{attrs.present? ? " #{attrs}" : ''} />"
212
+ end
213
+
214
+ def javascript_include_tag(source, options = {})
215
+ options[:src] = source.start_with?('/') ? source : "/assets/#{source}"
216
+ content_tag(:script, '', options)
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Authentication
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ helper_method :current_user if respond_to?(:helper_method)
10
+ end
11
+
12
+ def current_user
13
+ return @current_user if defined?(@current_user)
14
+ @current_user = User.find_by(id: session[:user_id]) if session[:user_id]
15
+ end
16
+
17
+ def require_authentication
18
+ redirect_to admin_login_path unless current_user
19
+ end
20
+
21
+ def require_admin
22
+ redirect_to admin_login_path unless current_user&.admin?
23
+ end
24
+
25
+ def sign_in(user)
26
+ session[:user_id] = user.id
27
+ @current_user = user
28
+ end
29
+
30
+ def sign_out
31
+ session.delete(:user_id)
32
+ @current_user = nil
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class ComponentRegistry
6
+ class << self
7
+ def components
8
+ @components ||= {}
9
+ end
10
+
11
+ def register(name, component_class)
12
+ components[name.to_sym] = component_class
13
+ end
14
+
15
+ def unregister(name)
16
+ components.delete(name.to_sym)
17
+ end
18
+
19
+ def get(name)
20
+ components[name.to_sym]
21
+ end
22
+
23
+ def all
24
+ components
25
+ end
26
+
27
+ def clear!
28
+ @components = {}
29
+ end
30
+
31
+ def registered?(name)
32
+ components.key?(name.to_sym)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end