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,90 +4,24 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class ContainerComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Called from Phlex - execute block directly to set instance variables
|
|
17
|
-
yield(self)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Set content_for :sidebar if slideover is present (enables breadcrumb toggle button)
|
|
22
|
-
# This must happen before rendering so the layout can use it
|
|
23
|
-
if @slideover_block && @slideover_title && defined?(view_context) && view_context
|
|
24
|
-
view_context.content_for(:sidebar) do
|
|
25
|
-
# The block contains ERB content, capture it for the sidebar
|
|
26
|
-
view_context.capture(&@slideover_block)
|
|
27
|
-
end
|
|
28
|
-
view_context.content_for(:sidebar_title, @slideover_title)
|
|
29
|
-
|
|
30
|
-
# Set footer content if present
|
|
31
|
-
if @footer_block
|
|
32
|
-
view_context.content_for(:sidebar_footer) do
|
|
33
|
-
view_context.capture(&@footer_block)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
main(class: "overflow-auto flex-1 h-full min-h-full max-h-full") do
|
|
39
|
-
div(class: "overflow-auto px-2 pt-2 mx-auto sm:px-6 lg:px-6") do
|
|
40
|
-
@heading_content&.call
|
|
41
|
-
@tab_bar_content&.call
|
|
42
|
-
|
|
43
|
-
section(class: section_classes) do
|
|
44
|
-
div(class: "flex-1 mt-4 w-full h-full") do
|
|
45
|
-
if @main_content
|
|
46
|
-
@main_content.call
|
|
47
|
-
elsif @body_html
|
|
48
|
-
raw(@body_html)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def content(&block)
|
|
57
|
-
@main_content = if defined?(view_context) && view_context
|
|
58
|
-
# Capture ERB content
|
|
59
|
-
-> { raw(view_context.capture(&block)) }
|
|
60
|
-
else
|
|
61
|
-
block
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def heading(**props, &block)
|
|
66
|
-
@heading_content = -> { render(Panda::Core::Admin::HeadingComponent.new(**props), &block) }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def tab_bar(**props, &block)
|
|
70
|
-
@tab_bar_content = -> { render(Panda::Core::Admin::TabBarComponent.new(**props), &block) } if defined?(Panda::Core::Admin::TabBarComponent)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def slideover(**props, &block)
|
|
74
|
-
@slideover_title = props[:title] || "Settings"
|
|
75
|
-
@slideover_block = block # Save the block for content_for
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def footer(&block)
|
|
79
|
-
@footer_block = block
|
|
7
|
+
renders_one :heading_slot, Panda::Core::Admin::HeadingComponent
|
|
8
|
+
renders_one :tab_bar_slot, Panda::Core::Admin::TabBarComponent
|
|
9
|
+
renders_one :body_slot
|
|
10
|
+
renders_one :slideover_slot, Panda::Core::Admin::SlideoverComponent
|
|
11
|
+
renders_one :footer_slot
|
|
12
|
+
|
|
13
|
+
def initialize(full_height: false, **attrs)
|
|
14
|
+
@full_height = full_height
|
|
15
|
+
super(**attrs)
|
|
80
16
|
end
|
|
81
17
|
|
|
82
|
-
|
|
83
|
-
alias_method :with_slideover, :slideover
|
|
84
|
-
alias_method :with_footer, :footer
|
|
18
|
+
attr_reader :full_height
|
|
85
19
|
|
|
86
20
|
private
|
|
87
21
|
|
|
88
22
|
def section_classes
|
|
89
23
|
base = "flex-auto"
|
|
90
|
-
height =
|
|
24
|
+
height = full_height ? "h-[calc(100vh-9rem)]" : nil
|
|
91
25
|
[base, height].compact.join(" ")
|
|
92
26
|
end
|
|
93
27
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
|
|
2
|
+
<% panel.with_heading_slot { heading } %>
|
|
3
|
+
<% panel.with_body_slot do %>
|
|
4
|
+
<div class="flex flex-wrap gap-3">
|
|
5
|
+
<% contributors.each do |contributor| %>
|
|
6
|
+
<%= render Panda::Core::Admin::UserDisplayComponent.new(user: contributor) %>
|
|
7
|
+
<% end %>
|
|
8
|
+
</div>
|
|
9
|
+
<p class="mt-4 text-sm text-gray-500">
|
|
10
|
+
<%= pluralize(contributors.size, "contributor") %> •
|
|
11
|
+
<%= pluralize(total_count, count_label) %> •
|
|
12
|
+
Last updated <%= time_ago_in_words(last_updated_at) %> ago
|
|
13
|
+
</p>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class ContributorsSummaryComponent < Panda::Core::Base
|
|
7
|
+
def initialize(contributors:, total_count:, last_updated_at:, count_label: "version", heading: "Contributors", **attrs)
|
|
8
|
+
@contributors = contributors
|
|
9
|
+
@total_count = total_count
|
|
10
|
+
@count_label = count_label
|
|
11
|
+
@last_updated_at = last_updated_at
|
|
12
|
+
@heading = heading
|
|
13
|
+
super(**attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :contributors, :total_count, :count_label, :last_updated_at, :heading
|
|
17
|
+
|
|
18
|
+
include ActionView::Helpers::TextHelper
|
|
19
|
+
include ActionView::Helpers::DateHelper
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<div <% if @attrs.any? %><%= tag.attributes(@attrs) %><% end %>>
|
|
2
|
+
<div class="p-6 bg-white shadow rounded-lg">
|
|
3
|
+
<h2 class="text-lg font-medium text-gray-900">Welcome to Panda Admin</h2>
|
|
4
|
+
<p class="mt-1 text-sm text-gray-500">Manage your application from this central dashboard.</p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
8
|
+
<% if defined?(Panda::CMS) %>
|
|
9
|
+
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
10
|
+
<div class="px-4 py-5 sm:p-6">
|
|
11
|
+
<div class="flex items-center">
|
|
12
|
+
<div class="flex-shrink-0">
|
|
13
|
+
<i class="fa-solid fa-file-lines text-3xl text-gray-400"></i>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="ml-5 w-0 flex-1">
|
|
16
|
+
<dt class="text-sm font-medium text-gray-500 truncate">Content Management</dt>
|
|
17
|
+
<dd class="mt-1 text-lg font-semibold text-gray-900">
|
|
18
|
+
<%= helpers.link_to "Manage CMS", helpers.panda_cms.admin_cms_dashboard_path, class: "text-primary-600 hover:text-primary-800" %>
|
|
19
|
+
</dd>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="mt-3">
|
|
23
|
+
<p class="text-sm text-gray-500">Pages, posts, menus, and content blocks</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
30
|
+
<div class="px-4 py-5 sm:p-6">
|
|
31
|
+
<div class="flex items-center">
|
|
32
|
+
<div class="flex-shrink-0">
|
|
33
|
+
<i class="fa-regular fa-user text-3xl text-gray-400"></i>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="ml-5 w-0 flex-1">
|
|
36
|
+
<dt class="text-sm font-medium text-gray-500 truncate">My Profile</dt>
|
|
37
|
+
<dd class="mt-1 text-lg font-semibold text-gray-900">
|
|
38
|
+
<%= helpers.link_to "Edit Profile", helpers.edit_admin_my_profile_path, class: "text-primary-600 hover:text-primary-800" %>
|
|
39
|
+
</dd>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="mt-3">
|
|
43
|
+
<p class="text-sm text-gray-500">Update your personal information</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<%# Hook for additional dashboard cards %>
|
|
49
|
+
<% dashboard_cards.each do |card| %>
|
|
50
|
+
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
51
|
+
<div class="px-4 py-5 sm:p-6">
|
|
52
|
+
<div class="flex items-center">
|
|
53
|
+
<div class="flex-shrink-0">
|
|
54
|
+
<i class="<%= card[:icon] %> text-3xl text-gray-400"></i>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="ml-5 w-0 flex-1">
|
|
57
|
+
<dt class="text-sm font-medium text-gray-500 truncate"><%= card[:title] %></dt>
|
|
58
|
+
<dd class="mt-1 text-lg font-semibold text-gray-900">
|
|
59
|
+
<%= helpers.link_to card[:link_text], card[:path], class: "text-primary-600 hover:text-primary-800" %>
|
|
60
|
+
</dd>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="mt-3">
|
|
64
|
+
<p class="text-sm text-gray-500"><%= card[:description] %></p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Default dashboard content component
|
|
7
|
+
# Displays welcome message and quick action cards
|
|
8
|
+
class DashboardDefaultContentComponent < Panda::Core::Base
|
|
9
|
+
def initialize(user:, **attrs)
|
|
10
|
+
@user = user
|
|
11
|
+
super(**attrs)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :user
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def default_attrs
|
|
19
|
+
{class: "mt-5"}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dashboard_cards
|
|
23
|
+
@dashboard_cards ||= if Panda::Core.config.respond_to?(:admin_dashboard_cards)
|
|
24
|
+
Panda::Core.config.admin_dashboard_cards&.call(@user) || []
|
|
25
|
+
else
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Renders a right-aligned delete button using `button_to` with DELETE method
|
|
7
|
+
# and a Turbo confirmation dialog.
|
|
8
|
+
#
|
|
9
|
+
# Lives in panda-core because every admin app needs destructive action buttons,
|
|
10
|
+
# and the existing ButtonComponent renders `<a>` tags rather than `<form>`-based
|
|
11
|
+
# `button_to` elements required for safe DELETE requests (CSRF + method override).
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# render Panda::Core::Admin::DeleteButtonComponent.new(
|
|
15
|
+
# text: "Delete Person",
|
|
16
|
+
# path: person_path(@person),
|
|
17
|
+
# confirm: "Are you sure you want to delete this person?"
|
|
18
|
+
# )
|
|
19
|
+
class DeleteButtonComponent < Panda::Core::Base
|
|
20
|
+
attr_reader :text, :path, :confirm
|
|
21
|
+
|
|
22
|
+
def initialize(text:, path:, confirm:, **attrs)
|
|
23
|
+
@text = text
|
|
24
|
+
@path = path
|
|
25
|
+
@confirm = confirm
|
|
26
|
+
super(**attrs)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def default_attrs
|
|
32
|
+
{
|
|
33
|
+
class: "inline-flex items-center gap-2 rounded-xl px-3 py-1.5 text-sm font-medium " \
|
|
34
|
+
"text-error-600 border border-error-200 bg-error-50 hover:bg-error-100 " \
|
|
35
|
+
"shadow-sm cursor-pointer transition-colors"
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<div <%= tag.attributes(**attrs) %>
|
|
2
|
+
data-controller="dropdown"
|
|
3
|
+
data-action="click@window->dropdown#hide keydown.escape@window->dropdown#hide">
|
|
4
|
+
<button type="button"
|
|
5
|
+
data-action="click->dropdown#toggle"
|
|
6
|
+
data-dropdown-target="button"
|
|
7
|
+
class="<%= button_classes %>"
|
|
8
|
+
aria-haspopup="true">
|
|
9
|
+
<%= render_text %>
|
|
10
|
+
</button>
|
|
11
|
+
|
|
12
|
+
<div data-dropdown-target="menu"
|
|
13
|
+
class="hidden absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-xl bg-white shadow-sm border border-gray-200 focus:outline-none"
|
|
14
|
+
data-transition-enter="transition ease-out duration-100"
|
|
15
|
+
data-transition-enter-from="transform opacity-0 scale-95"
|
|
16
|
+
data-transition-enter-to="transform opacity-100 scale-100"
|
|
17
|
+
data-transition-leave="transition ease-in duration-75"
|
|
18
|
+
data-transition-leave-from="transform opacity-100 scale-100"
|
|
19
|
+
data-transition-leave-to="transform opacity-0 scale-95"
|
|
20
|
+
role="menu"
|
|
21
|
+
aria-orientation="vertical">
|
|
22
|
+
<div class="py-1" role="none">
|
|
23
|
+
<% items.each do |item| %>
|
|
24
|
+
<%= item %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# A button-styled dropdown menu. Combines ButtonComponent styling with
|
|
7
|
+
# DropdownComponent behaviour (Stimulus `dropdown` controller).
|
|
8
|
+
#
|
|
9
|
+
# @example In a heading slot
|
|
10
|
+
# heading.with_dropdown_button(text: "View Board") do |db|
|
|
11
|
+
# db.with_item(label: "Fundraising", href: "/boards/1")
|
|
12
|
+
# db.with_item(label: "Events", href: "/boards/2")
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
class DropdownButtonComponent < Panda::Core::Base
|
|
16
|
+
renders_many :items, "ItemComponent"
|
|
17
|
+
|
|
18
|
+
def initialize(text: "Options", action: :secondary, icon: nil, size: :regular, **attrs)
|
|
19
|
+
@text = text
|
|
20
|
+
@action = action
|
|
21
|
+
@icon = icon
|
|
22
|
+
@size = size
|
|
23
|
+
super(**attrs)
|
|
24
|
+
yield self if block_given?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :text, :action, :icon, :size
|
|
28
|
+
|
|
29
|
+
class ItemComponent < Panda::Core::Base
|
|
30
|
+
def initialize(label:, href: "#", method: nil, **attrs)
|
|
31
|
+
@label = label
|
|
32
|
+
@href = href
|
|
33
|
+
@method = method
|
|
34
|
+
super(**attrs)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :label, :href, :method
|
|
38
|
+
|
|
39
|
+
def call
|
|
40
|
+
options = {
|
|
41
|
+
class: "block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900",
|
|
42
|
+
role: "menuitem",
|
|
43
|
+
data: {dropdown_target: "menuItem"}
|
|
44
|
+
}
|
|
45
|
+
options[:data][:turbo_method] = method if method
|
|
46
|
+
|
|
47
|
+
content_tag(:a, label, **options.merge(href: href))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def default_attrs
|
|
54
|
+
{class: "relative inline-block text-left"}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def button_classes
|
|
58
|
+
base = "inline-flex items-center rounded-xl font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 cursor-pointer transition-colors "
|
|
59
|
+
base + size_classes + action_classes
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def size_classes
|
|
63
|
+
case @size
|
|
64
|
+
when :small, :sm
|
|
65
|
+
"gap-x-1.5 px-3 py-1.5 text-xs "
|
|
66
|
+
when :medium, :regular, :md
|
|
67
|
+
"gap-x-2 px-4 py-2 text-sm "
|
|
68
|
+
when :large, :lg
|
|
69
|
+
"gap-x-2 px-5 py-2.5 text-base "
|
|
70
|
+
else
|
|
71
|
+
"gap-x-2 px-4 py-2 text-sm "
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def action_classes
|
|
76
|
+
case @action
|
|
77
|
+
when :save, :create, :add, :new
|
|
78
|
+
"text-white bg-primary-500 hover:bg-primary-600 focus-visible:outline-primary-600"
|
|
79
|
+
when :save_inactive
|
|
80
|
+
"text-white bg-gray-300 cursor-not-allowed"
|
|
81
|
+
when :secondary
|
|
82
|
+
"text-gray-700 border border-gray-200 bg-white hover:bg-gray-50"
|
|
83
|
+
when :delete, :destroy, :danger
|
|
84
|
+
"text-error-600 border border-error-200 bg-error-50 hover:bg-error-100 focus-visible:outline-error-300"
|
|
85
|
+
else
|
|
86
|
+
"text-gray-700 border border-gray-200 bg-white hover:bg-gray-50"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_text
|
|
91
|
+
parts = []
|
|
92
|
+
parts << content_tag(:i, "", class: "fa-solid fa-#{@icon}") if @icon
|
|
93
|
+
parts << @text.titleize
|
|
94
|
+
parts << content_tag(:i, "", class: "fa-solid fa-chevron-down text-xs", aria: {hidden: "true"})
|
|
95
|
+
safe_join(parts)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<div <%= tag.attributes(**attrs) %>
|
|
2
|
+
data-controller="dropdown"
|
|
3
|
+
data-action="click@window->dropdown#hide keydown.escape@window->dropdown#hide">
|
|
4
|
+
<button type="button"
|
|
5
|
+
data-action="click->dropdown#toggle"
|
|
6
|
+
data-dropdown-target="button"
|
|
7
|
+
class="inline-flex items-center gap-1 text-gray-500 hover:text-gray-700 focus:outline-none"
|
|
8
|
+
aria-haspopup="true">
|
|
9
|
+
<% if trigger_slot? %>
|
|
10
|
+
<%= trigger_slot %>
|
|
11
|
+
<% else %>
|
|
12
|
+
<%= chevron_icon %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</button>
|
|
15
|
+
|
|
16
|
+
<div data-dropdown-target="menu"
|
|
17
|
+
class="hidden absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-lg bg-white shadow-sm border border-gray-200 focus:outline-none"
|
|
18
|
+
data-transition-enter="transition ease-out duration-100"
|
|
19
|
+
data-transition-enter-from="transform opacity-0 scale-95"
|
|
20
|
+
data-transition-enter-to="transform opacity-100 scale-100"
|
|
21
|
+
data-transition-leave="transition ease-in duration-75"
|
|
22
|
+
data-transition-leave-from="transform opacity-100 scale-100"
|
|
23
|
+
data-transition-leave-to="transform opacity-0 scale-95"
|
|
24
|
+
role="menu"
|
|
25
|
+
aria-orientation="vertical">
|
|
26
|
+
<div class="py-1" role="none">
|
|
27
|
+
<% items.each do |item| %>
|
|
28
|
+
<%= item %>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Reusable dropdown menu component with a chevron trigger.
|
|
7
|
+
#
|
|
8
|
+
# Uses the pre-registered `dropdown` Stimulus controller from
|
|
9
|
+
# tailwindcss-stimulus-components for toggle, outside-click close,
|
|
10
|
+
# Escape key close, and CSS transitions.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic dropdown
|
|
13
|
+
# <%= render Panda::Core::Admin::DropdownComponent.new do |dropdown| %>
|
|
14
|
+
# <% dropdown.with_item(label: "Edit", href: edit_path) %>
|
|
15
|
+
# <% dropdown.with_item(label: "Delete", href: delete_path, method: :delete) %>
|
|
16
|
+
# <% end %>
|
|
17
|
+
#
|
|
18
|
+
# @example With custom trigger content
|
|
19
|
+
# <%= render Panda::Core::Admin::DropdownComponent.new do |dropdown| %>
|
|
20
|
+
# <% dropdown.with_trigger_slot do %>
|
|
21
|
+
# <span class="text-xs text-gray-500">Last 30 days</span>
|
|
22
|
+
# <i class="fa-solid fa-chevron-down text-xs"></i>
|
|
23
|
+
# <% end %>
|
|
24
|
+
# <% dropdown.with_item(label: "Last 7 days", href: "?period=7d") %>
|
|
25
|
+
# <% end %>
|
|
26
|
+
#
|
|
27
|
+
class DropdownComponent < Panda::Core::Base
|
|
28
|
+
renders_one :trigger_slot
|
|
29
|
+
renders_many :items, "ItemComponent"
|
|
30
|
+
|
|
31
|
+
class ItemComponent < Panda::Core::Base
|
|
32
|
+
def initialize(label:, href: "#", method: nil, **attrs)
|
|
33
|
+
@label = label
|
|
34
|
+
@href = href
|
|
35
|
+
@method = method
|
|
36
|
+
super(**attrs)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :label, :href, :method
|
|
40
|
+
|
|
41
|
+
def call
|
|
42
|
+
options = {
|
|
43
|
+
class: "block w-full text-left px-4 py-2 text-xs text-gray-500 hover:bg-gray-50 hover:text-gray-700",
|
|
44
|
+
role: "menuitem",
|
|
45
|
+
data: {dropdown_target: "menuItem"}
|
|
46
|
+
}
|
|
47
|
+
options[:data][:turbo_method] = method if method
|
|
48
|
+
|
|
49
|
+
content_tag(:a, label, **options.merge(href: href))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def default_attrs
|
|
56
|
+
{class: "relative inline-block text-left"}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def chevron_icon
|
|
60
|
+
content_tag(:i, nil, class: "fa-solid fa-chevron-down text-xs", aria: {hidden: "true"})
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<%= content_tag :div, **attrs do %>
|
|
2
|
+
<% if icon.present? %>
|
|
3
|
+
<div class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-gray-100 text-gray-400">
|
|
4
|
+
<i class="<%= icon %>"></i>
|
|
5
|
+
</div>
|
|
6
|
+
<% end %>
|
|
7
|
+
<h3 class="text-lg font-semibold text-gray-900"><%= title %></h3>
|
|
8
|
+
<% if description.present? %>
|
|
9
|
+
<p class="mt-2 text-sm text-gray-500"><%= description %></p>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% if content? %>
|
|
12
|
+
<div class="mt-4 flex justify-center">
|
|
13
|
+
<%= content %>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Empty state component for tables and lists.
|
|
7
|
+
class EmptyStateComponent < Panda::Core::Base
|
|
8
|
+
def initialize(title:, description: nil, icon: nil, **attrs)
|
|
9
|
+
@title = title
|
|
10
|
+
@description = description
|
|
11
|
+
@icon = icon
|
|
12
|
+
super(**attrs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :title, :description, :icon
|
|
16
|
+
|
|
17
|
+
def default_attrs
|
|
18
|
+
{
|
|
19
|
+
class: "border-2 border-dashed border-gray-200 bg-white rounded-2xl px-6 py-10 text-center"
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<% if @files.any? %>
|
|
2
|
+
<%# Gallery %>
|
|
3
|
+
<section>
|
|
4
|
+
<h2 id="gallery-heading" class="sr-only">Files</h2>
|
|
5
|
+
<ul role="list" class="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
|
|
6
|
+
<% @files.each do |file| %>
|
|
7
|
+
<% is_selected = @selected_file && @selected_file.id == file.id %>
|
|
8
|
+
<li class="relative cursor-pointer">
|
|
9
|
+
<div class="<%= file_container_classes(is_selected) %> aspect-square">
|
|
10
|
+
<% if file.image? %>
|
|
11
|
+
<%= tag.img src: url_for(file),
|
|
12
|
+
alt: file.filename.to_s,
|
|
13
|
+
class: file_image_classes(is_selected),
|
|
14
|
+
onerror: "this.onerror=null;this.style.display='none';this.nextElementSibling.classList.remove('hidden')" %>
|
|
15
|
+
<div class="hidden flex items-center justify-center h-full bg-gray-100 dark:bg-gray-700" role="img" aria-label="Image preview not available">
|
|
16
|
+
<i class="fa-solid fa-file-image text-4xl text-gray-400"></i>
|
|
17
|
+
</div>
|
|
18
|
+
<% else %>
|
|
19
|
+
<%# File icon placeholder - implement based on file type %>
|
|
20
|
+
<div class="flex items-center justify-center h-full bg-gray-100 dark:bg-gray-700">
|
|
21
|
+
<i class="fa-solid fa-file text-4xl text-gray-400"></i>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<%= content_tag :button,
|
|
26
|
+
type: "button",
|
|
27
|
+
class: "absolute inset-0 focus:outline-hidden",
|
|
28
|
+
data: {
|
|
29
|
+
action: "click->file-gallery#selectFile",
|
|
30
|
+
file_id: file.id,
|
|
31
|
+
file_url: url_for(file),
|
|
32
|
+
file_name: file.filename.to_s,
|
|
33
|
+
file_size: file.byte_size,
|
|
34
|
+
file_type: file.content_type,
|
|
35
|
+
file_created: file.created_at.to_s
|
|
36
|
+
} do %>
|
|
37
|
+
<span class="sr-only">View details for <%= file.filename %></span>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<p class="pointer-events-none mt-2 block truncate text-sm font-medium text-gray-900 dark:text-white">
|
|
42
|
+
<%= file.filename.to_s %>
|
|
43
|
+
</p>
|
|
44
|
+
<p class="pointer-events-none block text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
45
|
+
<%= file_metadata(file) %>
|
|
46
|
+
</p>
|
|
47
|
+
</li>
|
|
48
|
+
<% end %>
|
|
49
|
+
</ul>
|
|
50
|
+
</section>
|
|
51
|
+
<% else %>
|
|
52
|
+
<%# Empty state %>
|
|
53
|
+
<div class="text-center py-12">
|
|
54
|
+
<i class="fa-solid fa-images text-6xl text-gray-400 mb-4"></i>
|
|
55
|
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white">No files</h3>
|
|
56
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">Upload files to see them here.</p>
|
|
57
|
+
</div>
|
|
58
|
+
<% end %>
|