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,75 +4,24 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class FileGalleryComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
render_gallery
|
|
13
|
-
else
|
|
14
|
-
render_empty_state
|
|
15
|
-
end
|
|
7
|
+
def initialize(selected_file: nil, files: [], blob_categories: {}, **attrs)
|
|
8
|
+
@files = files
|
|
9
|
+
@selected_file = selected_file
|
|
10
|
+
@blob_categories = blob_categories
|
|
11
|
+
super(**attrs)
|
|
16
12
|
end
|
|
17
13
|
|
|
14
|
+
attr_reader :files, :selected_file, :blob_categories
|
|
15
|
+
|
|
18
16
|
private
|
|
19
17
|
|
|
20
18
|
def render_gallery
|
|
21
|
-
|
|
22
|
-
h2(id: "gallery-heading", class: "sr-only") { "Files" }
|
|
23
|
-
ul(
|
|
24
|
-
role: "list",
|
|
25
|
-
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"
|
|
26
|
-
) do
|
|
27
|
-
@files.each do |file|
|
|
28
|
-
render_file_item(file)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
19
|
+
# Implemented in ERB template
|
|
32
20
|
end
|
|
33
21
|
|
|
34
22
|
def render_file_item(file)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
li(class: "relative") do
|
|
38
|
-
div(
|
|
39
|
-
class: file_container_classes(is_selected),
|
|
40
|
-
style: "cursor: pointer;"
|
|
41
|
-
) do
|
|
42
|
-
if file.image?
|
|
43
|
-
img(
|
|
44
|
-
src: url_for(file),
|
|
45
|
-
alt: file.filename.to_s,
|
|
46
|
-
class: file_image_classes(is_selected)
|
|
47
|
-
)
|
|
48
|
-
else
|
|
49
|
-
render_file_icon(file)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
button(
|
|
53
|
-
type: "button",
|
|
54
|
-
class: "absolute inset-0 focus:outline-hidden",
|
|
55
|
-
data: {
|
|
56
|
-
action: "click->file-gallery#selectFile",
|
|
57
|
-
file_id: file.id,
|
|
58
|
-
file_url: url_for(file),
|
|
59
|
-
file_name: file.filename.to_s,
|
|
60
|
-
file_size: file.byte_size,
|
|
61
|
-
file_type: file.content_type,
|
|
62
|
-
file_created: file.created_at.to_s
|
|
63
|
-
}
|
|
64
|
-
) do
|
|
65
|
-
span(class: "sr-only") { "View details for #{file.filename}" }
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
p(class: "pointer-events-none mt-2 block truncate text-sm font-medium text-gray-900 dark:text-white") do
|
|
70
|
-
plain file.filename.to_s
|
|
71
|
-
end
|
|
72
|
-
p(class: "pointer-events-none block text-sm font-medium text-gray-500 dark:text-gray-400") do
|
|
73
|
-
plain number_to_human_size(file.byte_size)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
23
|
+
@selected_file && @selected_file.id == file.id
|
|
24
|
+
# Implemented in ERB template
|
|
76
25
|
end
|
|
77
26
|
|
|
78
27
|
def file_container_classes(selected)
|
|
@@ -80,56 +29,33 @@ module Panda
|
|
|
80
29
|
focus = if selected
|
|
81
30
|
"outline-2 outline-offset-2 outline-panda-dark dark:outline-panda-light outline"
|
|
82
31
|
else
|
|
83
|
-
"focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-
|
|
32
|
+
"focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary-600 dark:focus-within:outline-primary-500"
|
|
84
33
|
end
|
|
85
34
|
"#{base} #{focus}"
|
|
86
35
|
end
|
|
87
36
|
|
|
88
37
|
def file_image_classes(selected)
|
|
89
|
-
base = "pointer-events-none aspect-
|
|
38
|
+
base = "pointer-events-none aspect-square object-contain p-1 outline -outline-offset-1 outline-black/5 dark:outline-white/10"
|
|
90
39
|
hover = selected ? "" : "group-hover:opacity-75"
|
|
91
40
|
"#{base} #{hover}"
|
|
92
41
|
end
|
|
93
42
|
|
|
94
43
|
def render_file_icon(file)
|
|
95
|
-
|
|
96
|
-
div(class: "text-center") do
|
|
97
|
-
svg(
|
|
98
|
-
class: "mx-auto h-12 w-12 text-gray-400",
|
|
99
|
-
fill: "none",
|
|
100
|
-
viewBox: "0 0 24 24",
|
|
101
|
-
stroke: "currentColor",
|
|
102
|
-
aria: {hidden: "true"}
|
|
103
|
-
) do
|
|
104
|
-
path(
|
|
105
|
-
stroke_linecap: "round",
|
|
106
|
-
stroke_linejoin: "round",
|
|
107
|
-
d: "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
|
108
|
-
)
|
|
109
|
-
end
|
|
110
|
-
p(class: "mt-1 text-xs text-gray-500 uppercase") { file.content_type&.split("/")&.last || "file" }
|
|
111
|
-
end
|
|
112
|
-
end
|
|
44
|
+
# Implemented in ERB template
|
|
113
45
|
end
|
|
114
46
|
|
|
115
47
|
def render_empty_state
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
d: "M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
|
128
|
-
)
|
|
129
|
-
end
|
|
130
|
-
h3(class: "mt-2 text-sm font-semibold text-gray-900") { "No files" }
|
|
131
|
-
p(class: "mt-1 text-sm text-gray-500") { "Get started by uploading a file." }
|
|
132
|
-
end
|
|
48
|
+
# Implemented in ERB template
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Build the metadata subtitle for a file in the gallery grid
|
|
52
|
+
def file_metadata(file)
|
|
53
|
+
parts = []
|
|
54
|
+
variant_count = file.variant_records.size
|
|
55
|
+
parts << "#{variant_count} #{"variant".pluralize(variant_count)}" if variant_count > 0
|
|
56
|
+
category = @blob_categories[file.id]
|
|
57
|
+
parts << category.name if category
|
|
58
|
+
parts.any? ? parts.join(" \u00B7 ") : number_to_human_size(file.byte_size)
|
|
133
59
|
end
|
|
134
60
|
|
|
135
61
|
# Helper method to generate URL for ActiveStorage attachment
|
|
@@ -139,6 +65,8 @@ module Panda
|
|
|
139
65
|
else
|
|
140
66
|
"#"
|
|
141
67
|
end
|
|
68
|
+
rescue
|
|
69
|
+
"#"
|
|
142
70
|
end
|
|
143
71
|
|
|
144
72
|
# Helper method for human-readable file sizes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<div class="flex flex-wrap gap-2 mb-4">
|
|
2
|
+
<%= link_to @all_label, @url_helper.call(nil), class: pill_classes(@active_value.blank?) %>
|
|
3
|
+
<% @items.each do |item| %>
|
|
4
|
+
<%= link_to item[:label], @url_helper.call(item[:value]), class: pill_classes(@active_value == item[:value]) %>
|
|
5
|
+
<% end %>
|
|
6
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class FilterPillsComponent < Panda::Core::Base
|
|
7
|
+
def initialize(items:, url_helper:, param_name: :category, active_value: nil, all_label: "All", **attrs)
|
|
8
|
+
@items = items
|
|
9
|
+
@url_helper = url_helper
|
|
10
|
+
@param_name = param_name
|
|
11
|
+
@active_value = active_value
|
|
12
|
+
@all_label = all_label
|
|
13
|
+
super(**attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :items, :url_helper, :param_name, :active_value, :all_label
|
|
17
|
+
|
|
18
|
+
def pill_classes(active)
|
|
19
|
+
base = "inline-flex items-center rounded-full px-3 py-1.5 text-xs font-medium"
|
|
20
|
+
if active
|
|
21
|
+
"#{base} bg-primary-100 text-primary-700"
|
|
22
|
+
else
|
|
23
|
+
"#{base} bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50">
|
|
2
|
+
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
|
|
3
|
+
<%= content_tag :div, notification_attrs do %>
|
|
4
|
+
<div class="flex items-center">
|
|
5
|
+
<%= render_icon %>
|
|
6
|
+
<%= render_content %>
|
|
7
|
+
<%= render_close_button %>
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
@@ -4,37 +4,19 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class FlashMessageComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# Global notification container (fixed position)
|
|
14
|
-
div(
|
|
15
|
-
aria: {live: "assertive"},
|
|
16
|
-
class: "pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50"
|
|
17
|
-
) do
|
|
18
|
-
div(class: "flex w-full flex-col items-center space-y-4 sm:items-end") do
|
|
19
|
-
# Notification panel with Tailwind Plus styling
|
|
20
|
-
div(**notification_attrs) do
|
|
21
|
-
div(class: "p-4") do
|
|
22
|
-
div(class: "flex items-start") do
|
|
23
|
-
render_icon
|
|
24
|
-
render_content
|
|
25
|
-
render_close_button
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
7
|
+
def initialize(message: "", kind: :default, temporary: true, subtitle: nil, **attrs)
|
|
8
|
+
@message = message
|
|
9
|
+
@kind = kind
|
|
10
|
+
@temporary = temporary
|
|
11
|
+
@subtitle = subtitle
|
|
12
|
+
super(**attrs)
|
|
31
13
|
end
|
|
32
14
|
|
|
33
|
-
|
|
15
|
+
attr_reader :message, :kind, :temporary, :subtitle
|
|
34
16
|
|
|
35
17
|
def notification_attrs
|
|
36
18
|
{
|
|
37
|
-
class: "pointer-events-auto w-full max-w-
|
|
19
|
+
class: "pointer-events-auto w-full max-w-md translate-y-0 transform rounded-2xl border px-4 py-3 shadow-lg transition duration-300 ease-out sm:translate-x-0 starting:translate-y-2 starting:opacity-0 starting:sm:translate-x-2 starting:sm:translate-y-0 #{tone_classes}",
|
|
38
20
|
data: {
|
|
39
21
|
controller: "alert",
|
|
40
22
|
alert_dismiss_after_value: (@temporary ? "5000" : nil)
|
|
@@ -43,39 +25,30 @@ module Panda
|
|
|
43
25
|
end
|
|
44
26
|
|
|
45
27
|
def render_icon
|
|
46
|
-
div
|
|
47
|
-
i
|
|
28
|
+
content_tag(:div, class: "shrink-0") do
|
|
29
|
+
content_tag(:i, "", class: "fa-solid size-5 #{icon_css} #{icon_colour_css}")
|
|
48
30
|
end
|
|
49
31
|
end
|
|
50
32
|
|
|
51
33
|
def render_content
|
|
52
|
-
div
|
|
53
|
-
p
|
|
54
|
-
if @subtitle
|
|
55
|
-
p
|
|
34
|
+
content_tag(:div, class: "ml-2 w-0 flex-1") do
|
|
35
|
+
message_html = content_tag(:p, @message, class: "text-sm font-medium flash-message-title")
|
|
36
|
+
subtitle_html = if @subtitle
|
|
37
|
+
content_tag(:p, @subtitle, class: "mt-1 text-xs opacity-80 flash-message-subtitle")
|
|
38
|
+
else
|
|
39
|
+
"".html_safe
|
|
56
40
|
end
|
|
41
|
+
(message_html + subtitle_html).html_safe
|
|
57
42
|
end
|
|
58
43
|
end
|
|
59
44
|
|
|
60
45
|
def render_close_button
|
|
61
|
-
div
|
|
62
|
-
button
|
|
46
|
+
content_tag(:div, class: "ml-4 flex shrink-0") do
|
|
47
|
+
content_tag(:button,
|
|
63
48
|
type: "button",
|
|
64
|
-
class: "inline-flex
|
|
65
|
-
data: {action: "alert#close"}
|
|
66
|
-
|
|
67
|
-
span(class: "sr-only") { "Close" }
|
|
68
|
-
svg(
|
|
69
|
-
viewBox: "0 0 20 20",
|
|
70
|
-
fill: "currentColor",
|
|
71
|
-
data: {slot: "icon"},
|
|
72
|
-
aria: {hidden: "true"},
|
|
73
|
-
class: "size-5"
|
|
74
|
-
) do |s|
|
|
75
|
-
s.path(
|
|
76
|
-
d: "M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
|
|
77
|
-
)
|
|
78
|
-
end
|
|
49
|
+
class: "inline-flex items-center cursor-pointer opacity-60 hover:opacity-100",
|
|
50
|
+
data: {action: "alert#close"}) do
|
|
51
|
+
content_tag(:i, "", class: "fa-solid fa-xmark")
|
|
79
52
|
end
|
|
80
53
|
end
|
|
81
54
|
end
|
|
@@ -83,30 +56,30 @@ module Panda
|
|
|
83
56
|
def icon_colour_css
|
|
84
57
|
case @kind
|
|
85
58
|
when :success
|
|
86
|
-
"text-
|
|
59
|
+
"text-emerald-600"
|
|
87
60
|
when :alert, :error
|
|
88
|
-
"text-
|
|
61
|
+
"text-rose-600"
|
|
89
62
|
when :warning
|
|
90
|
-
"text-
|
|
63
|
+
"text-amber-600"
|
|
91
64
|
when :info, :notice
|
|
92
|
-
"text-
|
|
65
|
+
"text-sky-600"
|
|
93
66
|
else
|
|
94
|
-
"text-gray-
|
|
67
|
+
"text-gray-500"
|
|
95
68
|
end
|
|
96
69
|
end
|
|
97
70
|
|
|
98
|
-
def
|
|
71
|
+
def tone_classes
|
|
99
72
|
case @kind
|
|
100
73
|
when :success
|
|
101
|
-
"
|
|
74
|
+
"bg-emerald-50 text-emerald-700 border-emerald-200"
|
|
102
75
|
when :alert, :error
|
|
103
|
-
"
|
|
76
|
+
"bg-rose-50 text-rose-700 border-rose-200"
|
|
104
77
|
when :warning
|
|
105
|
-
"
|
|
78
|
+
"bg-amber-50 text-amber-700 border-amber-200"
|
|
106
79
|
when :info, :notice
|
|
107
|
-
"
|
|
80
|
+
"bg-sky-50 text-sky-700 border-sky-200"
|
|
108
81
|
else
|
|
109
|
-
"
|
|
82
|
+
"bg-white text-gray-700 border-gray-200"
|
|
110
83
|
end
|
|
111
84
|
end
|
|
112
85
|
|
|
@@ -4,33 +4,25 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class FormErrorComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def view_template
|
|
11
|
-
return unless should_render?
|
|
12
|
-
|
|
13
|
-
div(**@attrs) do
|
|
14
|
-
div(class: "text-sm text-red-600") do
|
|
15
|
-
error_messages.each do |message|
|
|
16
|
-
p { message }
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
7
|
+
def initialize(model:, **attrs)
|
|
8
|
+
@model = model
|
|
9
|
+
super(**attrs)
|
|
20
10
|
end
|
|
21
11
|
|
|
12
|
+
attr_reader :model
|
|
13
|
+
|
|
22
14
|
def default_attrs
|
|
23
15
|
{
|
|
24
16
|
class: "mb-4 p-4 bg-red-50 border border-red-200 rounded-md"
|
|
25
17
|
}
|
|
26
18
|
end
|
|
27
19
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def should_render?
|
|
20
|
+
def render?
|
|
31
21
|
error_messages.any?
|
|
32
22
|
end
|
|
33
23
|
|
|
24
|
+
private
|
|
25
|
+
|
|
34
26
|
def error_messages
|
|
35
27
|
@error_messages ||= if @model&.respond_to?(:errors)
|
|
36
28
|
@model.errors.full_messages
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div <%= tag.attributes(**attrs) %>>
|
|
2
|
+
<% if content? %>
|
|
3
|
+
<%= content %>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<% if cancel_path %>
|
|
7
|
+
<%= link_to "Cancel", cancel_path, class: cancel_link_classes %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<%= tag.button type: "submit", class: submit_button_classes, data: submit_data_attrs do %>
|
|
11
|
+
<% if computed_icon %>
|
|
12
|
+
<i class="fa-solid <%= computed_icon %>"></i>
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= submit_text %>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Standardized form footer component with submit button(s) and optional secondary actions.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# <%= render Panda::Core::Admin::FormFooterComponent.new(submit_text: "Save") %>
|
|
10
|
+
#
|
|
11
|
+
# @example With icon
|
|
12
|
+
# <%= render Panda::Core::Admin::FormFooterComponent.new(
|
|
13
|
+
# submit_text: "Create Page",
|
|
14
|
+
# icon: "fa-plus"
|
|
15
|
+
# ) %>
|
|
16
|
+
#
|
|
17
|
+
# @example With cancel link
|
|
18
|
+
# <%= render Panda::Core::Admin::FormFooterComponent.new(
|
|
19
|
+
# submit_text: "Update",
|
|
20
|
+
# cancel_path: admin_cms_pages_path
|
|
21
|
+
# ) %>
|
|
22
|
+
#
|
|
23
|
+
# @example With block for custom secondary actions
|
|
24
|
+
# <%= render Panda::Core::Admin::FormFooterComponent.new(submit_text: "Save") do %>
|
|
25
|
+
# <%= link_to "Preview", preview_path, class: "text-sm text-gray-600" %>
|
|
26
|
+
# <% end %>
|
|
27
|
+
#
|
|
28
|
+
class FormFooterComponent < Panda::Core::Base
|
|
29
|
+
def initialize(submit_text: "Save", icon: nil, cancel_path: nil, submit_action: nil, **attrs)
|
|
30
|
+
@submit_text = submit_text
|
|
31
|
+
@icon = icon
|
|
32
|
+
@cancel_path = cancel_path
|
|
33
|
+
@submit_action = submit_action
|
|
34
|
+
super(**attrs)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :submit_text, :icon, :cancel_path, :submit_action
|
|
38
|
+
|
|
39
|
+
def default_attrs
|
|
40
|
+
{
|
|
41
|
+
class: "flex justify-end gap-x-3 mt-2"
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def submit_button_classes
|
|
46
|
+
"inline-flex items-center gap-x-2 justify-center rounded-xl bg-primary-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def cancel_link_classes
|
|
50
|
+
"inline-flex items-center justify-center rounded-xl px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-900"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def computed_icon
|
|
54
|
+
@icon
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def submit_data_attrs
|
|
58
|
+
attrs = {disable_with: "Saving..."}
|
|
59
|
+
attrs[:action] = submit_action if submit_action
|
|
60
|
+
attrs
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= tag.input(**attrs) %>
|
|
@@ -4,18 +4,19 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class FormInputComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
input(**@attrs)
|
|
7
|
+
def initialize(name: "", value: nil, placeholder: nil, autocomplete: nil, type: :text, required: false, disabled: false, **attrs)
|
|
8
|
+
@name = name
|
|
9
|
+
@value = value
|
|
10
|
+
@type = type
|
|
11
|
+
@placeholder = placeholder
|
|
12
|
+
@required = required
|
|
13
|
+
@disabled = disabled
|
|
14
|
+
@autocomplete = autocomplete
|
|
15
|
+
super(**attrs)
|
|
17
16
|
end
|
|
18
17
|
|
|
18
|
+
attr_reader :name, :value, :type, :placeholder, :required, :disabled, :autocomplete
|
|
19
|
+
|
|
19
20
|
def default_attrs
|
|
20
21
|
base_attrs = {
|
|
21
22
|
type: @type.to_s,
|
|
@@ -36,12 +37,19 @@ module Panda
|
|
|
36
37
|
private
|
|
37
38
|
|
|
38
39
|
def input_classes
|
|
39
|
-
|
|
40
|
+
# Border/radius/focus-ring are themeable via the --panda-input-*
|
|
41
|
+
# tokens (see app/assets/tailwind/application.css). The var()
|
|
42
|
+
# fallbacks resolve to today's rounded-xl/border-gray-300/
|
|
43
|
+
# outline-primary-500 exactly, so this is a no-op unless a host
|
|
44
|
+
# app sets data-theme.
|
|
45
|
+
classes = "block w-full h-11 rounded-[var(--panda-input-radius,var(--radius-xl))] border border-[var(--panda-input-border,var(--color-gray-300))] bg-white px-3 py-2 text-gray-900 " \
|
|
46
|
+
"placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-[var(--panda-input-focus-ring,var(--color-primary-500))] " \
|
|
47
|
+
"dark:bg-white/5 dark:text-white dark:border-white/10 dark:placeholder:text-gray-500"
|
|
40
48
|
|
|
41
49
|
if @disabled
|
|
42
|
-
classes + "
|
|
50
|
+
classes + " border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
43
51
|
else
|
|
44
|
-
classes
|
|
52
|
+
classes
|
|
45
53
|
end
|
|
46
54
|
end
|
|
47
55
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div <%= tag.attributes(**attrs) %>>
|
|
2
|
+
<h3 class="<%= heading_classes %>">
|
|
3
|
+
<% if icon %>
|
|
4
|
+
<i class="fa-solid <%= icon %>"></i>
|
|
5
|
+
<% end %>
|
|
6
|
+
<%= title %>
|
|
7
|
+
</h3>
|
|
8
|
+
|
|
9
|
+
<% if description %>
|
|
10
|
+
<p class="<%= description_classes %>"><%= description %></p>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<div class="<%= content_classes %>">
|
|
14
|
+
<%= content %>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Standardized form section component for grouping related form fields.
|
|
7
|
+
# Provides a consistent heading style for form sections without the heavy
|
|
8
|
+
# visual weight of PanelComponent.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# <%= render Panda::Core::Admin::FormSectionComponent.new(title: "SEO Settings") do %>
|
|
12
|
+
# <%= f.text_field :seo_title %>
|
|
13
|
+
# <%= f.text_area :seo_description %>
|
|
14
|
+
# <% end %>
|
|
15
|
+
#
|
|
16
|
+
# @example With description
|
|
17
|
+
# <%= render Panda::Core::Admin::FormSectionComponent.new(
|
|
18
|
+
# title: "Notification Settings",
|
|
19
|
+
# description: "Configure how you receive notifications"
|
|
20
|
+
# ) do %>
|
|
21
|
+
# ...
|
|
22
|
+
# <% end %>
|
|
23
|
+
#
|
|
24
|
+
# @example With icon
|
|
25
|
+
# <%= render Panda::Core::Admin::FormSectionComponent.new(
|
|
26
|
+
# title: "Menu Items",
|
|
27
|
+
# icon: "fa-bars"
|
|
28
|
+
# ) do %>
|
|
29
|
+
# ...
|
|
30
|
+
# <% end %>
|
|
31
|
+
#
|
|
32
|
+
class FormSectionComponent < Panda::Core::Base
|
|
33
|
+
def initialize(title:, description: nil, icon: nil, border_top: true, **attrs)
|
|
34
|
+
@title = title
|
|
35
|
+
@description = description
|
|
36
|
+
@icon = icon
|
|
37
|
+
@border_top = border_top
|
|
38
|
+
super(**attrs)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :title, :description, :icon, :border_top
|
|
42
|
+
|
|
43
|
+
def default_attrs
|
|
44
|
+
base_classes = "mt-6 pt-4"
|
|
45
|
+
base_classes += " border-t border-gray-200" if border_top
|
|
46
|
+
|
|
47
|
+
{class: base_classes}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def heading_classes
|
|
51
|
+
"text-sm font-semibold text-gray-700 flex items-center gap-2"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def description_classes
|
|
55
|
+
"mt-1 text-xs text-gray-500"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def content_classes
|
|
59
|
+
"mt-4 space-y-4"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|