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,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>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Sidebar navigation component for admin pages
|
|
7
|
+
# Displays hierarchical navigation with toggle for nested items.
|
|
8
|
+
# Supports optional section grouping via the :section key on nav items.
|
|
9
|
+
class SidebarComponent < Panda::Core::Base
|
|
10
|
+
def initialize(user:, **attrs)
|
|
11
|
+
@user = user
|
|
12
|
+
super(**attrs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :user
|
|
16
|
+
|
|
17
|
+
def admin_logo
|
|
18
|
+
Panda::Core.config.admin_logo
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def admin_settings_path
|
|
22
|
+
Panda::Core.config.admin_settings_path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def compact_navigation?
|
|
26
|
+
Panda::Core.config.compact_navigation
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def admin_logo_actions
|
|
30
|
+
Panda::Core.config.admin_logo_actions || []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sectioned?(items)
|
|
34
|
+
items.any? { |item| item[:section].present? }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def safe_html(value)
|
|
38
|
+
raw = value.respond_to?(:call) ? value.call : value
|
|
39
|
+
helpers.sanitize(raw, tags: %w[svg path ellipse circle rect line polyline polygon g defs use
|
|
40
|
+
span a i div img br strong em b], attributes: %w[
|
|
41
|
+
viewBox fill stroke stroke-width stroke-linecap stroke-linejoin opacity cx cy rx ry r
|
|
42
|
+
x y x1 y1 x2 y2 width height d points transform xmlns class href style id
|
|
43
|
+
data-turbo-track rel
|
|
44
|
+
])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def default_attrs
|
|
50
|
+
{class: "flex flex-col flex-1"}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<%# Backdrop overlay - clicks close the slideover %>
|
|
2
|
+
<div class="hidden fixed inset-0 bg-gray-500/75 transition-opacity duration-500 ease-in-out dark:bg-gray-900/50 z-40"
|
|
3
|
+
data-toggle-target="toggleable"
|
|
4
|
+
data-action="click->toggle#toggle"
|
|
5
|
+
data-transition-enter="transition-opacity duration-500 ease-in-out"
|
|
6
|
+
data-transition-enter-from="opacity-0"
|
|
7
|
+
data-transition-enter-to="opacity-100"
|
|
8
|
+
data-transition-leave="transition-opacity duration-500 ease-in-out"
|
|
9
|
+
data-transition-leave-from="opacity-100"
|
|
10
|
+
data-transition-leave-to="opacity-0"></div>
|
|
11
|
+
|
|
12
|
+
<%# Slideover panel %>
|
|
13
|
+
<div id="slideover"
|
|
14
|
+
class="<%= slideover_classes %>"
|
|
15
|
+
data-toggle-target="toggleable"
|
|
16
|
+
data-transition-enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
17
|
+
data-transition-enter-from="translate-x-full"
|
|
18
|
+
data-transition-enter-to="translate-x-0"
|
|
19
|
+
data-transition-leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
20
|
+
data-transition-leave-from="translate-x-0"
|
|
21
|
+
data-transition-leave-to="translate-x-full">
|
|
22
|
+
|
|
23
|
+
<%# Main container %>
|
|
24
|
+
<div class="relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800">
|
|
25
|
+
<%# Header with title and close button %>
|
|
26
|
+
<div class="bg-gradient-admin px-4 py-6 sm:px-6">
|
|
27
|
+
<div class="flex items-center justify-between">
|
|
28
|
+
<h2 class="text-base font-semibold text-white" id="slideover-title">
|
|
29
|
+
<%= @title %>
|
|
30
|
+
</h2>
|
|
31
|
+
<div class="ml-3 flex items-center">
|
|
32
|
+
<%= content_tag :button,
|
|
33
|
+
type: "button",
|
|
34
|
+
data: {
|
|
35
|
+
action: "click->toggle#toggle touch->toggle#toggle"
|
|
36
|
+
},
|
|
37
|
+
class: "inline-flex items-center gap-1 rounded-full bg-white/10 px-2 py-1 text-xs text-white/80 hover:text-white cursor-pointer" do %>
|
|
38
|
+
<span>Close</span>
|
|
39
|
+
<i class="fa-solid fa-xmark"></i>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<%# Scrollable content area %>
|
|
46
|
+
<div class="flex-1 overflow-y-auto">
|
|
47
|
+
<% if @content_html %>
|
|
48
|
+
<%= raw(@content_html) %>
|
|
49
|
+
<% elsif @content_block %>
|
|
50
|
+
<%= @content_block.call %>
|
|
51
|
+
<% end %>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<%# Sticky footer (if footer content exists) %>
|
|
55
|
+
<% if footer_slot? || @footer_html || @footer_block %>
|
|
56
|
+
<div class="flex shrink-0 justify-end gap-x-3 border-t border-gray-200 px-4 py-4 dark:border-white/10">
|
|
57
|
+
<% if footer_slot? %>
|
|
58
|
+
<%= footer_slot %>
|
|
59
|
+
<% elsif @footer_html %>
|
|
60
|
+
<%= raw(@footer_html) %>
|
|
61
|
+
<% elsif @footer_block %>
|
|
62
|
+
<%= @footer_block.call %>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
@@ -4,79 +4,25 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class SlideoverComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
prop :open, _Nilable(_Boolean), default: -> { false }
|
|
7
|
+
renders_one :footer_slot
|
|
9
8
|
|
|
10
|
-
def
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
else
|
|
16
|
-
@content_block = block
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
div(
|
|
21
|
-
**default_attrs,
|
|
22
|
-
data: {
|
|
23
|
-
toggle_target: "toggleable",
|
|
24
|
-
transition_enter: "transform transition ease-in-out duration-500 sm:duration-700",
|
|
25
|
-
transition_enter_from: "translate-x-full",
|
|
26
|
-
transition_enter_to: "translate-x-0",
|
|
27
|
-
transition_leave: "transform transition ease-in-out duration-500 sm:duration-700",
|
|
28
|
-
transition_leave_from: "translate-x-0",
|
|
29
|
-
transition_leave_to: "translate-x-full"
|
|
30
|
-
}
|
|
31
|
-
) do
|
|
32
|
-
# Main container
|
|
33
|
-
div(class: "relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800") do
|
|
34
|
-
# Header with title and close button
|
|
35
|
-
div(class: "bg-gradient-admin px-4 py-6 sm:px-6") do
|
|
36
|
-
div(class: "flex items-center justify-between") do
|
|
37
|
-
h2(class: "text-base font-semibold text-white", id: "slideover-title") do
|
|
38
|
-
plain @title
|
|
39
|
-
end
|
|
40
|
-
div(class: "ml-3 flex h-7 items-center") do
|
|
41
|
-
button(
|
|
42
|
-
type: "button",
|
|
43
|
-
data: {action: "click->toggle#toggle touch->toggle#toggle"},
|
|
44
|
-
class: "relative rounded-md text-white/80 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
|
|
45
|
-
) do
|
|
46
|
-
span(class: "absolute -inset-2.5")
|
|
47
|
-
span(class: "sr-only") { "Close panel" }
|
|
48
|
-
# SVG close icon
|
|
49
|
-
svg(viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "1.5", aria_hidden: "true", class: "size-6") do
|
|
50
|
-
path(d: "M6 18 18 6M6 6l12 12", stroke_linecap: "round", stroke_linejoin: "round")
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
9
|
+
def initialize(title: "Settings", open: false, **attrs)
|
|
10
|
+
@title = title
|
|
11
|
+
@open = open
|
|
12
|
+
super(**attrs)
|
|
13
|
+
end
|
|
56
14
|
|
|
57
|
-
|
|
58
|
-
div(class: "flex-1 overflow-y-auto") do
|
|
59
|
-
if @content_html
|
|
60
|
-
raw(@content_html)
|
|
61
|
-
elsif @content_block
|
|
62
|
-
instance_eval(@content_block)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
15
|
+
attr_reader :title, :open
|
|
65
16
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
elsif @footer_block
|
|
72
|
-
instance_eval(&@footer_block)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
17
|
+
def before_render
|
|
18
|
+
# Store main content if provided
|
|
19
|
+
# In ViewComponent, content is already rendered HTML (SafeBuffer), not a block
|
|
20
|
+
if content.present?
|
|
21
|
+
@content_html = content
|
|
77
22
|
end
|
|
78
23
|
end
|
|
79
24
|
|
|
25
|
+
# Legacy method for backwards compatibility
|
|
80
26
|
def footer(&block)
|
|
81
27
|
if defined?(view_context) && view_context
|
|
82
28
|
@footer_html = view_context.capture(&block)
|
|
@@ -85,8 +31,6 @@ module Panda
|
|
|
85
31
|
end
|
|
86
32
|
end
|
|
87
33
|
|
|
88
|
-
alias_method :with_footer, :footer
|
|
89
|
-
|
|
90
34
|
private
|
|
91
35
|
|
|
92
36
|
def default_attrs
|
|
@@ -97,10 +41,27 @@ module Panda
|
|
|
97
41
|
end
|
|
98
42
|
|
|
99
43
|
def slideover_classes
|
|
100
|
-
|
|
44
|
+
# Use fixed positioning to escape parent overflow clipping
|
|
45
|
+
base = "ml-auto block size-full max-w-md transform fixed right-0 top-0 h-full z-50"
|
|
101
46
|
visibility = @open ? "" : "hidden"
|
|
102
47
|
[base, visibility].compact.join(" ")
|
|
103
48
|
end
|
|
49
|
+
|
|
50
|
+
def close_icon
|
|
51
|
+
content_tag(:svg,
|
|
52
|
+
viewBox: "0 0 24 24",
|
|
53
|
+
fill: "none",
|
|
54
|
+
stroke: "currentColor",
|
|
55
|
+
stroke_width: "1.5",
|
|
56
|
+
aria: {hidden: "true"},
|
|
57
|
+
class: "size-6") do
|
|
58
|
+
tag.path(
|
|
59
|
+
d: "M6 18 18 6M6 6l12 12",
|
|
60
|
+
stroke_linecap: "round",
|
|
61
|
+
stroke_linejoin: "round"
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
104
65
|
end
|
|
105
66
|
end
|
|
106
67
|
end
|
|
@@ -4,15 +4,13 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class StatisticsComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
div(class: "overflow-hidden p-4 bg-gradient-to-br rounded-lg border-2 from-light/20 to-light border-mid") do
|
|
12
|
-
dt(class: "text-base font-medium truncate text-dark") { @metric }
|
|
13
|
-
dd(class: "mt-1 text-3xl font-medium tracking-tight text-dark") { @value }
|
|
14
|
-
end
|
|
7
|
+
def initialize(metric: "", value: nil, **attrs)
|
|
8
|
+
@metric = metric
|
|
9
|
+
@value = value
|
|
10
|
+
super(**attrs)
|
|
15
11
|
end
|
|
12
|
+
|
|
13
|
+
attr_reader :metric, :value
|
|
16
14
|
end
|
|
17
15
|
end
|
|
18
16
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div class="mt-3">
|
|
2
|
+
<%# Mobile select dropdown %>
|
|
3
|
+
<div class="sm:hidden">
|
|
4
|
+
<%= tag.label "Select a tab", for: "tabs", class: "sr-only" %>
|
|
5
|
+
<%= tag.select id: "tabs",
|
|
6
|
+
name: "tabs",
|
|
7
|
+
onchange: "window.location.href=this.value",
|
|
8
|
+
class: "block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-primary-400 focus:border-primary-600 focus:ring-primary-600" do %>
|
|
9
|
+
<% @tabs.each_with_index do |tab, index| %>
|
|
10
|
+
<%= tag.option tab[:name], value: tab[:url] || "#", selected: tab_current?(tab, index) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<%# Desktop tab navigation %>
|
|
16
|
+
<div class="hidden sm:block">
|
|
17
|
+
<div class="flex items-center border-b border-gray-200">
|
|
18
|
+
<nav class="flex flex-1 -mb-px space-x-6 xl:space-x-8" aria-label="Tabs">
|
|
19
|
+
<% @tabs.each_with_index do |tab, index| %>
|
|
20
|
+
<%= content_tag :a,
|
|
21
|
+
tab[:name],
|
|
22
|
+
href: tab[:url] || "#",
|
|
23
|
+
class: tab_classes(tab, index),
|
|
24
|
+
aria: {current: tab_current?(tab, index) ? "page" : nil} %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</nav>
|
|
27
|
+
|
|
28
|
+
<%# View toggle buttons %>
|
|
29
|
+
<div class="hidden items-center p-0.5 ml-6 bg-gray-100 rounded-lg sm:flex">
|
|
30
|
+
<%# List view button %>
|
|
31
|
+
<%= content_tag :button,
|
|
32
|
+
type: "button",
|
|
33
|
+
class: view_button_classes(false) do %>
|
|
34
|
+
<%= list_view_icon %>
|
|
35
|
+
<span class="sr-only">Use list view</span>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<%# Grid view button %>
|
|
39
|
+
<%= content_tag :button,
|
|
40
|
+
type: "button",
|
|
41
|
+
class: view_button_classes(true) do %>
|
|
42
|
+
<%= grid_view_icon %>
|
|
43
|
+
<span class="sr-only">Use grid view</span>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|