panda-core 0.12.2 → 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 +413 -131
- data/app/builders/panda/core/form_builder.rb +129 -27
- 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 +15 -19
- 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 +37 -32
- 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 +34 -69
- 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 +85 -8
- 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 +96 -18
- 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 -67
- 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 -38
- 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 +6 -3
- 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 +4 -3
- data/lib/panda/core/engine/autoload_config.rb +21 -9
- data/lib/panda/core/engine/omniauth_config.rb +114 -32
- data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
- data/lib/panda/core/engine/route_config.rb +1 -1
- 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/module_registry.rb +21 -13
- 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 +20 -0
- data/lib/tasks/panda/core/file_categories.rake +41 -0
- data/public/panda-core-assets/panda-core-0.12.2.css +2 -0
- data/public/panda-core-assets/panda-core-0.12.3.css +2 -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 +205 -26
- data/lib/panda/core/engine/inflections_config.rb +0 -23
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
|
|
2
|
+
<div class="flex items-center gap-4">
|
|
3
|
+
<%# Provider icon %>
|
|
4
|
+
<div class="flex items-center justify-center w-12 h-12 rounded-lg bg-gray-100">
|
|
5
|
+
<i class="<%= provider_info[:icon] %> text-2xl" style="color: <%= provider_info[:color] %>;"></i>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<%# Provider details %>
|
|
9
|
+
<div>
|
|
10
|
+
<h3 class="font-medium text-gray-900"><%= provider_info[:name] %></h3>
|
|
11
|
+
<% if connected? %>
|
|
12
|
+
<p class="text-sm text-green-600">
|
|
13
|
+
<i class="fa-solid fa-check-circle"></i>
|
|
14
|
+
Connected
|
|
15
|
+
</p>
|
|
16
|
+
<% else %>
|
|
17
|
+
<p class="text-sm text-gray-500">Not connected</p>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<%# Action button %>
|
|
23
|
+
<div>
|
|
24
|
+
<% if connected? %>
|
|
25
|
+
<%= render Panda::Core::Admin::ButtonComponent.new(
|
|
26
|
+
text: "Connected",
|
|
27
|
+
action: :secondary,
|
|
28
|
+
as_button: true,
|
|
29
|
+
disabled: true
|
|
30
|
+
) %>
|
|
31
|
+
<% else %>
|
|
32
|
+
<%= render Panda::Core::Admin::ButtonComponent.new(
|
|
33
|
+
text: "Connect",
|
|
34
|
+
action: :primary,
|
|
35
|
+
href: "#",
|
|
36
|
+
disabled: true,
|
|
37
|
+
title: "OAuth re-connection coming soon"
|
|
38
|
+
) %>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
module MyProfile
|
|
7
|
+
class ConnectedAccountComponent < Panda::Core::Base
|
|
8
|
+
def initialize(provider:, user:)
|
|
9
|
+
@provider = provider
|
|
10
|
+
@user = user
|
|
11
|
+
@provider_info = Panda::Core::OAuthProviders.info(provider)
|
|
12
|
+
# TODO: Track which provider user logged in with
|
|
13
|
+
# For now, we don't track the provider, so we can't show connection status
|
|
14
|
+
@is_connected = user.respond_to?(:oauth_provider) && user.oauth_provider == provider.to_s
|
|
15
|
+
super()
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :provider_info
|
|
19
|
+
|
|
20
|
+
def connected?
|
|
21
|
+
@is_connected
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<li>
|
|
2
|
+
<% if expandable? %>
|
|
3
|
+
<div class="<%= margin_class %>" data-controller="navigation-toggle">
|
|
4
|
+
<div data-navigation-toggle-target="wrapper"
|
|
5
|
+
class="<%= 'rounded-xl bg-white/10 overflow-hidden' if active? %>">
|
|
6
|
+
<button type="button"
|
|
7
|
+
data-navigation-toggle-target="button"
|
|
8
|
+
data-action="click->navigation-toggle#toggle"
|
|
9
|
+
aria-controls="<%= menu_id %>"
|
|
10
|
+
aria-expanded="false"
|
|
11
|
+
class="<%= parent_classes %>">
|
|
12
|
+
<span class="text-center w-6"><i class="<%= icon %> text-xl fa-fw"></i></span>
|
|
13
|
+
<span class="flex-1 text-left"><%= label %></span>
|
|
14
|
+
<%= badge_tag %>
|
|
15
|
+
<i class="fa-solid fa-chevron-right text-xs transition-transform duration-150 ease-in-out"
|
|
16
|
+
data-navigation-toggle-target="icon"></i>
|
|
17
|
+
</button>
|
|
18
|
+
<div id="<%= menu_id %>"
|
|
19
|
+
class="overflow-hidden transition-all duration-200 ease-out <%= active? ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0' %>"
|
|
20
|
+
data-navigation-toggle-target="menu">
|
|
21
|
+
<% sub_items.each do |sub_item| %>
|
|
22
|
+
<%= sub_item %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<% else %>
|
|
28
|
+
<%= helpers.link_to path, class: link_classes, target: target do %>
|
|
29
|
+
<span class="text-center w-6"><i class="<%= icon %> text-xl fa-fw"></i></span>
|
|
30
|
+
<span class="flex-1"><%= label %></span>
|
|
31
|
+
<%= badge_tag %>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</li>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
module Navigation
|
|
7
|
+
# A top-level navigation item
|
|
8
|
+
# Can be a simple link or an expandable menu with children
|
|
9
|
+
class ItemComponent < Panda::Core::Base
|
|
10
|
+
BASE_CLASSES = "transition-all group flex items-center gap-x-3 px-3 " \
|
|
11
|
+
"text-sm font-medium cursor-pointer"
|
|
12
|
+
SPACIOUS_PADDING = "py-2.5"
|
|
13
|
+
COMPACT_PADDING = "py-1.5"
|
|
14
|
+
ACTIVE_CLASSES = "bg-primary-500/20 text-white rounded-xl"
|
|
15
|
+
EXPANDED_BUTTON_CLASSES = "bg-white/15 text-white rounded-t-xl"
|
|
16
|
+
INACTIVE_CLASSES = "text-white/80 hover:bg-white/5 rounded-xl"
|
|
17
|
+
|
|
18
|
+
renders_many :sub_items, SubItemComponent
|
|
19
|
+
|
|
20
|
+
def initialize(label:, icon:, path: nil, active: false, menu_id: nil, target: nil, badge: nil, badge_color: nil, compact: false, **attrs) # rubocop:disable Metrics/ParameterLists
|
|
21
|
+
@label = label
|
|
22
|
+
@icon = icon
|
|
23
|
+
@path = path
|
|
24
|
+
@active = active
|
|
25
|
+
@menu_id = menu_id
|
|
26
|
+
@target = target
|
|
27
|
+
@badge = badge
|
|
28
|
+
@badge_color = badge_color
|
|
29
|
+
@compact = compact
|
|
30
|
+
super(**attrs)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :label, :icon, :path, :active, :menu_id, :target, :badge, :badge_color
|
|
34
|
+
|
|
35
|
+
def active?
|
|
36
|
+
@active
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def expandable?
|
|
40
|
+
sub_items.any?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def padding_class
|
|
44
|
+
@compact ? COMPACT_PADDING : SPACIOUS_PADDING
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def margin_class
|
|
48
|
+
@compact ? "mb-0.5" : "mb-2"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parent_classes
|
|
52
|
+
"#{BASE_CLASSES} #{padding_class} w-full #{active? ? EXPANDED_BUTTON_CLASSES : INACTIVE_CLASSES}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def link_classes
|
|
56
|
+
"#{BASE_CLASSES} #{padding_class} rounded-xl #{margin_class} #{active? ? "#{ACTIVE_CLASSES} relative" : INACTIVE_CLASSES}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def badge_tag
|
|
60
|
+
return unless badge
|
|
61
|
+
helpers.content_tag(:span, badge,
|
|
62
|
+
class: "ml-auto px-1.5 py-0.5 text-[10px] font-semibold rounded-full text-white",
|
|
63
|
+
style: "background-color: #{badge_color || "#52B788"}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% if button? %>
|
|
2
|
+
<%= helpers.button_to path, method: method, **button_options, class: "#{item_classes} text-left" do %>
|
|
3
|
+
<%= label %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<% else %>
|
|
6
|
+
<%= helpers.link_to path, class: item_classes, target: target do %>
|
|
7
|
+
<%= label %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
module Navigation
|
|
7
|
+
# A child item within an expandable navigation menu
|
|
8
|
+
# Can render as a link or a button (for logout, etc.)
|
|
9
|
+
class SubItemComponent < Panda::Core::Base
|
|
10
|
+
BASE_CLASSES = "group flex items-center w-full px-3 py-2 cursor-pointer " \
|
|
11
|
+
"text-sm font-medium transition-colors"
|
|
12
|
+
ACTIVE_CLASSES = "bg-primary-500/20 text-white"
|
|
13
|
+
INACTIVE_CLASSES = "text-white/70 hover:bg-white/5"
|
|
14
|
+
|
|
15
|
+
# rubocop:disable Metrics/ParameterLists
|
|
16
|
+
def initialize(label:, path: nil, active: false, method: nil, button_options: {}, target: nil, **attrs)
|
|
17
|
+
@label = label
|
|
18
|
+
@path = path
|
|
19
|
+
@active = active
|
|
20
|
+
@method = method
|
|
21
|
+
@button_options = button_options
|
|
22
|
+
@target = target
|
|
23
|
+
super(**attrs)
|
|
24
|
+
end
|
|
25
|
+
# rubocop:enable Metrics/ParameterLists
|
|
26
|
+
|
|
27
|
+
attr_reader :label, :path, :active, :method, :button_options, :target
|
|
28
|
+
|
|
29
|
+
def active?
|
|
30
|
+
@active
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def button?
|
|
34
|
+
method.present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def item_classes
|
|
38
|
+
"#{BASE_CLASSES} #{active? ? ACTIVE_CLASSES : INACTIVE_CLASSES}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<li class="mt-auto">
|
|
2
|
+
<div class="mb-2" data-controller="navigation-toggle">
|
|
3
|
+
<div data-navigation-toggle-target="wrapper"
|
|
4
|
+
class="<%= 'rounded-xl bg-white/10 overflow-hidden' if active? %>">
|
|
5
|
+
<button type="button"
|
|
6
|
+
data-navigation-toggle-target="button"
|
|
7
|
+
data-action="click->navigation-toggle#toggle"
|
|
8
|
+
aria-controls="user-menu"
|
|
9
|
+
aria-expanded="false"
|
|
10
|
+
class="<%= button_classes %>">
|
|
11
|
+
<% avatar = user.avatar_url %>
|
|
12
|
+
<% if avatar.present? %>
|
|
13
|
+
<span class="text-center w-6"><img src="<%= avatar %>" alt="<%= user.name %>" class="w-auto h-7 rounded-full object-cover"></span>
|
|
14
|
+
<% else %>
|
|
15
|
+
<span class="text-center w-6"><i class="text-xl fa-solid fa-circle-user fa-fw"></i></span>
|
|
16
|
+
<% end %>
|
|
17
|
+
<span class="flex-1 text-left"><%= user.name %></span>
|
|
18
|
+
<i class="fa-solid fa-chevron-right text-xs transition-transform duration-150 ease-in-out"
|
|
19
|
+
data-navigation-toggle-target="icon"></i>
|
|
20
|
+
</button>
|
|
21
|
+
<div id="user-menu"
|
|
22
|
+
class="overflow-hidden transition-all duration-200 ease-out <%= active? ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0' %>"
|
|
23
|
+
data-navigation-toggle-target="menu">
|
|
24
|
+
<% sub_items.each do |sub_item| %>
|
|
25
|
+
<%= sub_item %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</li>
|
|
@@ -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
|