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
|
@@ -4,22 +4,18 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class FormSelectComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def view_template
|
|
16
|
-
select(**@attrs) do
|
|
17
|
-
render_prompt if @prompt
|
|
18
|
-
render_blank if @include_blank && !@prompt
|
|
19
|
-
render_options
|
|
20
|
-
end
|
|
7
|
+
def initialize(prompt:, name: "", options: [], required: false, disabled: false, include_blank: false, **attrs)
|
|
8
|
+
@name = name
|
|
9
|
+
@options = options
|
|
10
|
+
@prompt = prompt
|
|
11
|
+
@required = required
|
|
12
|
+
@disabled = disabled
|
|
13
|
+
@include_blank = include_blank
|
|
14
|
+
super(**attrs)
|
|
21
15
|
end
|
|
22
16
|
|
|
17
|
+
attr_reader :name, :options, :prompt, :required, :disabled, :include_blank
|
|
18
|
+
|
|
23
19
|
def default_attrs
|
|
24
20
|
base_attrs = {
|
|
25
21
|
name: @name,
|
|
@@ -36,31 +32,34 @@ module Panda
|
|
|
36
32
|
private
|
|
37
33
|
|
|
38
34
|
def select_classes
|
|
39
|
-
classes = "block w-full rounded-
|
|
35
|
+
classes = "block w-full h-11 rounded-xl border border-gray-300 bg-white px-3 py-2 text-gray-900 " \
|
|
36
|
+
"focus:outline-2 focus:-outline-offset-2 focus:outline-primary-500 " \
|
|
37
|
+
"dark:bg-white/5 dark:text-white dark:border-white/10"
|
|
40
38
|
|
|
41
39
|
if @disabled
|
|
42
|
-
classes + "
|
|
40
|
+
classes + " border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
43
41
|
else
|
|
44
|
-
classes
|
|
42
|
+
classes
|
|
45
43
|
end
|
|
46
44
|
end
|
|
47
45
|
|
|
48
46
|
def render_prompt
|
|
49
|
-
option
|
|
47
|
+
content_tag(:option, @prompt, value: "", disabled: true, selected: @selected.nil?)
|
|
50
48
|
end
|
|
51
49
|
|
|
52
50
|
def render_blank
|
|
53
|
-
|
|
51
|
+
content_tag(:option, "", value: "")
|
|
54
52
|
end
|
|
55
53
|
|
|
56
54
|
def render_options
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
safe_join(
|
|
56
|
+
@options.map do |option_data|
|
|
57
|
+
label, value = option_data
|
|
58
|
+
option_attrs = {value: value.to_s}
|
|
59
|
+
option_attrs[:selected] = true if value.to_s == @selected.to_s
|
|
60
|
+
content_tag(:option, label.to_s, **option_attrs)
|
|
61
|
+
end
|
|
62
|
+
)
|
|
64
63
|
end
|
|
65
64
|
end
|
|
66
65
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div class="heading-wrapper">
|
|
2
|
+
<% heading_tag = case level when 1 then :h1 when 2 then :h2 when 3 then :h3 when :panel then :h3 else :h2 end %>
|
|
3
|
+
|
|
4
|
+
<%= tag.send(heading_tag, class: heading_classes) do %>
|
|
5
|
+
<div class="grow flex items-center gap-x-2">
|
|
6
|
+
<% if icon.present? %>
|
|
7
|
+
<%= tag.i(class: icon) %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<span><%= text %></span>
|
|
10
|
+
</div>
|
|
11
|
+
<% if buttons.any? || dropdown_buttons.any? %>
|
|
12
|
+
<span class="actions flex gap-x-2 mt-1 min-h-[2.5rem]">
|
|
13
|
+
<% dropdown_buttons.each do |dropdown_button| %>
|
|
14
|
+
<%= dropdown_button %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% buttons.each do |button| %>
|
|
17
|
+
<%= button %>
|
|
18
|
+
<% end %>
|
|
19
|
+
</span>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<% if meta %>
|
|
24
|
+
<%= tag.p(raw(meta), class: "text-sm text-black/60 -mt-1 mb-5") %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
@@ -4,78 +4,41 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class HeadingComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
when 2
|
|
22
|
-
h2(class: heading_classes(@meta.present?)) { render_content }
|
|
23
|
-
when 3
|
|
24
|
-
h3(class: heading_classes(@meta.present?)) { render_content }
|
|
25
|
-
when :panel
|
|
26
|
-
h3(class: panel_heading_classes) { @text }
|
|
27
|
-
else
|
|
28
|
-
h2(class: heading_classes(@meta.present?)) { render_content }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
if @meta
|
|
32
|
-
p(class: "text-sm text-black/60 -mt-1 mb-5") { raw(@meta) }
|
|
33
|
-
end
|
|
34
|
-
end
|
|
7
|
+
renders_many :buttons, Panda::Core::Admin::ButtonComponent
|
|
8
|
+
renders_many :dropdown_buttons, Panda::Core::Admin::DropdownButtonComponent
|
|
9
|
+
|
|
10
|
+
def initialize(text: "", icon: "", meta: nil, level: 2, additional_styles: nil, **attrs, &block)
|
|
11
|
+
@text = text
|
|
12
|
+
@icon = icon
|
|
13
|
+
@meta = meta
|
|
14
|
+
@level = level
|
|
15
|
+
@additional_styles = additional_styles
|
|
16
|
+
super(**attrs)
|
|
17
|
+
# Execute block if passed to new() directly (for tests/direct instantiation)
|
|
18
|
+
# This allows: HeadingComponent.new(text: "Pages") { |h| h.with_button(...) }
|
|
19
|
+
# Note: When used via render() with a block, ViewComponent handles the block separately
|
|
20
|
+
yield self if block_given?
|
|
35
21
|
end
|
|
36
22
|
|
|
37
|
-
|
|
38
|
-
@buttons ||= []
|
|
39
|
-
@buttons << Panda::Core::Admin::ButtonComponent.new(**props)
|
|
40
|
-
end
|
|
23
|
+
attr_reader :text, :icon, :meta, :level
|
|
41
24
|
|
|
42
25
|
private
|
|
43
26
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
span { @text }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
span(class: "actions flex gap-x-2 mt-1 min-h-[2.5rem]") do
|
|
51
|
-
@buttons&.each { |btn| render(btn) }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def heading_classes(has_meta = false)
|
|
56
|
-
margin_bottom = has_meta ? "mb-0.5" : "mb-5"
|
|
57
|
-
base = "flex text-black #{margin_bottom} -mt-2"
|
|
27
|
+
def heading_classes
|
|
28
|
+
margin_bottom = @meta.present? ? "mb-0.5" : "mb-5"
|
|
29
|
+
base = "flex items-center gap-2 text-gray-900 #{margin_bottom}"
|
|
58
30
|
styles = case @level
|
|
59
31
|
when 1
|
|
60
|
-
"text-2xl font-
|
|
32
|
+
"text-2xl font-semibold"
|
|
61
33
|
when 2
|
|
62
|
-
"text-xl font-
|
|
34
|
+
"text-xl font-semibold"
|
|
63
35
|
when 3
|
|
64
|
-
"text-
|
|
36
|
+
"text-lg font-medium"
|
|
65
37
|
else
|
|
66
|
-
"text-xl font-
|
|
38
|
+
"text-xl font-semibold"
|
|
67
39
|
end
|
|
68
40
|
|
|
69
|
-
[base, styles,
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def panel_heading_classes
|
|
73
|
-
"text-base font-medium px-4 py-3 text-white"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def additional_styles_array
|
|
77
|
-
return [] if @additional_styles.blank?
|
|
78
|
-
@additional_styles.is_a?(String) ? @additional_styles.split(" ") : @additional_styles
|
|
41
|
+
[base, styles, @additional_styles].compact.join(" ")
|
|
79
42
|
end
|
|
80
43
|
end
|
|
81
44
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<div <%= tag.attributes(**attrs) %>>
|
|
2
|
+
<div class="text-center sm:mx-auto sm:w-full sm:max-w-sm">
|
|
3
|
+
<% if Panda::Core.config.login_logo_path %>
|
|
4
|
+
<img src="<%= Panda::Core.config.login_logo_path %>" class="py-2 mx-auto w-auto h-32">
|
|
5
|
+
<% end %>
|
|
6
|
+
<h2 class="mt-10 mb-6 text-2xl font-bold text-center text-white">
|
|
7
|
+
<%= Panda::Core.config.login_page_title || "Sign in to your account" %>
|
|
8
|
+
</h2>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<% if @providers&.any? %>
|
|
12
|
+
<% @providers.each do |provider| %>
|
|
13
|
+
<% provider_config = Panda::Core.config.authentication_providers[provider] %>
|
|
14
|
+
<% provider_path = provider_config&.dig(:path_name) || provider %>
|
|
15
|
+
<div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
|
|
16
|
+
<%= helpers.form_tag "#{Panda::Core.config.admin_path}/auth/#{provider_path}", method: "post", data: {turbo: false} do %>
|
|
17
|
+
<button type="submit" id="button-sign-in-<%= provider_path %>" class="inline-flex gap-x-2 items-center py-2.5 px-3.5 mx-auto mb-4 bg-white text-gray-900 rounded-md border min-w-56 border-neutral-400 hover:bg-gray-50">
|
|
18
|
+
<i class="<%= oauth_provider_non_brand?(provider) ? 'fa-solid' : 'fa-brands' %> fa-<%= oauth_provider_icon(provider) %> text-xl mr-1"></i>
|
|
19
|
+
Sign in with <%= oauth_provider_name(provider, provider_config) %>
|
|
20
|
+
</button>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% else %>
|
|
25
|
+
<div class="rounded-md bg-yellow-50 p-4 sm:mx-auto sm:w-full sm:max-w-sm">
|
|
26
|
+
<div class="flex">
|
|
27
|
+
<div class="ml-3">
|
|
28
|
+
<h3 class="text-sm font-medium text-yellow-800">
|
|
29
|
+
No authentication providers configured
|
|
30
|
+
</h3>
|
|
31
|
+
<div class="mt-2 text-sm text-yellow-700">
|
|
32
|
+
<p>Please configure at least one authentication provider in your Panda Core configuration.</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Login form component for OAuth provider authentication
|
|
7
|
+
# Displays configured authentication providers for user login
|
|
8
|
+
class LoginFormComponent < Panda::Core::Base
|
|
9
|
+
include Panda::Core::SessionsHelper
|
|
10
|
+
|
|
11
|
+
def initialize(providers: [], **attrs)
|
|
12
|
+
@providers = providers
|
|
13
|
+
super(**attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :providers
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def default_attrs
|
|
21
|
+
{
|
|
22
|
+
class: "flex flex-col justify-center py-12 px-6 min-h-full text-center lg:px-8"
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<%= render Panda::Core::Shared::HeaderComponent.new(html_class: "h-full", body_class: "") %>
|
|
2
|
+
|
|
3
|
+
<div class="flex h-full" id="panda-container">
|
|
4
|
+
<div data-controller="mobile-sidebar" data-action="keydown.esc@window->mobile-sidebar#close">
|
|
5
|
+
<div data-mobile-sidebar-target="backdrop" class="hidden fixed inset-0 bg-gray-900/50 z-40 lg:hidden" data-action="click->mobile-sidebar#close"></div>
|
|
6
|
+
<div class="absolute top-0 w-full lg:flex lg:fixed lg:inset-y-0 z-50 lg:flex-col lg:w-72">
|
|
7
|
+
<div data-mobile-sidebar-target="sidebar" class="flex overflow-y-auto flex-col gap-y-5 px-4 pb-4 max-h-16 bg-gradient-admin lg:max-h-full grow">
|
|
8
|
+
<div class="flex items-center justify-between pt-4 -mb-5 lg:hidden">
|
|
9
|
+
<button data-mobile-sidebar-target="hamburger" data-action="click->mobile-sidebar#toggle" type="button" class="text-white/80 hover:text-white p-1" aria-expanded="false" aria-label="Toggle navigation">
|
|
10
|
+
<i class="fa-solid fa-bars text-xl"></i>
|
|
11
|
+
</button>
|
|
12
|
+
<span class="text-white font-medium text-lg"><%= Panda::Core.config.admin_title || "Panda Admin" %></span>
|
|
13
|
+
<span class="w-7"></span>
|
|
14
|
+
</div>
|
|
15
|
+
<%= render Panda::Core::Admin::SidebarComponent.new(user: @user) %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="flex flex-col flex-1 mt-16 ml-0 lg:mt-0 lg:ml-72" id="panda-inner-container" <% if content_for :sidebar %> data-controller="toggle" data-action="keydown.esc@window->toggle#hide keydown.escape@window->toggle#hide" tabindex="-1"<% end %>>
|
|
20
|
+
<section id="panda-main" class="flex flex-row h-full">
|
|
21
|
+
<div class="flex-1 h-full overflow-y-auto pb-8" id="panda-primary-content">
|
|
22
|
+
<%= render Panda::Core::Admin::BreadcrumbsComponent.new(breadcrumbs: @breadcrumbs) %>
|
|
23
|
+
<%= render "panda/core/admin/shared/flash" %>
|
|
24
|
+
<%= content %>
|
|
25
|
+
</div>
|
|
26
|
+
<% if content_for :sidebar %>
|
|
27
|
+
<!-- Backdrop overlay -->
|
|
28
|
+
<div data-toggle-target="toggleable" class="hidden fixed inset-0 bg-gray-500/75 transition-opacity duration-500 ease-in-out dark:bg-gray-900/50 z-40"
|
|
29
|
+
data-transition-enter="transition-opacity duration-500 ease-in-out"
|
|
30
|
+
data-transition-enter-from="opacity-0"
|
|
31
|
+
data-transition-enter-to="opacity-100"
|
|
32
|
+
data-transition-leave="transition-opacity duration-500 ease-in-out"
|
|
33
|
+
data-transition-leave-from="opacity-100"
|
|
34
|
+
data-transition-leave-to="opacity-0"></div>
|
|
35
|
+
|
|
36
|
+
<!-- Slideover panel -->
|
|
37
|
+
<div data-toggle-target="toggleable" class="hidden ml-auto block size-full max-w-md transform fixed right-0 top-0 h-full z-50"
|
|
38
|
+
data-transition-enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
39
|
+
data-transition-enter-from="translate-x-full"
|
|
40
|
+
data-transition-enter-to="translate-x-0"
|
|
41
|
+
data-transition-leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
42
|
+
data-transition-leave-from="translate-x-0"
|
|
43
|
+
data-transition-leave-to="translate-x-full"
|
|
44
|
+
id="slideover">
|
|
45
|
+
<div class="relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800 dark:after:absolute dark:after:inset-y-0 dark:after:left-0 dark:after:w-px dark:after:bg-white/10">
|
|
46
|
+
<!-- Header -->
|
|
47
|
+
<div class="bg-gradient-admin px-4 py-3 sm:px-6">
|
|
48
|
+
<div class="flex items-center justify-between">
|
|
49
|
+
<h2 class="text-base font-semibold text-white" id="slideover-title"><%= content_for?(:sidebar_title) ? yield(:sidebar_title) : "Sidebar" %></h2>
|
|
50
|
+
<div class="ml-3 flex items-center">
|
|
51
|
+
<button type="button" data-action="click->toggle#toggle touch->toggle#toggle" class="flex items-center gap-x-1 rounded-full px-2 py-1 text-xs text-white/80 bg-white/10 hover:text-white hover:bg-white/20 transition-colors">
|
|
52
|
+
<span>Close</span>
|
|
53
|
+
<i class="fa-solid fa-xmark"></i>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Content -->
|
|
60
|
+
<% if content_for?(:sidebar) %>
|
|
61
|
+
<div class="flex-1 overflow-y-auto">
|
|
62
|
+
<%= yield :sidebar %>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
65
|
+
|
|
66
|
+
<!-- Footer (if present) -->
|
|
67
|
+
<% if content_for?(:sidebar_footer) %>
|
|
68
|
+
<div class="flex shrink-0 justify-end gap-x-3 border-t border-gray-200 px-4 py-4 dark:border-white/10">
|
|
69
|
+
<%= yield :sidebar_footer %>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<script>
|
|
75
|
+
window.addEventListener('keydown', function(e) {
|
|
76
|
+
if (e.key === 'Escape') {
|
|
77
|
+
document.querySelectorAll('[data-toggle-target="toggleable"]').forEach(function(el) {
|
|
78
|
+
el.classList.add('hidden');
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
83
|
+
<% end %>
|
|
84
|
+
</section>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<%= render Panda::Core::Shared::FooterComponent.new %>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Main admin layout component
|
|
7
|
+
# Handles the full admin page layout with sidebar, header, and main content
|
|
8
|
+
class MainLayoutComponent < ViewComponent::Base
|
|
9
|
+
def initialize(user:, breadcrumbs: [], **attrs)
|
|
10
|
+
super()
|
|
11
|
+
@user = user
|
|
12
|
+
@breadcrumbs = breadcrumbs
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :user, :breadcrumbs
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -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>
|