panda-core 0.12.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +53 -0
- data/app/assets/builds/panda_core_admin.css +2 -0
- data/app/assets/tailwind/application.css +236 -36
- data/app/builders/panda/core/form_builder.rb +120 -18
- data/app/components/panda/core/UI/badge.html.erb +10 -0
- data/app/components/panda/core/UI/badge.rb +14 -30
- data/app/components/panda/core/UI/button.html.erb +5 -0
- data/app/components/panda/core/UI/button.rb +11 -15
- data/app/components/panda/core/UI/card.html.erb +3 -0
- data/app/components/panda/core/UI/card.rb +7 -6
- data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
- data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
- data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
- data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
- data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
- data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
- data/app/components/panda/core/admin/button_component.html.erb +9 -0
- data/app/components/panda/core/admin/button_component.rb +36 -31
- data/app/components/panda/core/admin/callout_component.html.erb +17 -0
- data/app/components/panda/core/admin/callout_component.rb +57 -0
- data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
- data/app/components/panda/core/admin/code_block_component.rb +23 -0
- data/app/components/panda/core/admin/container_component.html.erb +27 -0
- data/app/components/panda/core/admin/container_component.rb +11 -77
- data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
- data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
- data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
- data/app/components/panda/core/admin/delete_button_component.rb +41 -0
- data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
- data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
- data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
- data/app/components/panda/core/admin/dropdown_component.rb +65 -0
- data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
- data/app/components/panda/core/admin/empty_state_component.rb +25 -0
- data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
- data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
- data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
- data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
- data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
- data/app/components/panda/core/admin/flash_message_component.rb +33 -60
- data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
- data/app/components/panda/core/admin/form_error_component.rb +8 -16
- data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_footer_component.rb +65 -0
- data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
- data/app/components/panda/core/admin/form_input_component.rb +21 -13
- data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_section_component.rb +64 -0
- data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
- data/app/components/panda/core/admin/form_select_component.rb +25 -26
- data/app/components/panda/core/admin/heading_component.html.erb +26 -0
- data/app/components/panda/core/admin/heading_component.rb +23 -60
- data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
- data/app/components/panda/core/admin/login_form_component.rb +28 -0
- data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
- data/app/components/panda/core/admin/main_layout_component.rb +19 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
- data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
- data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
- data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
- data/app/components/panda/core/admin/page_header_component.rb +19 -35
- data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
- data/app/components/panda/core/admin/pagination_component.rb +56 -0
- data/app/components/panda/core/admin/panel_component.html.erb +19 -0
- data/app/components/panda/core/admin/panel_component.rb +4 -29
- data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
- data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
- data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
- data/app/components/panda/core/admin/search_bar_component.rb +31 -0
- data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
- data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
- data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
- data/app/components/panda/core/admin/secret_field_component.rb +31 -0
- data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
- data/app/components/panda/core/admin/sidebar_component.rb +55 -0
- data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
- data/app/components/panda/core/admin/slideover_component.rb +31 -70
- data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/core/admin/statistics_component.rb +6 -8
- data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
- data/app/components/panda/core/admin/tab_bar_component.rb +32 -67
- data/app/components/panda/core/admin/table_component.html.erb +82 -0
- data/app/components/panda/core/admin/table_component.rb +54 -87
- data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
- data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
- data/app/components/panda/core/admin/tag_component.html.erb +1 -0
- data/app/components/panda/core/admin/tag_component.rb +51 -20
- data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
- data/app/components/panda/core/admin/tag_input_component.rb +22 -0
- data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
- data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
- data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
- data/app/components/panda/core/admin/user_activity_component.rb +9 -22
- data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
- data/app/components/panda/core/admin/user_display_component.rb +19 -22
- data/app/components/panda/core/base.rb +35 -42
- data/app/components/panda/core/shared/footer_component.html.erb +2 -0
- data/app/components/panda/core/shared/footer_component.rb +11 -0
- data/app/components/panda/core/shared/header_component.html.erb +32 -0
- data/app/components/panda/core/shared/header_component.rb +19 -0
- data/app/constraints/panda/core/admin_constraint.rb +2 -2
- data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
- data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
- data/app/controllers/panda/core/admin/base_controller.rb +62 -3
- data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
- data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
- data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
- data/app/controllers/panda/core/admin/files_controller.rb +230 -0
- data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
- data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
- data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
- data/app/controllers/panda/core/admin/search_controller.rb +15 -0
- data/app/controllers/panda/core/admin/sessions_controller.rb +84 -10
- data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
- data/app/controllers/panda/core/admin/users_controller.rb +184 -0
- data/app/controllers/panda/core/application_controller.rb +13 -1
- data/app/helpers/panda/core/active_storage_helper.rb +24 -0
- data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
- data/app/helpers/panda/core/form_helper.rb +15 -0
- data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
- data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
- data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
- data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
- data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
- data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
- data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
- data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
- data/app/javascript/panda/core/controllers/index.js +36 -0
- data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
- data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
- data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
- data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
- data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
- data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
- data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
- data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
- data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
- data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
- data/app/models/concerns/panda/core/has_metadata.rb +164 -0
- data/app/models/concerns/panda/core/importable.rb +79 -0
- data/app/models/concerns/panda/core/searchable.rb +66 -0
- data/app/models/concerns/panda/core/taggable.rb +37 -0
- data/app/models/panda/core/feature_flag.rb +56 -0
- data/app/models/panda/core/file_categorization.rb +14 -0
- data/app/models/panda/core/file_category.rb +45 -0
- data/app/models/panda/core/import_session.rb +79 -0
- data/app/models/panda/core/presence.rb +24 -0
- data/app/models/panda/core/tag.rb +33 -0
- data/app/models/panda/core/tagging.rb +14 -0
- data/app/models/panda/core/user.rb +94 -16
- data/app/models/panda/core/user_activity.rb +42 -0
- data/app/models/panda/core/user_session.rb +35 -0
- data/app/services/panda/core/attach_avatar_service.rb +15 -3
- data/app/services/panda/core/csv_import_service.rb +65 -0
- data/app/services/panda/core/file_categorizer.rb +52 -0
- data/app/services/panda/core/file_parser.rb +76 -0
- data/app/services/panda/core/invite_user_service.rb +44 -0
- data/app/services/panda/core/presence_service.rb +49 -0
- data/app/views/layouts/panda/core/admin.html.erb +6 -76
- data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
- data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
- data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
- data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
- data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
- data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
- data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
- data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
- data/app/views/panda/core/admin/files/index.html.erb +61 -0
- data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
- data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
- data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
- data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
- data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
- data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
- data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
- data/app/views/panda/core/admin/sessions/new.html.erb +1 -37
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
- data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
- data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
- data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
- data/app/views/panda/core/admin/tags/index.html.erb +41 -0
- data/app/views/panda/core/admin/tags/new.html.erb +4 -0
- data/app/views/panda/core/admin/users/activity.html.erb +90 -0
- data/app/views/panda/core/admin/users/edit.html.erb +48 -0
- data/app/views/panda/core/admin/users/index.html.erb +161 -0
- data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
- data/app/views/panda/core/admin/users/show.html.erb +224 -0
- data/app/views/panda/core/shared/_footer.html.erb +1 -2
- data/app/views/panda/core/shared/_header.html.erb +4 -22
- data/config/brakeman.ignore +68 -31
- data/config/importmap.rb +13 -11
- data/config/routes.rb +49 -2
- data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
- data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
- data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
- data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
- data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
- data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
- data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
- data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
- data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
- data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
- data/lib/generators/panda/core/install_generator.rb +32 -0
- data/lib/generators/panda/core/templates/panda.rb +63 -0
- data/lib/panda/core/asset_loader.rb +5 -2
- data/lib/panda/core/authentication.rb +3 -3
- data/lib/panda/core/configuration.rb +195 -1
- data/lib/panda/core/engine/admin_controller_config.rb +3 -6
- data/lib/panda/core/engine/autoload_config.rb +21 -9
- data/lib/panda/core/engine/omniauth_config.rb +108 -38
- data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
- data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
- data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
- data/lib/panda/core/engine.rb +90 -25
- data/lib/panda/core/middleware.rb +35 -0
- data/lib/panda/core/navigation_registry.rb +455 -0
- data/lib/panda/core/oauth_providers.rb +27 -0
- data/lib/panda/core/permission_registry.rb +89 -0
- data/lib/panda/core/search_registry.rb +69 -0
- data/lib/panda/core/seeds/file_categories.rb +30 -0
- data/lib/panda/core/shared/inflections_config.rb +1 -5
- data/lib/panda/core/testing/rails_helper.rb +87 -0
- data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
- data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
- data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core/widget_registry.rb +50 -0
- data/lib/panda/core.rb +17 -8
- data/lib/tasks/panda/core/file_categories.rake +41 -0
- data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
- data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- data/public/panda-core-assets/vanilla-calendar.css +47 -0
- metadata +203 -26
- data/lib/panda/core/engine/inflections_config.rb +0 -23
|
@@ -0,0 +1,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-primary-50/20 to-primary-50 border-primary-400") do
|
|
12
|
-
dt(class: "text-base font-medium truncate text-primary-900") { @metric }
|
|
13
|
-
dd(class: "mt-1 text-3xl font-medium tracking-tight text-primary-900") { @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>
|
|
@@ -4,95 +4,60 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class TabBarComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
div(class: "mt-3 sm:mt-2") do
|
|
11
|
-
render_mobile_select
|
|
12
|
-
render_desktop_tabs
|
|
13
|
-
end
|
|
7
|
+
def initialize(tabs: [].freeze, **attrs)
|
|
8
|
+
@tabs = tabs
|
|
9
|
+
super(**attrs)
|
|
14
10
|
end
|
|
15
11
|
|
|
12
|
+
attr_reader :tabs
|
|
13
|
+
|
|
16
14
|
private
|
|
17
15
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
label(for: "tabs", class: "sr-only") { "Select a tab" }
|
|
21
|
-
select(
|
|
22
|
-
id: "tabs",
|
|
23
|
-
name: "tabs",
|
|
24
|
-
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"
|
|
25
|
-
) do
|
|
26
|
-
@tabs.each do |tab|
|
|
27
|
-
option { tab[:name] }
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
16
|
+
def any_tab_current?
|
|
17
|
+
@any_tab_current ||= @tabs.any? { |tab| tab[:current] }
|
|
31
18
|
end
|
|
32
19
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
div(class: "flex items-center border-b border-gray-200") do
|
|
36
|
-
nav(class: "flex flex-1 -mb-px space-x-6 xl:space-x-8", aria: {label: "Tabs"}) do
|
|
37
|
-
@tabs.each_with_index do |tab, index|
|
|
38
|
-
render_tab(tab, index.zero?)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
render_view_toggle
|
|
42
|
-
end
|
|
43
|
-
end
|
|
20
|
+
def tab_current?(tab, index)
|
|
21
|
+
tab[:current] || (!any_tab_current? && index.zero?)
|
|
44
22
|
end
|
|
45
23
|
|
|
46
|
-
def
|
|
24
|
+
def tab_classes(tab, index)
|
|
47
25
|
classes = "py-4 px-1 text-sm font-medium whitespace-nowrap border-b-2 "
|
|
48
|
-
classes += if
|
|
26
|
+
classes += if tab_current?(tab, index)
|
|
49
27
|
"border-primary-600 text-primary-600"
|
|
50
28
|
else
|
|
51
29
|
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|
52
30
|
end
|
|
53
|
-
|
|
54
|
-
a(
|
|
55
|
-
href: tab[:url] || "#",
|
|
56
|
-
class: classes,
|
|
57
|
-
aria: {current: (is_current || tab[:current]) ? "page" : nil}
|
|
58
|
-
) { tab[:name] }
|
|
31
|
+
classes
|
|
59
32
|
end
|
|
60
33
|
|
|
61
|
-
def
|
|
62
|
-
div(class: "hidden items-center p-0.5 ml-6 bg-gray-100 rounded-lg sm:flex") do
|
|
63
|
-
render_view_button(:list)
|
|
64
|
-
render_view_button(:grid, selected: true)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def render_view_button(type, selected: false)
|
|
34
|
+
def view_button_classes(selected)
|
|
69
35
|
button_class = "p-1.5 text-gray-400 rounded-md focus:ring-2 focus:ring-inset focus:outline-none focus:ring-primary-600"
|
|
70
36
|
button_class += if selected
|
|
71
37
|
" ml-0.5 bg-white shadow-sm"
|
|
72
38
|
else
|
|
73
39
|
" hover:bg-white hover:shadow-sm"
|
|
74
40
|
end
|
|
41
|
+
button_class
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def list_view_icon
|
|
45
|
+
content_tag(:svg, class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do
|
|
46
|
+
tag.path(
|
|
47
|
+
fill_rule: "evenodd",
|
|
48
|
+
d: "M2 3.75A.75.75 0 012.75 3h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.166a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z",
|
|
49
|
+
clip_rule: "evenodd"
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
75
53
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
span(class: "sr-only") { "Use list view" }
|
|
86
|
-
else
|
|
87
|
-
svg(class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do |s|
|
|
88
|
-
s.path(
|
|
89
|
-
fill_rule: "evenodd",
|
|
90
|
-
d: "M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z",
|
|
91
|
-
clip_rule: "evenodd"
|
|
92
|
-
)
|
|
93
|
-
end
|
|
94
|
-
span(class: "sr-only") { "Use grid view" }
|
|
95
|
-
end
|
|
54
|
+
def grid_view_icon
|
|
55
|
+
content_tag(:svg, class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do
|
|
56
|
+
tag.path(
|
|
57
|
+
fill_rule: "evenodd",
|
|
58
|
+
d: "M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z",
|
|
59
|
+
clip_rule: "evenodd"
|
|
60
|
+
)
|
|
96
61
|
end
|
|
97
62
|
end
|
|
98
63
|
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<%# Chrome (bg/border/radius, header/row borders, cell text colour) is
|
|
2
|
+
themeable via the --panda-panel-* / --panda-table-* tokens — see
|
|
3
|
+
app/assets/tailwind/application.css. Fallbacks resolve to today's
|
|
4
|
+
literal utilities exactly, so this is a no-op unless a host app sets
|
|
5
|
+
data-theme. %>
|
|
6
|
+
<% if rows.any? %>
|
|
7
|
+
<% if responsive? %>
|
|
8
|
+
<%# Mobile card view - hidden on lg screens %>
|
|
9
|
+
<div class="mb-12 space-y-3 lg:hidden">
|
|
10
|
+
<% rows.each do |row| %>
|
|
11
|
+
<div class="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))] p-4 hover:bg-gray-50 transition-colors" data-controller="row-link" data-action="click->row-link#click">
|
|
12
|
+
<% columns.each_with_index do |column, i| %>
|
|
13
|
+
<% cell_content = render_cell_content(row, column.cell) %>
|
|
14
|
+
<% if i == 0 %>
|
|
15
|
+
<div class="font-medium text-gray-900 text-sm mb-2"><%= cell_content %></div>
|
|
16
|
+
<% else %>
|
|
17
|
+
<% if cell_content.present? && cell_content.to_s.strip.present? %>
|
|
18
|
+
<div class="flex justify-between items-baseline py-1">
|
|
19
|
+
<span class="text-xs text-gray-400"><%= column.label %></span>
|
|
20
|
+
<span class="text-sm text-gray-600"><%= cell_content %></span>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<%# Desktop table view %>
|
|
31
|
+
<div class="mb-12 w-full overflow-x-auto 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))] <%= 'hidden lg:block' if responsive? %>" data-controller="row-link" data-action="click->row-link#click">
|
|
32
|
+
<%# Header %>
|
|
33
|
+
<div class="table w-full" style="table-layout: fixed;">
|
|
34
|
+
<div class="table-header-group">
|
|
35
|
+
<div class="table-row text-sm font-medium text-gray-500 truncate bg-[var(--panda-table-header-bg,var(--color-gray-50))] border-b border-[var(--panda-table-border,var(--color-gray-200))]">
|
|
36
|
+
<% columns.each do |column| %>
|
|
37
|
+
<% header_style = column.width ? "width: #{column.width};" : nil %>
|
|
38
|
+
<% aria_sort = if column.sortable? && sort == column.sort_key
|
|
39
|
+
sort_direction == "asc" ? "ascending" : "descending"
|
|
40
|
+
elsif column.sortable?
|
|
41
|
+
"none"
|
|
42
|
+
end %>
|
|
43
|
+
<div class="table-cell sticky top-0 z-10 px-4 py-3 align-middle" style="<%= header_style %>"<% if aria_sort %> aria-sort="<%= aria_sort %>"<% end %>>
|
|
44
|
+
<% if column.sortable? %>
|
|
45
|
+
<a href="<%= sort_url_for(column) %>" class="inline-flex items-center gap-1 hover:text-gray-900 transition-colors group" aria-label="Sort by <%= column.label %>">
|
|
46
|
+
<%= column.label %>
|
|
47
|
+
<% if (indicator = sort_indicator_for(column)) %>
|
|
48
|
+
<span class="text-gray-700 text-xs" aria-hidden="true"><%= indicator %></span>
|
|
49
|
+
<% else %>
|
|
50
|
+
<span class="text-gray-300 text-xs opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden="true">↕</span>
|
|
51
|
+
<% end %>
|
|
52
|
+
</a>
|
|
53
|
+
<% else %>
|
|
54
|
+
<%= column.label %>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
<% end %>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<%# Rows %>
|
|
62
|
+
<div class="table-row-group">
|
|
63
|
+
<% rows.each do |row| %>
|
|
64
|
+
<div class="table-row odd:bg-white even:bg-gray-50/60 hover:bg-gray-100 cursor-pointer" data-<%= term %>-id="<%= row.id %>">
|
|
65
|
+
<% columns.each do |column| %>
|
|
66
|
+
<div class="table-cell px-4 py-3 text-sm align-middle break-words border-b border-[var(--panda-table-row-border,color-mix(in_srgb,var(--color-gray-200)_70%,transparent))] text-[var(--panda-table-cell-color,var(--color-gray-600))]">
|
|
67
|
+
<%= render_cell_content(row, column.cell) %>
|
|
68
|
+
</div>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<% else %>
|
|
76
|
+
<%# Empty state %>
|
|
77
|
+
<%= render Panda::Core::Admin::EmptyStateComponent.new(
|
|
78
|
+
title: "No #{term.pluralize}",
|
|
79
|
+
description: "Get started by creating a new #{term}.",
|
|
80
|
+
icon: icon
|
|
81
|
+
) %>
|
|
82
|
+
<% end %>
|