panda-core 0.12.5 → 1.1.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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +53 -0
- data/app/assets/builds/panda_core_admin.css +2 -0
- data/app/assets/tailwind/application.css +236 -36
- data/app/builders/panda/core/form_builder.rb +120 -18
- data/app/components/panda/core/UI/badge.html.erb +10 -0
- data/app/components/panda/core/UI/badge.rb +14 -30
- data/app/components/panda/core/UI/button.html.erb +5 -0
- data/app/components/panda/core/UI/button.rb +11 -15
- data/app/components/panda/core/UI/card.html.erb +3 -0
- data/app/components/panda/core/UI/card.rb +7 -6
- data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
- data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
- data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
- data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
- data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
- data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
- data/app/components/panda/core/admin/button_component.html.erb +9 -0
- data/app/components/panda/core/admin/button_component.rb +36 -31
- data/app/components/panda/core/admin/callout_component.html.erb +17 -0
- data/app/components/panda/core/admin/callout_component.rb +57 -0
- data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
- data/app/components/panda/core/admin/code_block_component.rb +23 -0
- data/app/components/panda/core/admin/container_component.html.erb +27 -0
- data/app/components/panda/core/admin/container_component.rb +11 -77
- data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
- data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
- data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
- data/app/components/panda/core/admin/delete_button_component.rb +41 -0
- data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
- data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
- data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
- data/app/components/panda/core/admin/dropdown_component.rb +65 -0
- data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
- data/app/components/panda/core/admin/empty_state_component.rb +25 -0
- data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
- data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
- data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
- data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
- data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
- data/app/components/panda/core/admin/flash_message_component.rb +33 -60
- data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
- data/app/components/panda/core/admin/form_error_component.rb +8 -16
- data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_footer_component.rb +65 -0
- data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
- data/app/components/panda/core/admin/form_input_component.rb +21 -13
- data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_section_component.rb +64 -0
- data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
- data/app/components/panda/core/admin/form_select_component.rb +25 -26
- data/app/components/panda/core/admin/heading_component.html.erb +26 -0
- data/app/components/panda/core/admin/heading_component.rb +23 -60
- data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
- data/app/components/panda/core/admin/login_form_component.rb +28 -0
- data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
- data/app/components/panda/core/admin/main_layout_component.rb +19 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
- data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
- data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
- data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
- data/app/components/panda/core/admin/page_header_component.rb +19 -35
- data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
- data/app/components/panda/core/admin/pagination_component.rb +56 -0
- data/app/components/panda/core/admin/panel_component.html.erb +19 -0
- data/app/components/panda/core/admin/panel_component.rb +4 -29
- data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
- data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
- data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
- data/app/components/panda/core/admin/search_bar_component.rb +31 -0
- data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
- data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
- data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
- data/app/components/panda/core/admin/secret_field_component.rb +31 -0
- data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
- data/app/components/panda/core/admin/sidebar_component.rb +55 -0
- data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
- data/app/components/panda/core/admin/slideover_component.rb +31 -70
- data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/core/admin/statistics_component.rb +6 -8
- data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
- data/app/components/panda/core/admin/tab_bar_component.rb +32 -67
- data/app/components/panda/core/admin/table_component.html.erb +82 -0
- data/app/components/panda/core/admin/table_component.rb +54 -87
- data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
- data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
- data/app/components/panda/core/admin/tag_component.html.erb +1 -0
- data/app/components/panda/core/admin/tag_component.rb +51 -20
- data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
- data/app/components/panda/core/admin/tag_input_component.rb +22 -0
- data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
- data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
- data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
- data/app/components/panda/core/admin/user_activity_component.rb +9 -22
- data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
- data/app/components/panda/core/admin/user_display_component.rb +19 -22
- data/app/components/panda/core/base.rb +35 -42
- data/app/components/panda/core/shared/footer_component.html.erb +2 -0
- data/app/components/panda/core/shared/footer_component.rb +11 -0
- data/app/components/panda/core/shared/header_component.html.erb +32 -0
- data/app/components/panda/core/shared/header_component.rb +19 -0
- data/app/constraints/panda/core/admin_constraint.rb +2 -2
- data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
- data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
- data/app/controllers/panda/core/admin/base_controller.rb +62 -3
- data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
- data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
- data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
- data/app/controllers/panda/core/admin/files_controller.rb +230 -0
- data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
- data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
- data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
- data/app/controllers/panda/core/admin/search_controller.rb +15 -0
- data/app/controllers/panda/core/admin/sessions_controller.rb +84 -10
- data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
- data/app/controllers/panda/core/admin/users_controller.rb +184 -0
- data/app/controllers/panda/core/application_controller.rb +13 -1
- data/app/helpers/panda/core/active_storage_helper.rb +24 -0
- data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
- data/app/helpers/panda/core/form_helper.rb +15 -0
- data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
- data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
- data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
- data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
- data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
- data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
- data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
- data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
- data/app/javascript/panda/core/controllers/index.js +36 -0
- data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
- data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
- data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
- data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
- data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
- data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
- data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
- data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
- data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
- data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
- data/app/models/concerns/panda/core/has_metadata.rb +164 -0
- data/app/models/concerns/panda/core/importable.rb +79 -0
- data/app/models/concerns/panda/core/searchable.rb +66 -0
- data/app/models/concerns/panda/core/taggable.rb +37 -0
- data/app/models/panda/core/feature_flag.rb +56 -0
- data/app/models/panda/core/file_categorization.rb +14 -0
- data/app/models/panda/core/file_category.rb +45 -0
- data/app/models/panda/core/import_session.rb +79 -0
- data/app/models/panda/core/presence.rb +24 -0
- data/app/models/panda/core/tag.rb +33 -0
- data/app/models/panda/core/tagging.rb +14 -0
- data/app/models/panda/core/user.rb +94 -16
- data/app/models/panda/core/user_activity.rb +42 -0
- data/app/models/panda/core/user_session.rb +35 -0
- data/app/services/panda/core/attach_avatar_service.rb +15 -3
- data/app/services/panda/core/csv_import_service.rb +65 -0
- data/app/services/panda/core/file_categorizer.rb +52 -0
- data/app/services/panda/core/file_parser.rb +76 -0
- data/app/services/panda/core/invite_user_service.rb +44 -0
- data/app/services/panda/core/presence_service.rb +49 -0
- data/app/views/layouts/panda/core/admin.html.erb +6 -76
- data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
- data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
- data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
- data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
- data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
- data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
- data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
- data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
- data/app/views/panda/core/admin/files/index.html.erb +61 -0
- data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
- data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
- data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
- data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
- data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
- data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
- data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
- data/app/views/panda/core/admin/sessions/new.html.erb +1 -37
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
- data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
- data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
- data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
- data/app/views/panda/core/admin/tags/index.html.erb +41 -0
- data/app/views/panda/core/admin/tags/new.html.erb +4 -0
- data/app/views/panda/core/admin/users/activity.html.erb +90 -0
- data/app/views/panda/core/admin/users/edit.html.erb +48 -0
- data/app/views/panda/core/admin/users/index.html.erb +161 -0
- data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
- data/app/views/panda/core/admin/users/show.html.erb +224 -0
- data/app/views/panda/core/shared/_footer.html.erb +1 -2
- data/app/views/panda/core/shared/_header.html.erb +4 -22
- data/config/brakeman.ignore +68 -31
- data/config/importmap.rb +13 -11
- data/config/routes.rb +49 -2
- data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
- data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
- data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
- data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
- data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
- data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
- data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
- data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
- data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
- data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
- data/lib/generators/panda/core/install_generator.rb +32 -0
- data/lib/generators/panda/core/templates/panda.rb +63 -0
- data/lib/panda/core/asset_loader.rb +5 -2
- data/lib/panda/core/authentication.rb +3 -3
- data/lib/panda/core/configuration.rb +195 -1
- data/lib/panda/core/engine/admin_controller_config.rb +3 -6
- data/lib/panda/core/engine/autoload_config.rb +21 -9
- data/lib/panda/core/engine/omniauth_config.rb +108 -38
- data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
- data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
- data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
- data/lib/panda/core/engine.rb +90 -25
- data/lib/panda/core/middleware.rb +35 -0
- data/lib/panda/core/navigation_registry.rb +455 -0
- data/lib/panda/core/oauth_providers.rb +27 -0
- data/lib/panda/core/permission_registry.rb +89 -0
- data/lib/panda/core/search_registry.rb +69 -0
- data/lib/panda/core/seeds/file_categories.rb +30 -0
- data/lib/panda/core/shared/inflections_config.rb +1 -5
- data/lib/panda/core/testing/rails_helper.rb +87 -0
- data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
- data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
- data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core/widget_registry.rb +50 -0
- data/lib/panda/core.rb +17 -8
- data/lib/tasks/panda/core/file_categories.rake +41 -0
- data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
- data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- data/public/panda-core-assets/vanilla-calendar.css +47 -0
- metadata +203 -26
- data/lib/panda/core/engine/inflections_config.rb +0 -23
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
module Navigation
|
|
7
|
+
# User profile menu component for the sidebar footer
|
|
8
|
+
# Displays user avatar/name with expandable menu for profile, security, and logout
|
|
9
|
+
class UserMenuComponent < Panda::Core::Base
|
|
10
|
+
BASE_CLASSES = "transition-all group flex items-center w-full gap-x-3 px-3 py-2.5 " \
|
|
11
|
+
"text-sm font-medium cursor-pointer"
|
|
12
|
+
EXPANDED_BUTTON_CLASSES = "bg-white/15 text-white rounded-t-xl"
|
|
13
|
+
INACTIVE_CLASSES = "text-white/80 hover:bg-white/5 rounded-xl"
|
|
14
|
+
|
|
15
|
+
renders_many :sub_items, SubItemComponent
|
|
16
|
+
|
|
17
|
+
def initialize(user:, active: false, **attrs)
|
|
18
|
+
@user = user
|
|
19
|
+
@active = active
|
|
20
|
+
super(**attrs)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :user
|
|
24
|
+
|
|
25
|
+
def active?
|
|
26
|
+
@active
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def button_classes
|
|
30
|
+
"#{BASE_CLASSES} #{active? ? EXPANDED_BUTTON_CLASSES : INACTIVE_CLASSES}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<%# Breadcrumbs section %>
|
|
3
|
+
<% if @breadcrumbs %>
|
|
4
|
+
<%= render Panda::Core::Admin::BreadcrumbComponent.new(
|
|
5
|
+
items: @breadcrumbs,
|
|
6
|
+
show_back: @show_back
|
|
7
|
+
) %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<%# Title and actions section %>
|
|
11
|
+
<div class="mt-2 md:flex md:items-center md:justify-between">
|
|
12
|
+
<%# Title %>
|
|
13
|
+
<div class="min-w-0 flex-1">
|
|
14
|
+
<h2 class="text-2xl/7 font-bold text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight dark:text-white">
|
|
15
|
+
<%= @title %>
|
|
16
|
+
</h2>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<%# Action buttons %>
|
|
20
|
+
<% if buttons.any? %>
|
|
21
|
+
<div class="mt-4 flex shrink-0 md:mt-0 md:ml-4">
|
|
22
|
+
<% buttons.each_with_index do |button_data, index| %>
|
|
23
|
+
<%= render create_button(button_data, index) %>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
@@ -33,47 +33,31 @@ module Panda
|
|
|
33
33
|
# end
|
|
34
34
|
#
|
|
35
35
|
class PageHeaderComponent < Panda::Core::Base
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def initialize(**props)
|
|
41
|
-
super
|
|
36
|
+
def initialize(title: "", breadcrumbs: nil, show_back: true, **attrs, &block)
|
|
37
|
+
@title = title
|
|
38
|
+
@breadcrumbs = breadcrumbs
|
|
39
|
+
@show_back = show_back
|
|
42
40
|
@buttons = []
|
|
41
|
+
@setup_block = block
|
|
42
|
+
super(**attrs)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
# Allow buttons to be defined via block
|
|
47
|
-
instance_eval(&block) if block_given?
|
|
48
|
-
|
|
49
|
-
div do
|
|
50
|
-
# Breadcrumbs section
|
|
51
|
-
if @breadcrumbs
|
|
52
|
-
render Panda::Core::Admin::BreadcrumbComponent.new(
|
|
53
|
-
items: @breadcrumbs,
|
|
54
|
-
show_back: @show_back
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Title and actions section
|
|
59
|
-
div(class: "mt-2 md:flex md:items-center md:justify-between") do
|
|
60
|
-
# Title
|
|
61
|
-
div(class: "min-w-0 flex-1") do
|
|
62
|
-
h2(class: "text-2xl/7 font-bold text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight dark:text-white") do
|
|
63
|
-
@title
|
|
64
|
-
end
|
|
65
|
-
end
|
|
45
|
+
attr_reader :title, :breadcrumbs, :show_back
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
47
|
+
# Lazy accessor that ensures buttons are registered before returning them.
|
|
48
|
+
# Supports two patterns:
|
|
49
|
+
# 1. Block passed to new() - executed here (for tests)
|
|
50
|
+
# 2. Block passed to render() - executed via content (for ERB templates)
|
|
51
|
+
def buttons
|
|
52
|
+
unless @buttons_registered
|
|
53
|
+
if @setup_block
|
|
54
|
+
@setup_block.call(self)
|
|
55
|
+
else
|
|
56
|
+
content
|
|
75
57
|
end
|
|
58
|
+
@buttons_registered = true
|
|
76
59
|
end
|
|
60
|
+
@buttons
|
|
77
61
|
end
|
|
78
62
|
|
|
79
63
|
# Define a button to be rendered in the header actions area
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<nav aria-label="Pagination" class="mt-4 flex items-center justify-between border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
2
|
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
3
|
+
Showing <%= first_item %> to <%= last_item %> of <%= total_count %> <%= item_name %>
|
|
4
|
+
</div>
|
|
5
|
+
<div class="flex gap-1">
|
|
6
|
+
<% if previous_page? %>
|
|
7
|
+
<%= link_to page_url(page - 1),
|
|
8
|
+
class: "rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800",
|
|
9
|
+
"aria-label": "Previous page" do %>
|
|
10
|
+
<i class="fa-solid fa-chevron-left"></i>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<% (1..total_pages).each do |page_num| %>
|
|
15
|
+
<% if page_num == page %>
|
|
16
|
+
<span class="rounded-md bg-primary-600 px-3 py-2 text-sm font-medium text-white" aria-current="page"><%= page_num %></span>
|
|
17
|
+
<% elsif show_page?(page_num) %>
|
|
18
|
+
<%= link_to page_num, page_url(page_num),
|
|
19
|
+
class: "rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800" %>
|
|
20
|
+
<% elsif show_ellipsis?(page_num) %>
|
|
21
|
+
<span class="px-2 py-2 text-sm text-gray-400">…</span>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<% if next_page? %>
|
|
26
|
+
<%= link_to page_url(page + 1),
|
|
27
|
+
class: "rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800",
|
|
28
|
+
"aria-label": "Next page" do %>
|
|
29
|
+
<i class="fa-solid fa-chevron-right"></i>
|
|
30
|
+
<% end %>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
</nav>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class PaginationComponent < Panda::Core::Base
|
|
7
|
+
def initialize(page:, total_pages:, total_count:, per_page:, item_name: "items", **attrs)
|
|
8
|
+
@page = page
|
|
9
|
+
@total_pages = total_pages
|
|
10
|
+
@total_count = total_count
|
|
11
|
+
@per_page = per_page
|
|
12
|
+
@item_name = item_name
|
|
13
|
+
super(**attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :page, :total_pages, :total_count, :per_page, :item_name
|
|
17
|
+
|
|
18
|
+
def render?
|
|
19
|
+
total_pages > 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def first_item
|
|
23
|
+
((page - 1) * per_page) + 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def last_item
|
|
27
|
+
[page * per_page, total_count].min
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def previous_page?
|
|
31
|
+
page > 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def next_page?
|
|
35
|
+
page < total_pages
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def show_page?(page_num)
|
|
39
|
+
page_num == page ||
|
|
40
|
+
(page_num - page).abs <= 2 ||
|
|
41
|
+
page_num == 1 ||
|
|
42
|
+
page_num == total_pages
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def show_ellipsis?(page_num)
|
|
46
|
+
(page_num - page).abs == 3
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def page_url(page_num)
|
|
50
|
+
query = helpers.request.query_parameters.merge("page" => page_num)
|
|
51
|
+
"#{helpers.request.path}?#{query.to_query}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%# Chrome (bg/border/radius) is themeable via --panda-panel-* tokens — see
|
|
2
|
+
app/assets/tailwind/application.css. Fallbacks resolve to today's
|
|
3
|
+
bg-white/border-gray-200/rounded-2xl exactly, so this is a no-op unless a
|
|
4
|
+
host app sets data-theme. %>
|
|
5
|
+
<div class="col-span-3 mt-4 rounded-[var(--panda-panel-radius,var(--radius-2xl))] border border-[var(--panda-panel-border,var(--color-gray-200))] bg-[var(--panda-panel-bg,var(--color-white))] shadow-sm">
|
|
6
|
+
<% if heading_slot? %>
|
|
7
|
+
<div class="flex items-center text-sm font-medium px-4 py-3 text-gray-500 truncate border-b border-[var(--panda-panel-border,var(--color-gray-200))]">
|
|
8
|
+
<%= heading_slot %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<div class="px-4 py-3 text-black">
|
|
13
|
+
<% if body_slot? %>
|
|
14
|
+
<%= body_slot %>
|
|
15
|
+
<% else %>
|
|
16
|
+
<%= content %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
@@ -4,37 +4,12 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class PanelComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
# Capture block content differently based on context (ERB vs Phlex)
|
|
9
|
-
if block_given?
|
|
10
|
-
if defined?(view_context) && view_context
|
|
11
|
-
# Called from ERB - capture HTML output
|
|
12
|
-
@body_html = view_context.capture { yield(self) }
|
|
13
|
-
else
|
|
14
|
-
# Called from Phlex - execute block directly to set instance variables
|
|
15
|
-
yield(self)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
7
|
+
renders_one :heading_slot
|
|
18
8
|
|
|
19
|
-
|
|
20
|
-
@heading_content&.call
|
|
9
|
+
renders_one :body_slot
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@body_content.call
|
|
25
|
-
elsif @body_html
|
|
26
|
-
raw(@body_html)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def heading(**props)
|
|
33
|
-
@heading_content = -> { render(Panda::Core::Admin::HeadingComponent.new(**props.merge(level: :panel))) }
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def body(&block)
|
|
37
|
-
@body_content = block
|
|
11
|
+
def initialize(**attrs)
|
|
12
|
+
super
|
|
38
13
|
end
|
|
39
14
|
end
|
|
40
15
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Renders a PanelComponent containing prose-formatted text via `simple_format`.
|
|
7
|
+
# Self-hides via `render?` when text is blank.
|
|
8
|
+
#
|
|
9
|
+
# Lives in panda-core because rich-text prose display inside panels is a generic
|
|
10
|
+
# admin pattern (notes, descriptions, agendas) used across any Panda app, not
|
|
11
|
+
# specific to any single domain.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# render Panda::Core::Admin::ProsePanelComponent.new(heading: "Notes", text: @person.notes)
|
|
15
|
+
#
|
|
16
|
+
# @example With extra prose styling
|
|
17
|
+
# render Panda::Core::Admin::ProsePanelComponent.new(
|
|
18
|
+
# heading: "Internal Notes",
|
|
19
|
+
# text: @case.notes,
|
|
20
|
+
# prose_class: "text-gray-600"
|
|
21
|
+
# )
|
|
22
|
+
class ProsePanelComponent < Panda::Core::Base
|
|
23
|
+
attr_reader :heading, :text, :prose_class
|
|
24
|
+
|
|
25
|
+
def initialize(heading:, text:, prose_class: "", **attrs)
|
|
26
|
+
@heading = heading
|
|
27
|
+
@text = text
|
|
28
|
+
@prose_class = prose_class
|
|
29
|
+
super(**attrs)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def render?
|
|
33
|
+
text.present?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prose_classes
|
|
37
|
+
base = "prose prose-sm max-w-none"
|
|
38
|
+
prose_class.present? ? "#{base} #{prose_class}" : base
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<div class="relative px-2 mb-2" data-controller="global-search" data-global-search-url-value="<%= search_url %>">
|
|
2
|
+
<div class="relative">
|
|
3
|
+
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-white/40 text-xs pointer-events-none"></i>
|
|
4
|
+
<input
|
|
5
|
+
type="text"
|
|
6
|
+
placeholder="Search..."
|
|
7
|
+
autocomplete="off"
|
|
8
|
+
data-global-search-target="input"
|
|
9
|
+
data-action="input->global-search#onInput keydown->global-search#onInputKeydown focus->global-search#onFocus"
|
|
10
|
+
class="w-full rounded-lg border border-white/10 bg-white/5 pl-9 pr-12 py-2 text-sm text-white placeholder-white/40 focus:bg-white/10 focus:border-white/20 focus:outline-none focus:ring-0 transition-colors"
|
|
11
|
+
/>
|
|
12
|
+
<kbd class="absolute right-2.5 top-1/2 -translate-y-1/2 text-[10px] text-white/30 border border-white/15 rounded px-1.5 py-0.5 pointer-events-none"><%= shortcut_hint %></kbd>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
data-global-search-target="results"
|
|
17
|
+
class="hidden absolute left-2 right-2 top-full mt-1 max-h-80 overflow-y-auto rounded-xl border border-white/10 bg-gray-900/95 backdrop-blur-sm shadow-xl z-50"
|
|
18
|
+
></div>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class SearchBarComponent < Panda::Core::Base
|
|
7
|
+
def initialize(**attrs)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def render?
|
|
12
|
+
Panda::Core::SearchRegistry.providers.any? { |p| p[:search_class].respond_to?(:admin_search) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def search_url
|
|
16
|
+
Panda::Core::Engine.routes.url_helpers.admin_search_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def shortcut_hint
|
|
20
|
+
mac? ? "\u2318K" : "Ctrl+K"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def mac?
|
|
26
|
+
true # Safe default for shortcut display; JS handles both
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
2
|
+
<%= form_with url: url, method: :get, local: true, class: "flex flex-col gap-3 sm:flex-row sm:items-center w-full" do %>
|
|
3
|
+
<div class="flex-1 max-w-md grid grid-cols-1">
|
|
4
|
+
<%= text_field_tag search_name, search_value,
|
|
5
|
+
placeholder: search_placeholder,
|
|
6
|
+
class: "col-start-1 row-start-1 block w-full h-9 rounded-md bg-white py-1.5 pr-3 pl-9 text-sm text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-600 dark:bg-white/5 dark:text-white dark:ring-white/10 dark:placeholder:text-gray-500 dark:focus:outline-primary-500" %>
|
|
7
|
+
<i class="fa-solid fa-magnifying-glass pointer-events-none col-start-1 row-start-1 ml-3 size-4 self-center text-gray-400 dark:text-gray-500"></i>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="flex gap-2 items-center">
|
|
11
|
+
<% filters.each do |filter| %>
|
|
12
|
+
<%= filter %>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<%= submit_tag "Filter", class: "h-9 rounded-md bg-white px-3 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-700 dark:text-white dark:ring-gray-600 dark:hover:bg-gray-600 cursor-pointer" %>
|
|
16
|
+
|
|
17
|
+
<% if show_clear %>
|
|
18
|
+
<%= link_to "Clear", clear_url, class: "rounded-md px-3 py-1.5 text-sm font-semibold text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300" %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class SearchFilterBarComponent < Panda::Core::Base
|
|
7
|
+
renders_many :filters
|
|
8
|
+
|
|
9
|
+
def initialize(url:, search_name: :q, search_value: nil, search_placeholder: "Search...", clear_url: nil, show_clear: false, **attrs)
|
|
10
|
+
@url = url
|
|
11
|
+
@search_name = search_name
|
|
12
|
+
@search_value = search_value
|
|
13
|
+
@search_placeholder = search_placeholder
|
|
14
|
+
@clear_url = clear_url || url
|
|
15
|
+
@show_clear = show_clear
|
|
16
|
+
super(**attrs)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :url, :search_name, :search_value, :search_placeholder, :clear_url, :show_clear
|
|
20
|
+
|
|
21
|
+
def select_classes
|
|
22
|
+
"h-9 w-auto rounded-md border-0 py-1.5 pl-3 pr-10 text-sm text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary-600 dark:bg-gray-700 dark:text-white dark:ring-gray-600"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<div data-controller="clipboard"
|
|
2
|
+
data-clipboard-secret-value="<%= value %>"
|
|
3
|
+
data-clipboard-masked-value="<%= masked %>"
|
|
4
|
+
class="bg-gray-50 rounded-lg p-3 font-mono text-sm">
|
|
5
|
+
<div class="flex items-center gap-3">
|
|
6
|
+
<code data-clipboard-target="display" class="text-gray-700 break-all flex-1"><%= display_value %></code>
|
|
7
|
+
<button type="button"
|
|
8
|
+
data-action="click->clipboard#toggleReveal"
|
|
9
|
+
data-clipboard-target="revealButton"
|
|
10
|
+
class="<%= button_classes %>"
|
|
11
|
+
aria-label="Reveal secret value">
|
|
12
|
+
<i data-clipboard-target="revealIcon" class="fa-solid fa-eye"></i>
|
|
13
|
+
<span data-clipboard-target="revealText">Reveal</span>
|
|
14
|
+
</button>
|
|
15
|
+
<button type="button"
|
|
16
|
+
data-action="click->clipboard#copy"
|
|
17
|
+
data-clipboard-target="copyButton"
|
|
18
|
+
class="<%= button_classes %>"
|
|
19
|
+
aria-label="Copy secret value to clipboard">
|
|
20
|
+
<i data-clipboard-target="copyIcon" class="fa-solid fa-copy"></i>
|
|
21
|
+
<span data-clipboard-target="copyText">Copy</span>
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class SecretFieldComponent < Panda::Core::Base
|
|
7
|
+
def initialize(value:, masked: true, **attrs)
|
|
8
|
+
@value = value
|
|
9
|
+
@masked = masked
|
|
10
|
+
super(**attrs)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :value, :masked
|
|
14
|
+
|
|
15
|
+
def display_value
|
|
16
|
+
return value unless masked && value.present?
|
|
17
|
+
|
|
18
|
+
if value.length > 4
|
|
19
|
+
"\u2022" * 12 + value[-4..]
|
|
20
|
+
else
|
|
21
|
+
"\u2022" * value.length
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def button_classes
|
|
26
|
+
"shrink-0 btn btn-secondary transition"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<nav <%= tag.attributes(**attrs) %>>
|
|
2
|
+
<ul role="list" class="flex flex-col flex-1">
|
|
3
|
+
<%# Logo area — uses admin_logo config or falls back to admin_title text %>
|
|
4
|
+
<li class="hidden lg:block mt-4 mb-4 px-2">
|
|
5
|
+
<% if admin_logo %>
|
|
6
|
+
<div class="flex items-center justify-between">
|
|
7
|
+
<div class="flex items-center gap-2">
|
|
8
|
+
<%= safe_html(admin_logo) %>
|
|
9
|
+
</div>
|
|
10
|
+
<% if admin_logo_actions.any? || admin_settings_path %>
|
|
11
|
+
<div class="flex items-center gap-1">
|
|
12
|
+
<% admin_logo_actions.each do |action| %>
|
|
13
|
+
<%= helpers.link_to action[:path], class: "flex items-center justify-center w-7 h-7 rounded-lg text-white/40 hover:text-white hover:bg-white/10 transition-colors", title: action[:title] do %>
|
|
14
|
+
<i class="<%= action[:icon] %> text-sm"></i>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if admin_settings_path %>
|
|
18
|
+
<%= helpers.link_to admin_settings_path, class: "flex items-center justify-center w-7 h-7 rounded-lg text-white/40 hover:text-white hover:bg-white/10 transition-colors", title: "Settings" do %>
|
|
19
|
+
<i class="fa-solid fa-gear text-sm"></i>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
</div>
|
|
25
|
+
<% else %>
|
|
26
|
+
<a class="p-0 text-xl font-medium text-white"><%= Panda::Core.config.admin_title || "Panda Admin" %></a>
|
|
27
|
+
<% end %>
|
|
28
|
+
</li>
|
|
29
|
+
|
|
30
|
+
<%= render Panda::Core::Admin::SearchBarComponent.new %>
|
|
31
|
+
|
|
32
|
+
<%
|
|
33
|
+
nav_items = Panda::Core::NavigationRegistry.build(@user, helpers: helpers)
|
|
34
|
+
|
|
35
|
+
top_items = nav_items.select { |item| item[:position] != :bottom }
|
|
36
|
+
bottom_items = nav_items.select { |item| item[:position] == :bottom }
|
|
37
|
+
|
|
38
|
+
def collect_paths(items)
|
|
39
|
+
items.flat_map do |item|
|
|
40
|
+
paths = item[:path] ? [item[:path]] : []
|
|
41
|
+
paths += collect_paths(item[:children] || [])
|
|
42
|
+
paths
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
all_paths = collect_paths(nav_items).compact
|
|
47
|
+
current_path = helpers.request&.path || "/"
|
|
48
|
+
active_path = all_paths
|
|
49
|
+
.sort_by { |path| -path.length }
|
|
50
|
+
.find { |path| current_path == path || current_path.starts_with?(path + "/") }
|
|
51
|
+
%>
|
|
52
|
+
|
|
53
|
+
<% if sectioned?(top_items) %>
|
|
54
|
+
<%# Section-aware navigation — insert headings when section changes, preserving order %>
|
|
55
|
+
<% current_section = nil %>
|
|
56
|
+
<% section_count = 0 %>
|
|
57
|
+
<% top_items.each_with_index do |item, index| %>
|
|
58
|
+
<% item_section = item[:section].presence %>
|
|
59
|
+
<% if item_section && item_section != current_section %>
|
|
60
|
+
<% current_section = item_section %>
|
|
61
|
+
<li class="px-3 <%= section_count > 0 ? (compact_navigation? ? 'mt-3' : 'mt-5') : '' %> <%= compact_navigation? ? 'mb-0.5' : 'mb-1' %>">
|
|
62
|
+
<span class="text-[10px] font-bold uppercase tracking-widest text-white/40 sidebar-section-label"><%= current_section %></span>
|
|
63
|
+
</li>
|
|
64
|
+
<% section_count += 1 %>
|
|
65
|
+
<% end %>
|
|
66
|
+
<%
|
|
67
|
+
has_children = item[:children].present?
|
|
68
|
+
if has_children
|
|
69
|
+
is_active = item[:children].any? { |child| child[:path] == active_path }
|
|
70
|
+
else
|
|
71
|
+
is_active = item[:path] == active_path
|
|
72
|
+
end
|
|
73
|
+
%>
|
|
74
|
+
<%= render Panda::Core::Admin::Navigation::ItemComponent.new(
|
|
75
|
+
label: item[:label],
|
|
76
|
+
icon: item[:icon],
|
|
77
|
+
path: item[:path],
|
|
78
|
+
active: is_active,
|
|
79
|
+
menu_id: "sub-menu-#{section_count}-#{index}",
|
|
80
|
+
target: item[:target],
|
|
81
|
+
badge: item[:badge],
|
|
82
|
+
badge_color: item[:badge_color],
|
|
83
|
+
compact: compact_navigation?
|
|
84
|
+
) do |nav_item|
|
|
85
|
+
if has_children
|
|
86
|
+
item[:children].each do |child|
|
|
87
|
+
child_is_active = child[:path] == active_path
|
|
88
|
+
nav_item.with_sub_item(label: child[:label], path: child[:path], active: child_is_active, target: child[:target])
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end %>
|
|
92
|
+
<% end %>
|
|
93
|
+
<% else %>
|
|
94
|
+
<%# Legacy flat navigation (no section keys) %>
|
|
95
|
+
<% top_items.each_with_index do |item, index| %>
|
|
96
|
+
<%
|
|
97
|
+
has_children = item[:children].present?
|
|
98
|
+
if has_children
|
|
99
|
+
is_active = item[:children].any? { |child| child[:path] == active_path }
|
|
100
|
+
else
|
|
101
|
+
is_active = item[:path] == active_path
|
|
102
|
+
end
|
|
103
|
+
%>
|
|
104
|
+
<%= render Panda::Core::Admin::Navigation::ItemComponent.new(
|
|
105
|
+
label: item[:label],
|
|
106
|
+
icon: item[:icon],
|
|
107
|
+
path: item[:path],
|
|
108
|
+
active: is_active,
|
|
109
|
+
menu_id: "sub-menu-#{index}",
|
|
110
|
+
target: item[:target],
|
|
111
|
+
badge: item[:badge],
|
|
112
|
+
badge_color: item[:badge_color],
|
|
113
|
+
compact: compact_navigation?
|
|
114
|
+
) do |nav_item|
|
|
115
|
+
if has_children
|
|
116
|
+
item[:children].each do |child|
|
|
117
|
+
child_is_active = child[:path] == active_path
|
|
118
|
+
nav_item.with_sub_item(label: child[:label], path: child[:path], active: child_is_active, target: child[:target])
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end %>
|
|
122
|
+
<% end %>
|
|
123
|
+
<% end %>
|
|
124
|
+
|
|
125
|
+
<% bottom_items.each do |section| %>
|
|
126
|
+
<%
|
|
127
|
+
is_section_active = (section[:children] || []).any? { |child|
|
|
128
|
+
child[:path] && current_path.starts_with?(child[:path])
|
|
129
|
+
}
|
|
130
|
+
%>
|
|
131
|
+
<%= render Panda::Core::Admin::Navigation::UserMenuComponent.new(user: helpers.current_user, active: is_section_active) do |user_menu|
|
|
132
|
+
(section[:children] || []).each do |item|
|
|
133
|
+
item_path = item[:path]
|
|
134
|
+
if item_path.nil? && item[:path_helper]
|
|
135
|
+
helper_name = item[:path_helper].to_sym
|
|
136
|
+
item_path = helpers.panda_core.public_send(helper_name) if helpers.panda_core.respond_to?(helper_name)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
item_active = item_path && current_path.starts_with?(item_path)
|
|
140
|
+
user_menu.with_sub_item(
|
|
141
|
+
label: item[:label],
|
|
142
|
+
path: item_path,
|
|
143
|
+
active: item_active,
|
|
144
|
+
method: item[:method],
|
|
145
|
+
button_options: item[:button_options] || {}
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end %>
|
|
149
|
+
<% end %>
|
|
150
|
+
|
|
151
|
+
<% sidebar_footer = Panda::Core.config.admin_sidebar_footer %>
|
|
152
|
+
<li class="px-2 py-3 border-t border-white/10">
|
|
153
|
+
<% if sidebar_footer %>
|
|
154
|
+
<%= safe_html(sidebar_footer) %>
|
|
155
|
+
<% else %>
|
|
156
|
+
<span class="text-xs text-white/30">Panda Core v<%= Panda::Core::VERSION %></span>
|
|
157
|
+
<% end %>
|
|
158
|
+
</li>
|
|
159
|
+
</ul>
|
|
160
|
+
</nav>
|