panda-core 0.12.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +53 -0
- data/app/assets/builds/panda_core_admin.css +2 -0
- data/app/assets/tailwind/application.css +413 -131
- data/app/builders/panda/core/form_builder.rb +129 -27
- data/app/components/panda/core/UI/badge.html.erb +10 -0
- data/app/components/panda/core/UI/badge.rb +14 -30
- data/app/components/panda/core/UI/button.html.erb +5 -0
- data/app/components/panda/core/UI/button.rb +15 -19
- data/app/components/panda/core/UI/card.html.erb +3 -0
- data/app/components/panda/core/UI/card.rb +7 -6
- data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
- data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
- data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
- data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
- data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
- data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
- data/app/components/panda/core/admin/button_component.html.erb +9 -0
- data/app/components/panda/core/admin/button_component.rb +37 -32
- data/app/components/panda/core/admin/callout_component.html.erb +17 -0
- data/app/components/panda/core/admin/callout_component.rb +57 -0
- data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
- data/app/components/panda/core/admin/code_block_component.rb +23 -0
- data/app/components/panda/core/admin/container_component.html.erb +27 -0
- data/app/components/panda/core/admin/container_component.rb +11 -77
- data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
- data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
- data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
- data/app/components/panda/core/admin/delete_button_component.rb +41 -0
- data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
- data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
- data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
- data/app/components/panda/core/admin/dropdown_component.rb +65 -0
- data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
- data/app/components/panda/core/admin/empty_state_component.rb +25 -0
- data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
- data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
- data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
- data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
- data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
- data/app/components/panda/core/admin/flash_message_component.rb +33 -60
- data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
- data/app/components/panda/core/admin/form_error_component.rb +8 -16
- data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_footer_component.rb +65 -0
- data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
- data/app/components/panda/core/admin/form_input_component.rb +21 -13
- data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_section_component.rb +64 -0
- data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
- data/app/components/panda/core/admin/form_select_component.rb +25 -26
- data/app/components/panda/core/admin/heading_component.html.erb +26 -0
- data/app/components/panda/core/admin/heading_component.rb +23 -60
- data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
- data/app/components/panda/core/admin/login_form_component.rb +28 -0
- data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
- data/app/components/panda/core/admin/main_layout_component.rb +19 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
- data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
- data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
- data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
- data/app/components/panda/core/admin/page_header_component.rb +19 -35
- data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
- data/app/components/panda/core/admin/pagination_component.rb +56 -0
- data/app/components/panda/core/admin/panel_component.html.erb +19 -0
- data/app/components/panda/core/admin/panel_component.rb +4 -29
- data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
- data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
- data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
- data/app/components/panda/core/admin/search_bar_component.rb +31 -0
- data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
- data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
- data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
- data/app/components/panda/core/admin/secret_field_component.rb +31 -0
- data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
- data/app/components/panda/core/admin/sidebar_component.rb +55 -0
- data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
- data/app/components/panda/core/admin/slideover_component.rb +31 -70
- data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/core/admin/statistics_component.rb +6 -8
- data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
- data/app/components/panda/core/admin/tab_bar_component.rb +34 -69
- data/app/components/panda/core/admin/table_component.html.erb +82 -0
- data/app/components/panda/core/admin/table_component.rb +54 -87
- data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
- data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
- data/app/components/panda/core/admin/tag_component.html.erb +1 -0
- data/app/components/panda/core/admin/tag_component.rb +51 -20
- data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
- data/app/components/panda/core/admin/tag_input_component.rb +22 -0
- data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
- data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
- data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
- data/app/components/panda/core/admin/user_activity_component.rb +9 -22
- data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
- data/app/components/panda/core/admin/user_display_component.rb +19 -22
- data/app/components/panda/core/base.rb +35 -42
- data/app/components/panda/core/shared/footer_component.html.erb +2 -0
- data/app/components/panda/core/shared/footer_component.rb +11 -0
- data/app/components/panda/core/shared/header_component.html.erb +32 -0
- data/app/components/panda/core/shared/header_component.rb +19 -0
- data/app/constraints/panda/core/admin_constraint.rb +2 -2
- data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
- data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
- data/app/controllers/panda/core/admin/base_controller.rb +62 -3
- data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
- data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
- data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
- data/app/controllers/panda/core/admin/files_controller.rb +230 -0
- data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
- data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
- data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
- data/app/controllers/panda/core/admin/search_controller.rb +15 -0
- data/app/controllers/panda/core/admin/sessions_controller.rb +85 -8
- data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
- data/app/controllers/panda/core/admin/users_controller.rb +184 -0
- data/app/controllers/panda/core/application_controller.rb +13 -1
- data/app/helpers/panda/core/active_storage_helper.rb +24 -0
- data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
- data/app/helpers/panda/core/form_helper.rb +15 -0
- data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
- data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
- data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
- data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
- data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
- data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
- data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
- data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
- data/app/javascript/panda/core/controllers/index.js +36 -0
- data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
- data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
- data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
- data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
- data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
- data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
- data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
- data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
- data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
- data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
- data/app/models/concerns/panda/core/has_metadata.rb +164 -0
- data/app/models/concerns/panda/core/importable.rb +79 -0
- data/app/models/concerns/panda/core/searchable.rb +66 -0
- data/app/models/concerns/panda/core/taggable.rb +37 -0
- data/app/models/panda/core/feature_flag.rb +56 -0
- data/app/models/panda/core/file_categorization.rb +14 -0
- data/app/models/panda/core/file_category.rb +45 -0
- data/app/models/panda/core/import_session.rb +79 -0
- data/app/models/panda/core/presence.rb +24 -0
- data/app/models/panda/core/tag.rb +33 -0
- data/app/models/panda/core/tagging.rb +14 -0
- data/app/models/panda/core/user.rb +96 -18
- data/app/models/panda/core/user_activity.rb +42 -0
- data/app/models/panda/core/user_session.rb +35 -0
- data/app/services/panda/core/attach_avatar_service.rb +15 -3
- data/app/services/panda/core/csv_import_service.rb +65 -0
- data/app/services/panda/core/file_categorizer.rb +52 -0
- data/app/services/panda/core/file_parser.rb +76 -0
- data/app/services/panda/core/invite_user_service.rb +44 -0
- data/app/services/panda/core/presence_service.rb +49 -0
- data/app/views/layouts/panda/core/admin.html.erb +6 -67
- data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
- data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
- data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
- data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
- data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
- data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
- data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
- data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
- data/app/views/panda/core/admin/files/index.html.erb +61 -0
- data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
- data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
- data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
- data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
- data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
- data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
- data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
- data/app/views/panda/core/admin/sessions/new.html.erb +1 -38
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
- data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
- data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
- data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
- data/app/views/panda/core/admin/tags/index.html.erb +41 -0
- data/app/views/panda/core/admin/tags/new.html.erb +4 -0
- data/app/views/panda/core/admin/users/activity.html.erb +90 -0
- data/app/views/panda/core/admin/users/edit.html.erb +48 -0
- data/app/views/panda/core/admin/users/index.html.erb +161 -0
- data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
- data/app/views/panda/core/admin/users/show.html.erb +224 -0
- data/app/views/panda/core/shared/_footer.html.erb +1 -2
- data/app/views/panda/core/shared/_header.html.erb +4 -22
- data/config/brakeman.ignore +68 -31
- data/config/importmap.rb +13 -11
- data/config/routes.rb +49 -2
- data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
- data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
- data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
- data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
- data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
- data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
- data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
- data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
- data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
- data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
- data/lib/generators/panda/core/install_generator.rb +32 -0
- data/lib/generators/panda/core/templates/panda.rb +63 -0
- data/lib/panda/core/asset_loader.rb +6 -3
- data/lib/panda/core/authentication.rb +3 -3
- data/lib/panda/core/configuration.rb +195 -1
- data/lib/panda/core/engine/admin_controller_config.rb +4 -3
- data/lib/panda/core/engine/autoload_config.rb +21 -9
- data/lib/panda/core/engine/omniauth_config.rb +114 -32
- data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
- data/lib/panda/core/engine/route_config.rb +1 -1
- data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
- data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
- data/lib/panda/core/engine.rb +90 -25
- data/lib/panda/core/middleware.rb +35 -0
- data/lib/panda/core/module_registry.rb +21 -13
- data/lib/panda/core/navigation_registry.rb +455 -0
- data/lib/panda/core/oauth_providers.rb +27 -0
- data/lib/panda/core/permission_registry.rb +89 -0
- data/lib/panda/core/search_registry.rb +69 -0
- data/lib/panda/core/seeds/file_categories.rb +30 -0
- data/lib/panda/core/shared/inflections_config.rb +1 -5
- data/lib/panda/core/testing/rails_helper.rb +87 -0
- data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
- data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
- data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core/widget_registry.rb +50 -0
- data/lib/panda/core.rb +20 -0
- data/lib/tasks/panda/core/file_categories.rake +41 -0
- data/public/panda-core-assets/panda-core-0.12.2.css +2 -0
- data/public/panda-core-assets/panda-core-0.12.3.css +2 -0
- data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
- data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- data/public/panda-core-assets/vanilla-calendar.css +47 -0
- metadata +205 -26
- data/lib/panda/core/engine/inflections_config.rb +0 -23
|
@@ -10,6 +10,12 @@ module Panda
|
|
|
10
10
|
super(attribute, text, options.reverse_merge(class: label_styles))
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# Override field_id to match FormBuilder's signature, since the included
|
|
14
|
+
# FormTagHelper defines a field_id with a different arity that shadows it.
|
|
15
|
+
def field_id(method, *suffixes, namespace: @options[:namespace], index: @options[:index])
|
|
16
|
+
@template.field_id(@object_name, method, *suffixes, namespace: namespace, index: index)
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
def text_field(attribute, options = {})
|
|
14
20
|
# Extract custom label if provided
|
|
15
21
|
custom_label = options.delete(:label)
|
|
@@ -48,6 +54,22 @@ module Panda
|
|
|
48
54
|
end
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
def telephone_field(method, options = {})
|
|
58
|
+
custom_label = options.delete(:label)
|
|
59
|
+
|
|
60
|
+
content_tag :div, class: container_styles do
|
|
61
|
+
label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def url_field(method, options = {})
|
|
66
|
+
custom_label = options.delete(:label)
|
|
67
|
+
|
|
68
|
+
content_tag :div, class: container_styles do
|
|
69
|
+
label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
51
73
|
def datetime_field(method, options = {})
|
|
52
74
|
# Extract custom label if provided
|
|
53
75
|
custom_label = options.delete(:label)
|
|
@@ -57,6 +79,31 @@ module Panda
|
|
|
57
79
|
end
|
|
58
80
|
end
|
|
59
81
|
|
|
82
|
+
def datetime_split_field(method, options = {})
|
|
83
|
+
custom_label = options.delete(:label)
|
|
84
|
+
value = object.respond_to?(method) ? object.send(method) : nil
|
|
85
|
+
|
|
86
|
+
content_tag :div, class: container_styles, data: {controller: "datetime"} do
|
|
87
|
+
label(method, custom_label) +
|
|
88
|
+
meta_text(options) +
|
|
89
|
+
hidden_field(method, data: {datetime_target: "combinedField"}, value: value&.iso8601) +
|
|
90
|
+
content_tag(:div, class: "flex gap-3") do
|
|
91
|
+
@template.tag.input(
|
|
92
|
+
type: "date",
|
|
93
|
+
class: input_styles,
|
|
94
|
+
data: {datetime_target: "dateField", action: "change->datetime#update"}
|
|
95
|
+
) +
|
|
96
|
+
@template.tag.input(
|
|
97
|
+
type: "time",
|
|
98
|
+
step: "60",
|
|
99
|
+
class: input_styles,
|
|
100
|
+
data: {datetime_target: "timeField", action: "change->datetime#update"}
|
|
101
|
+
)
|
|
102
|
+
end +
|
|
103
|
+
error_message(method)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
60
107
|
def text_area(method, options = {})
|
|
61
108
|
# Extract custom label if provided
|
|
62
109
|
custom_label = options.delete(:label)
|
|
@@ -75,12 +122,24 @@ module Panda
|
|
|
75
122
|
end
|
|
76
123
|
end
|
|
77
124
|
|
|
78
|
-
def
|
|
125
|
+
def number_field(method, options = {})
|
|
79
126
|
# Extract custom label if provided
|
|
80
127
|
custom_label = options.delete(:label)
|
|
81
128
|
|
|
82
129
|
content_tag :div, class: container_styles do
|
|
83
|
-
label(method, custom_label) + meta_text(options) + super(method,
|
|
130
|
+
label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def select(method, choices = nil, options = {}, html_options = {})
|
|
135
|
+
# Extract custom label if provided
|
|
136
|
+
custom_label = options.delete(:label)
|
|
137
|
+
|
|
138
|
+
html_options = html_options.reverse_merge(class: select_styles)
|
|
139
|
+
html_options[:data] = (html_options[:data] || {}).merge("custom-select-target": "select")
|
|
140
|
+
|
|
141
|
+
content_tag :div, class: container_styles, data: {controller: "custom-select"} do
|
|
142
|
+
label(method, custom_label) + meta_text(options) + super(method, choices, options, html_options) + select_svg + error_message(method)
|
|
84
143
|
end
|
|
85
144
|
end
|
|
86
145
|
|
|
@@ -88,19 +147,25 @@ module Panda
|
|
|
88
147
|
# Extract custom label if provided
|
|
89
148
|
custom_label = options.delete(:label)
|
|
90
149
|
|
|
91
|
-
|
|
92
|
-
|
|
150
|
+
html_options = html_options.reverse_merge(class: input_styles)
|
|
151
|
+
html_options[:data] = (html_options[:data] || {}).merge("custom-select-target": "select")
|
|
152
|
+
|
|
153
|
+
content_tag :div, class: container_styles, data: {controller: "custom-select"} do
|
|
154
|
+
label(method, custom_label) + meta_text(options) + super(method, collection, value_method, text_method, options, html_options) + error_message(method)
|
|
93
155
|
end
|
|
94
156
|
end
|
|
95
157
|
|
|
96
158
|
def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
|
|
97
|
-
|
|
98
|
-
|
|
159
|
+
html_options = html_options.reverse_merge(class: select_styles)
|
|
160
|
+
html_options[:data] = (html_options[:data] || {}).merge("custom-select-target": "select")
|
|
161
|
+
|
|
162
|
+
content_tag :div, class: container_styles, data: {controller: "custom-select"} do
|
|
163
|
+
label(method) + meta_text(options) + super(
|
|
99
164
|
method,
|
|
100
165
|
priority_zones,
|
|
101
166
|
options,
|
|
102
|
-
html_options
|
|
103
|
-
)
|
|
167
|
+
html_options
|
|
168
|
+
) + error_message(method)
|
|
104
169
|
end
|
|
105
170
|
end
|
|
106
171
|
|
|
@@ -125,8 +190,7 @@ module Panda
|
|
|
125
190
|
content_tag :div, class: container_styles do
|
|
126
191
|
label(method, custom_label) +
|
|
127
192
|
meta_text(options) +
|
|
128
|
-
#
|
|
129
|
-
@template.content_tag(:link, nil, rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/cropperjs@1.6.2/dist/cropper.min.css") +
|
|
193
|
+
# Note: cropperjs 2.x uses Web Components with built-in styles — no external CSS needed
|
|
130
194
|
# File input
|
|
131
195
|
content_tag(:div, class: "mt-2") do
|
|
132
196
|
super(method, options.reverse_merge(
|
|
@@ -149,7 +213,7 @@ module Panda
|
|
|
149
213
|
@template.image_tag("", alt: "Crop preview", data: {image_cropper_target: "preview"}, class: "max-w-full") +
|
|
150
214
|
# Cropper controls
|
|
151
215
|
content_tag(:div, class: "mt-4 flex gap-2 flex-wrap") do
|
|
152
|
-
@template.button_tag("Crop & Save", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-
|
|
216
|
+
@template.button_tag("Crop & Save", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-primary-600", data: {action: "click->image-cropper#crop"}) +
|
|
153
217
|
@template.button_tag("Cancel", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#cancel"}) +
|
|
154
218
|
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#reset"}) do
|
|
155
219
|
@template.content_tag(:i, "", class: "fa-solid fa-rotate-left") +
|
|
@@ -200,7 +264,7 @@ module Panda
|
|
|
200
264
|
end +
|
|
201
265
|
# Upload area
|
|
202
266
|
content_tag(:div, class: "mt-4 flex items-baseline justify-center text-sm leading-6 text-gray-600 dark:text-gray-400") do
|
|
203
|
-
content_tag(:label, for: field_id, class: "relative cursor-pointer rounded-md bg-transparent font-semibold text-
|
|
267
|
+
content_tag(:label, for: field_id, class: "relative cursor-pointer rounded-md bg-transparent font-semibold text-primary-600 focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary-600 hover:text-primary-500 dark:text-primary-400 dark:focus-within:outline-primary-500 dark:hover:text-primary-300") do
|
|
204
268
|
content_tag(:span, "Upload a file") +
|
|
205
269
|
super(method, options.reverse_merge(
|
|
206
270
|
id: field_id,
|
|
@@ -261,11 +325,11 @@ module Panda
|
|
|
261
325
|
def submit(value = nil, options = {})
|
|
262
326
|
value ||= submit_default_value
|
|
263
327
|
|
|
264
|
-
# Use the primary
|
|
265
|
-
action = object
|
|
328
|
+
# Use the primary color for save/create actions
|
|
329
|
+
action = object&.persisted? ? :save : :create
|
|
266
330
|
button_classes = case action
|
|
267
331
|
when :save, :create
|
|
268
|
-
"text-white bg-
|
|
332
|
+
"text-white bg-primary-500 hover:bg-primary-600"
|
|
269
333
|
when :save_inactive
|
|
270
334
|
"text-white bg-gray-400"
|
|
271
335
|
when :secondary
|
|
@@ -275,7 +339,7 @@ module Panda
|
|
|
275
339
|
end
|
|
276
340
|
|
|
277
341
|
# Combine with common button classes
|
|
278
|
-
classes = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-
|
|
342
|
+
classes = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 px-3 py-2 #{button_classes}"
|
|
279
343
|
|
|
280
344
|
options[:class] = options[:class] ? "#{options[:class]} #{classes}" : classes
|
|
281
345
|
|
|
@@ -287,16 +351,48 @@ module Panda
|
|
|
287
351
|
custom_label = options.delete(:label)
|
|
288
352
|
|
|
289
353
|
content_tag :div, class: container_styles do
|
|
290
|
-
label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: "border-gray-300 ml-2"), checked_value, unchecked_value)
|
|
354
|
+
label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: "border-gray-300 text-primary-600 focus:ring-primary-500 ml-2"), checked_value, unchecked_value)
|
|
291
355
|
end
|
|
292
356
|
end
|
|
293
357
|
|
|
294
358
|
def date_field(method, options = {})
|
|
295
|
-
# Extract custom label if provided
|
|
296
359
|
custom_label = options.delete(:label)
|
|
360
|
+
date_min = options.delete(:min)
|
|
361
|
+
date_max = options.delete(:max)
|
|
297
362
|
|
|
298
|
-
|
|
299
|
-
|
|
363
|
+
# Format current value for display
|
|
364
|
+
raw_value = object.respond_to?(method) ? object.send(method) : nil
|
|
365
|
+
iso_value = if raw_value.respond_to?(:strftime)
|
|
366
|
+
raw_value.strftime("%Y-%m-%d")
|
|
367
|
+
else
|
|
368
|
+
raw_value
|
|
369
|
+
end
|
|
370
|
+
display_value = if raw_value.respond_to?(:strftime)
|
|
371
|
+
raw_value.strftime("%-d %b %Y")
|
|
372
|
+
elsif raw_value.is_a?(String) && raw_value.present?
|
|
373
|
+
raw_value
|
|
374
|
+
else
|
|
375
|
+
""
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
controller_data = {controller: "datepicker"}
|
|
379
|
+
controller_data[:datepicker_date_min_value] = date_min.to_s if date_min
|
|
380
|
+
controller_data[:datepicker_date_max_value] = date_max.to_s if date_max
|
|
381
|
+
|
|
382
|
+
content_tag :div, class: "#{container_styles} relative", data: controller_data do
|
|
383
|
+
label(method, custom_label) +
|
|
384
|
+
meta_text(options) +
|
|
385
|
+
hidden_field(method, value: iso_value, data: {datepicker_target: "hidden"}) +
|
|
386
|
+
@template.text_field_tag(
|
|
387
|
+
nil,
|
|
388
|
+
display_value,
|
|
389
|
+
class: "#{input_styles} cursor-pointer",
|
|
390
|
+
readonly: true,
|
|
391
|
+
placeholder: options[:placeholder] || "Select date...",
|
|
392
|
+
data: {datepicker_target: "display", action: "click->datepicker#toggle focus->datepicker#toggle"}
|
|
393
|
+
) +
|
|
394
|
+
content_tag(:div, "", class: "hidden absolute z-50 mt-1 left-0", data: {datepicker_target: "calendar"}) +
|
|
395
|
+
error_message(method)
|
|
300
396
|
end
|
|
301
397
|
end
|
|
302
398
|
|
|
@@ -317,7 +413,7 @@ module Panda
|
|
|
317
413
|
is_checked = (current_value.to_s == choice_value.to_s)
|
|
318
414
|
|
|
319
415
|
content_tag(:label, class: "flex items-center gap-x-3 rounded-lg border border-gray-300 px-3 py-3 text-sm/6 font-medium cursor-pointer hover:bg-gray-50 dark:border-white/10 dark:hover:bg-white/5") do
|
|
320
|
-
radio_button(method, choice_value, {id: choice_id, checked: is_checked, class: "size-4 border-gray-300 text-
|
|
416
|
+
radio_button(method, choice_value, {id: choice_id, checked: is_checked, class: "size-4 border-gray-300 text-primary-600 focus:ring-primary-600 dark:border-white/10 dark:bg-white/5"}) +
|
|
321
417
|
content_tag(:span, choice_label, class: "text-gray-900 dark:text-white")
|
|
322
418
|
end
|
|
323
419
|
end.join.html_safe
|
|
@@ -332,8 +428,8 @@ module Panda
|
|
|
332
428
|
end
|
|
333
429
|
|
|
334
430
|
def section_heading(text, options = {})
|
|
335
|
-
@template.content_tag(:div, class: "-
|
|
336
|
-
@template.content_tag(:h3, text, class: "text-
|
|
431
|
+
@template.content_tag(:div, class: "mt-6 pt-4 border-t border-gray-200 mb-4") do
|
|
432
|
+
@template.content_tag(:h3, text, class: "text-sm font-semibold text-gray-700")
|
|
337
433
|
end
|
|
338
434
|
end
|
|
339
435
|
|
|
@@ -344,7 +440,7 @@ module Panda
|
|
|
344
440
|
end
|
|
345
441
|
|
|
346
442
|
def base_input_styles
|
|
347
|
-
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-
|
|
443
|
+
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-primary-500"
|
|
348
444
|
end
|
|
349
445
|
|
|
350
446
|
def input_styles
|
|
@@ -360,7 +456,7 @@ module Panda
|
|
|
360
456
|
end
|
|
361
457
|
|
|
362
458
|
def select_styles
|
|
363
|
-
"block w-full rounded-md bg-white px-3 py-1.5 pr-8 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 appearance-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-
|
|
459
|
+
"block w-full rounded-md bg-white px-3 py-1.5 pr-8 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 appearance-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:focus:outline-primary-500"
|
|
364
460
|
end
|
|
365
461
|
|
|
366
462
|
def select_svg
|
|
@@ -383,7 +479,13 @@ module Panda
|
|
|
383
479
|
end
|
|
384
480
|
|
|
385
481
|
def submit_default_value
|
|
386
|
-
object.
|
|
482
|
+
if object.nil?
|
|
483
|
+
"Submit"
|
|
484
|
+
elsif object.persisted?
|
|
485
|
+
"Update #{object.class.name.demodulize}"
|
|
486
|
+
else
|
|
487
|
+
"Create #{object.class.name.demodulize}"
|
|
488
|
+
end
|
|
387
489
|
end
|
|
388
490
|
|
|
389
491
|
def wrap_field(method, options = {}, &block)
|
|
@@ -401,7 +503,7 @@ module Panda
|
|
|
401
503
|
def error_message(attribute)
|
|
402
504
|
return unless object.respond_to?(:errors) && object.errors[attribute]&.any?
|
|
403
505
|
|
|
404
|
-
content_tag(:p, class: "mt-2 text-sm text-
|
|
506
|
+
content_tag(:p, class: "mt-2 text-sm text-error-600") do
|
|
405
507
|
object.errors[attribute].join(", ")
|
|
406
508
|
end
|
|
407
509
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%= content_tag :span, @attrs do %>
|
|
2
|
+
<%= text %>
|
|
3
|
+
<% if removable? %>
|
|
4
|
+
<%= content_tag :button, type: "button", class: "inline-flex items-center ml-1 hover:opacity-70", aria: { label: "Remove" } do %>
|
|
5
|
+
<%= content_tag :svg, class: "h-3 w-3", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor" do %>
|
|
6
|
+
<%= tag.path d: "M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% end %>
|
|
@@ -27,45 +27,29 @@ module Panda
|
|
|
27
27
|
# )
|
|
28
28
|
#
|
|
29
29
|
class Badge < Panda::Core::Base
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
def initialize(text:, variant: :default, size: :medium, removable: false, rounded: false, **attrs)
|
|
31
|
+
@text = text
|
|
32
|
+
@variant = variant
|
|
33
|
+
@size = size
|
|
34
|
+
@removable = removable
|
|
35
|
+
@rounded = rounded
|
|
36
|
+
super(**attrs)
|
|
37
|
+
end
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
whitespace
|
|
41
|
-
button(
|
|
42
|
-
type: "button",
|
|
43
|
-
class: "inline-flex items-center ml-1 hover:opacity-70",
|
|
44
|
-
aria: {label: "Remove"}
|
|
45
|
-
) do
|
|
46
|
-
svg(
|
|
47
|
-
class: "h-3 w-3",
|
|
48
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
49
|
-
viewBox: "0 0 20 20",
|
|
50
|
-
fill: "currentColor"
|
|
51
|
-
) do |s|
|
|
52
|
-
s.path(
|
|
53
|
-
d: "M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
39
|
+
attr_reader :text, :variant, :size, :removable, :rounded
|
|
40
|
+
|
|
41
|
+
def removable?
|
|
42
|
+
removable
|
|
59
43
|
end
|
|
60
44
|
|
|
45
|
+
private
|
|
46
|
+
|
|
61
47
|
def default_attrs
|
|
62
48
|
{
|
|
63
49
|
class: badge_classes
|
|
64
50
|
}
|
|
65
51
|
end
|
|
66
52
|
|
|
67
|
-
private
|
|
68
|
-
|
|
69
53
|
def badge_classes
|
|
70
54
|
base = "inline-flex items-center font-medium"
|
|
71
55
|
base += " #{size_classes}"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Panda
|
|
4
4
|
module Core
|
|
5
5
|
module UI
|
|
6
|
-
# Modern
|
|
6
|
+
# Modern ViewComponent-based button component with type-safe props.
|
|
7
7
|
#
|
|
8
8
|
# Supports both <button> and <a> elements based on whether an href is provided.
|
|
9
9
|
# Follows Tailwind UI Plus styling patterns with dark mode support.
|
|
@@ -34,22 +34,18 @@ module Panda
|
|
|
34
34
|
# )
|
|
35
35
|
#
|
|
36
36
|
class Button < Panda::Core::Base
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def view_template
|
|
46
|
-
if @href
|
|
47
|
-
a(**@attrs) { @text }
|
|
48
|
-
else
|
|
49
|
-
button(**@attrs) { @text }
|
|
50
|
-
end
|
|
37
|
+
def initialize(text: "", variant: :default, size: :medium, disabled: false, type: "button", href: nil, **attrs)
|
|
38
|
+
@text = text
|
|
39
|
+
@variant = variant
|
|
40
|
+
@size = size
|
|
41
|
+
@disabled = disabled
|
|
42
|
+
@type = type
|
|
43
|
+
@href = href
|
|
44
|
+
super(**attrs)
|
|
51
45
|
end
|
|
52
46
|
|
|
47
|
+
attr_reader :text, :variant, :size, :disabled, :type, :href
|
|
48
|
+
|
|
53
49
|
def default_attrs
|
|
54
50
|
base = {
|
|
55
51
|
class: button_classes
|
|
@@ -89,15 +85,15 @@ module Panda
|
|
|
89
85
|
def variant_classes
|
|
90
86
|
case @variant
|
|
91
87
|
when :primary
|
|
92
|
-
#
|
|
93
|
-
"bg-
|
|
88
|
+
# Primary button with dark mode support
|
|
89
|
+
"bg-primary-500 text-white shadow-xs hover:bg-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 dark:bg-primary-400 dark:shadow-none dark:hover:bg-primary-500 dark:focus-visible:outline-primary-500"
|
|
94
90
|
when :secondary
|
|
95
91
|
# White/gray secondary button with ring and dark mode support
|
|
96
92
|
"bg-white text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
|
|
97
93
|
when :success
|
|
98
|
-
"bg-
|
|
94
|
+
"bg-success-600 text-white shadow-xs hover:bg-success-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-success-600 dark:bg-success-500 dark:shadow-none dark:hover:bg-success-600 dark:focus-visible:outline-success-500"
|
|
99
95
|
when :danger
|
|
100
|
-
"bg-
|
|
96
|
+
"bg-error-600 text-white shadow-xs hover:bg-error-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-error-600 dark:bg-error-500 dark:shadow-none dark:hover:bg-error-600 dark:focus-visible:outline-error-500"
|
|
101
97
|
when :ghost
|
|
102
98
|
"bg-transparent text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
103
99
|
else # :default
|
|
@@ -29,14 +29,15 @@ module Panda
|
|
|
29
29
|
# end
|
|
30
30
|
#
|
|
31
31
|
class Card < Panda::Core::Base
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
div(**@attrs, &block)
|
|
32
|
+
def initialize(padding: :medium, elevation: :low, border: true, **attrs)
|
|
33
|
+
@padding = padding
|
|
34
|
+
@elevation = elevation
|
|
35
|
+
@border = border
|
|
36
|
+
super(**attrs)
|
|
38
37
|
end
|
|
39
38
|
|
|
39
|
+
attr_reader :padding, :elevation, :border
|
|
40
|
+
|
|
40
41
|
def default_attrs
|
|
41
42
|
{
|
|
42
43
|
class: card_classes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
|
|
2
|
+
<% panel.with_heading_slot do %>
|
|
3
|
+
<%= heading %>
|
|
4
|
+
<span class="text-sm font-normal text-gray-500 dark:text-gray-400 ml-1">(<%= changes.size %> changed)</span>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% panel.with_body_slot do %>
|
|
7
|
+
<div class="divide-y divide-gray-200 dark:divide-gray-700 -mx-4 -mb-4">
|
|
8
|
+
<% changes.each do |attr, values| %>
|
|
9
|
+
<div class="p-4 grid grid-cols-3 gap-4">
|
|
10
|
+
<div class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= attr.humanize %></div>
|
|
11
|
+
<div class="text-sm">
|
|
12
|
+
<span class="px-1 bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 line-through"><%= display_value(values[:old]) %></span>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="text-sm">
|
|
15
|
+
<span class="px-1 bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200"><%= display_value(values[:new]) %></span>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class AttributeDiffComponent < Panda::Core::Base
|
|
7
|
+
def initialize(changes:, heading: "Attribute Changes", **attrs)
|
|
8
|
+
@changes = changes
|
|
9
|
+
@heading = heading
|
|
10
|
+
super(**attrs)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :changes, :heading
|
|
14
|
+
|
|
15
|
+
def render?
|
|
16
|
+
changes.any?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def display_value(value)
|
|
20
|
+
if value.is_a?(FalseClass) || value.is_a?(TrueClass)
|
|
21
|
+
value.to_s
|
|
22
|
+
else
|
|
23
|
+
value.presence || "(empty)"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<%# Mobile back link %>
|
|
3
|
+
<% if @show_back && @items.any? %>
|
|
4
|
+
<nav aria-label="Back" class="sm:hidden">
|
|
5
|
+
<%= content_tag :a, href: back_link_href, class: "flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300" do %>
|
|
6
|
+
<%= render_chevron_left_icon %>
|
|
7
|
+
Back
|
|
8
|
+
<% end %>
|
|
9
|
+
</nav>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<%# Desktop breadcrumb trail %>
|
|
13
|
+
<nav aria-label="Breadcrumb" class="hidden sm:flex">
|
|
14
|
+
<ol role="list" class="flex items-center space-x-4">
|
|
15
|
+
<% @items.each_with_index do |item, index| %>
|
|
16
|
+
<li>
|
|
17
|
+
<% if index.zero? %>
|
|
18
|
+
<%# First item (no separator) %>
|
|
19
|
+
<div class="flex">
|
|
20
|
+
<%= content_tag :a, item[:text], href: item[:href], class: breadcrumb_link_classes(index) %>
|
|
21
|
+
</div>
|
|
22
|
+
<% else %>
|
|
23
|
+
<%# Subsequent items (with separator) %>
|
|
24
|
+
<div class="flex items-center">
|
|
25
|
+
<%= render_chevron_right_icon %>
|
|
26
|
+
<%= content_tag :a, item[:text],
|
|
27
|
+
href: item[:href],
|
|
28
|
+
aria: ((index == @items.length - 1) ? {current: "page"} : nil),
|
|
29
|
+
class: breadcrumb_link_classes(index) %>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
</li>
|
|
33
|
+
<% end %>
|
|
34
|
+
</ol>
|
|
35
|
+
</nav>
|
|
36
|
+
</div>
|
|
@@ -26,64 +26,14 @@ module Panda
|
|
|
26
26
|
# )
|
|
27
27
|
#
|
|
28
28
|
class BreadcrumbComponent < Panda::Core::Base
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
div do
|
|
34
|
-
# Mobile back link
|
|
35
|
-
if @show_back && @items.any?
|
|
36
|
-
nav(
|
|
37
|
-
aria: {label: "Back"},
|
|
38
|
-
class: "sm:hidden"
|
|
39
|
-
) do
|
|
40
|
-
a(
|
|
41
|
-
href: back_link_href,
|
|
42
|
-
class: "flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
|
43
|
-
) do
|
|
44
|
-
render_chevron_left_icon
|
|
45
|
-
plain "Back"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Desktop breadcrumb trail
|
|
51
|
-
nav(
|
|
52
|
-
aria: {label: "Breadcrumb"},
|
|
53
|
-
class: "hidden sm:flex"
|
|
54
|
-
) do
|
|
55
|
-
ol(
|
|
56
|
-
role: "list",
|
|
57
|
-
class: "flex items-center space-x-4"
|
|
58
|
-
) do
|
|
59
|
-
@items.each_with_index do |item, index|
|
|
60
|
-
li do
|
|
61
|
-
if index.zero?
|
|
62
|
-
# First item (no separator)
|
|
63
|
-
div(class: "flex") do
|
|
64
|
-
a(
|
|
65
|
-
href: item[:href],
|
|
66
|
-
class: breadcrumb_link_classes(index)
|
|
67
|
-
) { item[:text] }
|
|
68
|
-
end
|
|
69
|
-
else
|
|
70
|
-
# Subsequent items (with separator)
|
|
71
|
-
div(class: "flex items-center") do
|
|
72
|
-
render_chevron_right_icon
|
|
73
|
-
a(
|
|
74
|
-
href: item[:href],
|
|
75
|
-
aria: ((index == @items.length - 1) ? {current: "page"} : nil),
|
|
76
|
-
class: breadcrumb_link_classes(index)
|
|
77
|
-
) { item[:text] }
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
29
|
+
def initialize(items: [], show_back: true, **attrs)
|
|
30
|
+
@items = items
|
|
31
|
+
@show_back = show_back
|
|
32
|
+
super(**attrs)
|
|
85
33
|
end
|
|
86
34
|
|
|
35
|
+
attr_reader :items, :show_back
|
|
36
|
+
|
|
87
37
|
private
|
|
88
38
|
|
|
89
39
|
def back_link_href
|
|
@@ -97,14 +47,13 @@ module Panda
|
|
|
97
47
|
end
|
|
98
48
|
|
|
99
49
|
def render_chevron_left_icon
|
|
100
|
-
svg
|
|
50
|
+
content_tag(:svg,
|
|
101
51
|
viewBox: "0 0 20 20",
|
|
102
52
|
fill: "currentColor",
|
|
103
53
|
data: {slot: "icon"},
|
|
104
54
|
aria: {hidden: "true"},
|
|
105
|
-
class: "mr-1 -ml-1 size-5 shrink-0 text-gray-400 dark:text-gray-500"
|
|
106
|
-
|
|
107
|
-
s.path(
|
|
55
|
+
class: "mr-1 -ml-1 size-5 shrink-0 text-gray-400 dark:text-gray-500") do
|
|
56
|
+
tag.path(
|
|
108
57
|
d: "M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z",
|
|
109
58
|
clip_rule: "evenodd",
|
|
110
59
|
fill_rule: "evenodd"
|
|
@@ -113,14 +62,13 @@ module Panda
|
|
|
113
62
|
end
|
|
114
63
|
|
|
115
64
|
def render_chevron_right_icon
|
|
116
|
-
svg
|
|
65
|
+
content_tag(:svg,
|
|
117
66
|
viewBox: "0 0 20 20",
|
|
118
67
|
fill: "currentColor",
|
|
119
68
|
data: {slot: "icon"},
|
|
120
69
|
aria: {hidden: "true"},
|
|
121
|
-
class: "size-5 shrink-0 text-gray-400 dark:text-gray-500"
|
|
122
|
-
|
|
123
|
-
s.path(
|
|
70
|
+
class: "size-5 shrink-0 text-gray-400 dark:text-gray-500") do
|
|
71
|
+
tag.path(
|
|
124
72
|
d: "M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z",
|
|
125
73
|
clip_rule: "evenodd",
|
|
126
74
|
fill_rule: "evenodd"
|