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
|
@@ -4,95 +4,60 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class TabBarComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
div(class: "mt-3 sm:mt-2") do
|
|
11
|
-
render_mobile_select
|
|
12
|
-
render_desktop_tabs
|
|
13
|
-
end
|
|
7
|
+
def initialize(tabs: [].freeze, **attrs)
|
|
8
|
+
@tabs = tabs
|
|
9
|
+
super(**attrs)
|
|
14
10
|
end
|
|
15
11
|
|
|
12
|
+
attr_reader :tabs
|
|
13
|
+
|
|
16
14
|
private
|
|
17
15
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
label(for: "tabs", class: "sr-only") { "Select a tab" }
|
|
21
|
-
select(
|
|
22
|
-
id: "tabs",
|
|
23
|
-
name: "tabs",
|
|
24
|
-
class: "block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-mid focus:border-panda-dark focus:ring-panda-dark"
|
|
25
|
-
) do
|
|
26
|
-
@tabs.each do |tab|
|
|
27
|
-
option { tab[:name] }
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
16
|
+
def any_tab_current?
|
|
17
|
+
@any_tab_current ||= @tabs.any? { |tab| tab[:current] }
|
|
31
18
|
end
|
|
32
19
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
div(class: "flex items-center border-b border-gray-200") do
|
|
36
|
-
nav(class: "flex flex-1 -mb-px space-x-6 xl:space-x-8", aria: {label: "Tabs"}) do
|
|
37
|
-
@tabs.each_with_index do |tab, index|
|
|
38
|
-
render_tab(tab, index.zero?)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
render_view_toggle
|
|
42
|
-
end
|
|
43
|
-
end
|
|
20
|
+
def tab_current?(tab, index)
|
|
21
|
+
tab[:current] || (!any_tab_current? && index.zero?)
|
|
44
22
|
end
|
|
45
23
|
|
|
46
|
-
def
|
|
24
|
+
def tab_classes(tab, index)
|
|
47
25
|
classes = "py-4 px-1 text-sm font-medium whitespace-nowrap border-b-2 "
|
|
48
|
-
classes += if
|
|
49
|
-
"border-
|
|
26
|
+
classes += if tab_current?(tab, index)
|
|
27
|
+
"border-primary-600 text-primary-600"
|
|
50
28
|
else
|
|
51
29
|
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|
52
30
|
end
|
|
53
|
-
|
|
54
|
-
a(
|
|
55
|
-
href: tab[:url] || "#",
|
|
56
|
-
class: classes,
|
|
57
|
-
aria: {current: (is_current || tab[:current]) ? "page" : nil}
|
|
58
|
-
) { tab[:name] }
|
|
31
|
+
classes
|
|
59
32
|
end
|
|
60
33
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
render_view_button(:list)
|
|
64
|
-
render_view_button(:grid, selected: true)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def render_view_button(type, selected: false)
|
|
69
|
-
button_class = "p-1.5 text-gray-400 rounded-md focus:ring-2 focus:ring-inset focus:outline-none focus:ring-panda-dark"
|
|
34
|
+
def view_button_classes(selected)
|
|
35
|
+
button_class = "p-1.5 text-gray-400 rounded-md focus:ring-2 focus:ring-inset focus:outline-none focus:ring-primary-600"
|
|
70
36
|
button_class += if selected
|
|
71
37
|
" ml-0.5 bg-white shadow-sm"
|
|
72
38
|
else
|
|
73
39
|
" hover:bg-white hover:shadow-sm"
|
|
74
40
|
end
|
|
41
|
+
button_class
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def list_view_icon
|
|
45
|
+
content_tag(:svg, class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do
|
|
46
|
+
tag.path(
|
|
47
|
+
fill_rule: "evenodd",
|
|
48
|
+
d: "M2 3.75A.75.75 0 012.75 3h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.166a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z",
|
|
49
|
+
clip_rule: "evenodd"
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
75
53
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
span(class: "sr-only") { "Use list view" }
|
|
86
|
-
else
|
|
87
|
-
svg(class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do |s|
|
|
88
|
-
s.path(
|
|
89
|
-
fill_rule: "evenodd",
|
|
90
|
-
d: "M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z",
|
|
91
|
-
clip_rule: "evenodd"
|
|
92
|
-
)
|
|
93
|
-
end
|
|
94
|
-
span(class: "sr-only") { "Use grid view" }
|
|
95
|
-
end
|
|
54
|
+
def grid_view_icon
|
|
55
|
+
content_tag(:svg, class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do
|
|
56
|
+
tag.path(
|
|
57
|
+
fill_rule: "evenodd",
|
|
58
|
+
d: "M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z",
|
|
59
|
+
clip_rule: "evenodd"
|
|
60
|
+
)
|
|
96
61
|
end
|
|
97
62
|
end
|
|
98
63
|
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<%# Chrome (bg/border/radius, header/row borders, cell text colour) is
|
|
2
|
+
themeable via the --panda-panel-* / --panda-table-* tokens — see
|
|
3
|
+
app/assets/tailwind/application.css. Fallbacks resolve to today's
|
|
4
|
+
literal utilities exactly, so this is a no-op unless a host app sets
|
|
5
|
+
data-theme. %>
|
|
6
|
+
<% if rows.any? %>
|
|
7
|
+
<% if responsive? %>
|
|
8
|
+
<%# Mobile card view - hidden on lg screens %>
|
|
9
|
+
<div class="mb-12 space-y-3 lg:hidden">
|
|
10
|
+
<% rows.each do |row| %>
|
|
11
|
+
<div class="rounded-[var(--panda-panel-radius,var(--radius-2xl))] border border-[var(--panda-panel-border,var(--color-gray-200))] bg-[var(--panda-panel-bg,var(--color-white))] p-4 hover:bg-gray-50 transition-colors" data-controller="row-link" data-action="click->row-link#click">
|
|
12
|
+
<% columns.each_with_index do |column, i| %>
|
|
13
|
+
<% cell_content = render_cell_content(row, column.cell) %>
|
|
14
|
+
<% if i == 0 %>
|
|
15
|
+
<div class="font-medium text-gray-900 text-sm mb-2"><%= cell_content %></div>
|
|
16
|
+
<% else %>
|
|
17
|
+
<% if cell_content.present? && cell_content.to_s.strip.present? %>
|
|
18
|
+
<div class="flex justify-between items-baseline py-1">
|
|
19
|
+
<span class="text-xs text-gray-400"><%= column.label %></span>
|
|
20
|
+
<span class="text-sm text-gray-600"><%= cell_content %></span>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<%# Desktop table view %>
|
|
31
|
+
<div class="mb-12 w-full overflow-x-auto rounded-[var(--panda-panel-radius,var(--radius-2xl))] border border-[var(--panda-panel-border,var(--color-gray-200))] bg-[var(--panda-panel-bg,var(--color-white))] <%= 'hidden lg:block' if responsive? %>" data-controller="row-link" data-action="click->row-link#click">
|
|
32
|
+
<%# Header %>
|
|
33
|
+
<div class="table w-full" style="table-layout: fixed;">
|
|
34
|
+
<div class="table-header-group">
|
|
35
|
+
<div class="table-row text-sm font-medium text-gray-500 truncate bg-[var(--panda-table-header-bg,var(--color-gray-50))] border-b border-[var(--panda-table-border,var(--color-gray-200))]">
|
|
36
|
+
<% columns.each do |column| %>
|
|
37
|
+
<% header_style = column.width ? "width: #{column.width};" : nil %>
|
|
38
|
+
<% aria_sort = if column.sortable? && sort == column.sort_key
|
|
39
|
+
sort_direction == "asc" ? "ascending" : "descending"
|
|
40
|
+
elsif column.sortable?
|
|
41
|
+
"none"
|
|
42
|
+
end %>
|
|
43
|
+
<div class="table-cell sticky top-0 z-10 px-4 py-3 align-middle" style="<%= header_style %>"<% if aria_sort %> aria-sort="<%= aria_sort %>"<% end %>>
|
|
44
|
+
<% if column.sortable? %>
|
|
45
|
+
<a href="<%= sort_url_for(column) %>" class="inline-flex items-center gap-1 hover:text-gray-900 transition-colors group" aria-label="Sort by <%= column.label %>">
|
|
46
|
+
<%= column.label %>
|
|
47
|
+
<% if (indicator = sort_indicator_for(column)) %>
|
|
48
|
+
<span class="text-gray-700 text-xs" aria-hidden="true"><%= indicator %></span>
|
|
49
|
+
<% else %>
|
|
50
|
+
<span class="text-gray-300 text-xs opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden="true">↕</span>
|
|
51
|
+
<% end %>
|
|
52
|
+
</a>
|
|
53
|
+
<% else %>
|
|
54
|
+
<%= column.label %>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
<% end %>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<%# Rows %>
|
|
62
|
+
<div class="table-row-group">
|
|
63
|
+
<% rows.each do |row| %>
|
|
64
|
+
<div class="table-row odd:bg-white even:bg-gray-50/60 hover:bg-gray-100 cursor-pointer" data-<%= term %>-id="<%= row.id %>">
|
|
65
|
+
<% columns.each do |column| %>
|
|
66
|
+
<div class="table-cell px-4 py-3 text-sm align-middle break-words border-b border-[var(--panda-table-row-border,color-mix(in_srgb,var(--color-gray-200)_70%,transparent))] text-[var(--panda-table-cell-color,var(--color-gray-600))]">
|
|
67
|
+
<%= render_cell_content(row, column.cell) %>
|
|
68
|
+
</div>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<% else %>
|
|
76
|
+
<%# Empty state %>
|
|
77
|
+
<%= render Panda::Core::Admin::EmptyStateComponent.new(
|
|
78
|
+
title: "No #{term.pluralize}",
|
|
79
|
+
description: "Get started by creating a new #{term}.",
|
|
80
|
+
icon: icon
|
|
81
|
+
) %>
|
|
82
|
+
<% end %>
|
|
@@ -4,122 +4,89 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class TableComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
attr_reader :term, :rows, :icon, :sort, :sort_direction
|
|
8
|
+
|
|
9
|
+
def initialize(term: "", rows: [], icon: "", responsive: true, sort: nil, sort_direction: nil, **attrs, &block)
|
|
10
|
+
@term = term
|
|
11
|
+
@rows = rows
|
|
12
|
+
@icon = icon
|
|
13
|
+
@responsive = responsive
|
|
14
|
+
@sort = sort&.to_s
|
|
15
|
+
@sort_direction = (sort_direction&.to_s == "desc") ? "desc" : "asc"
|
|
15
16
|
@columns = []
|
|
17
|
+
@setup_block = block
|
|
18
|
+
super(**attrs)
|
|
16
19
|
end
|
|
17
20
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
instance_eval(&block) if block_given?
|
|
21
|
-
|
|
22
|
-
if @rows.any?
|
|
23
|
-
render_table_with_rows
|
|
24
|
-
else
|
|
25
|
-
render_empty_table
|
|
26
|
-
end
|
|
21
|
+
def responsive?
|
|
22
|
+
@responsive
|
|
27
23
|
end
|
|
28
24
|
|
|
29
|
-
def column(label, width: nil, &cell_block)
|
|
30
|
-
@columns << Column.new(label, width, &cell_block)
|
|
25
|
+
def column(label, width: nil, sortable: false, sort_key: nil, &cell_block)
|
|
26
|
+
@columns << Column.new(label, width, sortable: sortable, sort_key: sort_key, &cell_block)
|
|
27
|
+
self # Allow chaining
|
|
31
28
|
end
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
render_rows
|
|
39
|
-
end
|
|
30
|
+
def render_cell_content(row, cell_block)
|
|
31
|
+
# Use capture to properly handle ERB blocks that output to the template buffer.
|
|
32
|
+
# Directly calling cell_block.call(row) causes double-rendering because ERB blocks
|
|
33
|
+
# both output to the buffer AND return their last expression.
|
|
34
|
+
helpers.capture(row, &cell_block)
|
|
40
35
|
end
|
|
41
36
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
def sort_url_for(column)
|
|
38
|
+
return unless column.sortable?
|
|
39
|
+
|
|
40
|
+
new_direction = (sort == column.sort_key && sort_direction == "asc") ? "desc" : "asc"
|
|
41
|
+
request = helpers.request
|
|
42
|
+
query_params = request.query_parameters.merge("sort" => column.sort_key, "direction" => new_direction)
|
|
43
|
+
"#{request.path}?#{query_params.to_query}"
|
|
48
44
|
end
|
|
49
45
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
div(class: "table-row text-base font-medium text-white bg-dark") do
|
|
53
|
-
@columns.each_with_index do |column, i|
|
|
54
|
-
header_classes = "table-cell sticky top-0 z-10 p-4"
|
|
55
|
-
header_classes += " rounded-tl-md" if i.zero?
|
|
56
|
-
header_classes += " rounded-tr-md" if i == @columns.size - 1
|
|
46
|
+
def sort_indicator_for(column)
|
|
47
|
+
return unless sort == column.sort_key
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
div(class: header_classes, style: header_style) { column.label }
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
49
|
+
(sort_direction == "asc") ? "↑" : "↓"
|
|
63
50
|
end
|
|
64
51
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
end
|
|
78
|
-
end
|
|
52
|
+
# Lazy accessor that ensures columns are registered before returning them.
|
|
53
|
+
# Supports two patterns:
|
|
54
|
+
# 1. Block passed to new() - executed here (for tests)
|
|
55
|
+
# 2. Block passed to render() - executed via content (for ERB templates)
|
|
56
|
+
def columns
|
|
57
|
+
unless @columns_registered
|
|
58
|
+
if @setup_block
|
|
59
|
+
# Block was passed to new() - execute it
|
|
60
|
+
@setup_block.call(self)
|
|
61
|
+
else
|
|
62
|
+
# Block was passed to render() - execute via content
|
|
63
|
+
content
|
|
79
64
|
end
|
|
65
|
+
@columns_registered = true
|
|
80
66
|
end
|
|
67
|
+
@columns
|
|
81
68
|
end
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
# When called from ERB, we need to capture the block's output buffer
|
|
85
|
-
# When called from Phlex, evaluate directly
|
|
86
|
-
if defined?(view_context) && view_context
|
|
87
|
-
# Use capture to get ERB output buffer content
|
|
88
|
-
captured_html = view_context.capture(row, &cell_block)
|
|
89
|
-
# Render the captured HTML (already html_safe from capture)
|
|
90
|
-
raw(captured_html)
|
|
91
|
-
else
|
|
92
|
-
# Pure Phlex context - execute block directly
|
|
93
|
-
result = cell_block.call(row)
|
|
70
|
+
private
|
|
94
71
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
plain(result)
|
|
98
|
-
elsif result.respond_to?(:render_in)
|
|
99
|
-
render(result)
|
|
100
|
-
else
|
|
101
|
-
plain(result.to_s)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
72
|
+
def pluralized_term
|
|
73
|
+
@pluralized_term ||= ActiveSupport::Inflector.pluralize(term)
|
|
104
74
|
end
|
|
105
75
|
end
|
|
106
76
|
|
|
107
77
|
class Column
|
|
108
|
-
attr_reader :label, :cell, :width
|
|
78
|
+
attr_reader :label, :cell, :width, :sort_key
|
|
109
79
|
|
|
110
|
-
def initialize(label, width = nil, &block)
|
|
80
|
+
def initialize(label, width = nil, sortable: false, sort_key: nil, &block)
|
|
111
81
|
@label = label
|
|
112
82
|
@width = width
|
|
83
|
+
@sortable = sortable
|
|
84
|
+
@sort_key = sort_key || ActiveSupport::Inflector.parameterize(label.to_s, separator: "_")
|
|
113
85
|
@cell = block
|
|
114
86
|
end
|
|
115
|
-
end
|
|
116
87
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def initialize(label, &block)
|
|
121
|
-
@label = label
|
|
122
|
-
@cell = Panda::Core::Admin::TagComponent.new(status: block)
|
|
88
|
+
def sortable?
|
|
89
|
+
@sortable
|
|
123
90
|
end
|
|
124
91
|
end
|
|
125
92
|
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium"
|
|
2
|
+
style="background-color: <%= colour %>1a; color: <%= colour %>; border: 1px solid <%= colour %>33;">
|
|
3
|
+
<%= tag.name %>
|
|
4
|
+
<% if removable %>
|
|
5
|
+
<button type="button" class="ml-0.5 hover:opacity-70" data-action="tag-input#removeTag" data-tag-id="<%= tag.id %>">×</button>
|
|
6
|
+
<% end %>
|
|
7
|
+
</span>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class TagBadgeComponent < Panda::Core::Base
|
|
7
|
+
attr_reader :tag, :removable
|
|
8
|
+
|
|
9
|
+
def initialize(tag:, removable: false, **attrs)
|
|
10
|
+
@tag = tag
|
|
11
|
+
@removable = removable
|
|
12
|
+
super(**attrs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def colour
|
|
16
|
+
tag.display_colour
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= content_tag :span, computed_text, class: tag_classes %>
|
|
@@ -4,15 +4,14 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class TagComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
span(class: tag_classes) { computed_text }
|
|
7
|
+
def initialize(text: nil, page_type: nil, status: :active, **attrs)
|
|
8
|
+
@status = status
|
|
9
|
+
@text = text
|
|
10
|
+
@page_type = page_type
|
|
11
|
+
super(**attrs)
|
|
13
12
|
end
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
attr_reader :status, :text, :page_type
|
|
16
15
|
|
|
17
16
|
def computed_text
|
|
18
17
|
if @page_type
|
|
@@ -33,44 +32,76 @@ module Panda
|
|
|
33
32
|
end
|
|
34
33
|
end
|
|
35
34
|
|
|
35
|
+
# Badge colours are themeable via the --panda-badge-{success,error,
|
|
36
|
+
# warning,info,neutral}-{bg,fg} tokens (see
|
|
37
|
+
# app/assets/tailwind/application.css). Each helper below returns an
|
|
38
|
+
# arbitrary-value Tailwind utility pair whose var() fallback resolves
|
|
39
|
+
# to exactly today's literal emerald/amber/sky/rose/gray-100
|
|
40
|
+
# utility classes, so this is a no-op unless a host app sets
|
|
41
|
+
# data-theme.
|
|
36
42
|
def tag_classes
|
|
37
|
-
base = "inline-flex items-center
|
|
43
|
+
base = "inline-flex items-center px-2.5 py-0.5 text-xs font-medium rounded-full "
|
|
38
44
|
base + (@page_type ? type_classes : status_classes)
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def type_classes
|
|
42
48
|
case @page_type
|
|
43
49
|
when :system
|
|
44
|
-
|
|
50
|
+
badge_classes(:error)
|
|
45
51
|
when :posts
|
|
46
|
-
|
|
52
|
+
badge_classes(:info)
|
|
47
53
|
when :code
|
|
48
|
-
|
|
54
|
+
badge_classes(:info)
|
|
49
55
|
when :standard
|
|
50
|
-
|
|
56
|
+
badge_classes(:success)
|
|
51
57
|
when :hidden_type
|
|
52
|
-
|
|
58
|
+
badge_classes(:neutral)
|
|
53
59
|
else
|
|
54
|
-
|
|
60
|
+
badge_classes(:neutral)
|
|
55
61
|
end
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
def status_classes
|
|
59
65
|
case @status
|
|
60
66
|
when :active
|
|
61
|
-
|
|
67
|
+
badge_classes(:success)
|
|
68
|
+
when :live
|
|
69
|
+
badge_classes(:success)
|
|
62
70
|
when :draft
|
|
63
|
-
|
|
71
|
+
badge_classes(:warning)
|
|
64
72
|
when :inactive, :hidden
|
|
65
|
-
|
|
73
|
+
badge_classes(:neutral)
|
|
66
74
|
when :auto
|
|
67
|
-
|
|
75
|
+
badge_classes(:info)
|
|
76
|
+
when :warning
|
|
77
|
+
badge_classes(:error)
|
|
68
78
|
when :static
|
|
69
|
-
|
|
79
|
+
badge_classes(:neutral)
|
|
70
80
|
else
|
|
71
|
-
|
|
81
|
+
badge_classes(:neutral)
|
|
72
82
|
end
|
|
73
83
|
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Tailwind's content scanner extracts candidate class names by
|
|
88
|
+
# scanning raw source text — it does not evaluate Ruby, so every
|
|
89
|
+
# arbitrary-value class must appear as a complete literal string
|
|
90
|
+
# somewhere in this file (interpolating the tone into the class name
|
|
91
|
+
# would produce unscannable text like "bg-[var(--panda-badge-#{tone}
|
|
92
|
+
# -bg...)]" and silently generate no CSS).
|
|
93
|
+
BADGE_CLASSES = {
|
|
94
|
+
success: "text-[var(--panda-badge-success-fg,var(--color-emerald-600))] bg-[var(--panda-badge-success-bg,var(--color-emerald-50))]",
|
|
95
|
+
warning: "text-[var(--panda-badge-warning-fg,var(--color-amber-600))] bg-[var(--panda-badge-warning-bg,var(--color-amber-50))]",
|
|
96
|
+
info: "text-[var(--panda-badge-info-fg,var(--color-sky-600))] bg-[var(--panda-badge-info-bg,var(--color-sky-50))]",
|
|
97
|
+
error: "text-[var(--panda-badge-error-fg,var(--color-rose-600))] bg-[var(--panda-badge-error-bg,var(--color-rose-50))]",
|
|
98
|
+
neutral: "text-[var(--panda-badge-neutral-fg,var(--color-gray-600))] bg-[var(--panda-badge-neutral-bg,var(--color-gray-100))]"
|
|
99
|
+
}.freeze
|
|
100
|
+
private_constant :BADGE_CLASSES
|
|
101
|
+
|
|
102
|
+
def badge_classes(tone)
|
|
103
|
+
BADGE_CLASSES.fetch(tone)
|
|
104
|
+
end
|
|
74
105
|
end
|
|
75
106
|
end
|
|
76
107
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div data-controller="tag-input"
|
|
2
|
+
data-tag-input-url-value="<%= tags_url %>"
|
|
3
|
+
data-tag-input-selected-value="<%= selected_tags_json %>"
|
|
4
|
+
class="relative">
|
|
5
|
+
|
|
6
|
+
<%# Selected tags as pills + hidden inputs %>
|
|
7
|
+
<div data-tag-input-target="selected" class="flex flex-wrap gap-1.5 mb-2">
|
|
8
|
+
<% selected_tags.each do |tag| %>
|
|
9
|
+
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium"
|
|
10
|
+
style="background-color: <%= tag.display_colour %>1a; color: <%= tag.display_colour %>; border: 1px solid <%= tag.display_colour %>33;"
|
|
11
|
+
data-tag-id="<%= tag.id %>">
|
|
12
|
+
<%= tag.name %>
|
|
13
|
+
<button type="button" class="ml-0.5 hover:opacity-70" data-action="tag-input#removeTag" data-tag-id="<%= tag.id %>">×</button>
|
|
14
|
+
</span>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<%# Hidden inputs for form submission %>
|
|
19
|
+
<div data-tag-input-target="hiddenInputs">
|
|
20
|
+
<% selected_tags.each do |tag| %>
|
|
21
|
+
<input type="hidden" name="<%= field_name %>" value="<%= tag.id %>" data-tag-id="<%= tag.id %>">
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<%# Search input %>
|
|
26
|
+
<input type="text"
|
|
27
|
+
data-tag-input-target="input"
|
|
28
|
+
data-action="focus->tag-input#showAll input->tag-input#search keydown->tag-input#keydown"
|
|
29
|
+
placeholder="Search or add tags..."
|
|
30
|
+
autocomplete="off"
|
|
31
|
+
class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-primary">
|
|
32
|
+
|
|
33
|
+
<%# Results dropdown %>
|
|
34
|
+
<div data-tag-input-target="results"
|
|
35
|
+
class="absolute z-10 mt-1 hidden w-full rounded-lg border border-gray-200 bg-white shadow-lg max-h-48 overflow-y-auto">
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class TagInputComponent < Panda::Core::Base
|
|
7
|
+
attr_reader :field_name, :tags_url, :selected_tags
|
|
8
|
+
|
|
9
|
+
def initialize(tags_url:, field_name: "tag_ids[]", selected_tags: [], **attrs)
|
|
10
|
+
@field_name = field_name
|
|
11
|
+
@tags_url = tags_url
|
|
12
|
+
@selected_tags = selected_tags
|
|
13
|
+
super(**attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def selected_tags_json
|
|
17
|
+
selected_tags.map { |t| {id: t.id, name: t.name, colour: t.display_colour} }.to_json
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
|
|
2
|
+
<% panel.with_heading_slot { heading } %>
|
|
3
|
+
<div class="flex flex-wrap gap-2">
|
|
4
|
+
<% tags.each do |tag| %>
|
|
5
|
+
<%= render Panda::Core::Admin::TagBadgeComponent.new(tag: tag) %>
|
|
6
|
+
<% end %>
|
|
7
|
+
</div>
|
|
8
|
+
<% end %>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Renders a PanelComponent containing TagBadgeComponents for a collection of tags.
|
|
7
|
+
# Self-hides via `render?` when the tags collection is empty.
|
|
8
|
+
#
|
|
9
|
+
# Lives in panda-core because tags are a core Panda concept (Panda::Core::Tag)
|
|
10
|
+
# and this component composes two existing panda-core components (PanelComponent
|
|
11
|
+
# and TagBadgeComponent) into a pattern used on most admin show pages.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# render Panda::Core::Admin::TagsPanelComponent.new(tags: @person.tags)
|
|
15
|
+
#
|
|
16
|
+
# @example Custom heading
|
|
17
|
+
# render Panda::Core::Admin::TagsPanelComponent.new(tags: @item.tags, heading: "Categories")
|
|
18
|
+
class TagsPanelComponent < Panda::Core::Base
|
|
19
|
+
attr_reader :tags, :heading
|
|
20
|
+
|
|
21
|
+
def initialize(tags:, heading: "Tags", **attrs)
|
|
22
|
+
@tags = tags
|
|
23
|
+
@heading = heading
|
|
24
|
+
super(**attrs)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render?
|
|
28
|
+
tags.present? && tags.any?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% if should_render? %>
|
|
2
|
+
<% if @user.present? && time %>
|
|
3
|
+
<%= render Panda::Core::Admin::UserDisplayComponent.new(
|
|
4
|
+
user: @user,
|
|
5
|
+
metadata: "#{time_ago_in_words(time)} ago"
|
|
6
|
+
) %>
|
|
7
|
+
<% elsif @user.present? %>
|
|
8
|
+
<%= render Panda::Core::Admin::UserDisplayComponent.new(
|
|
9
|
+
user: @user,
|
|
10
|
+
metadata: "Not published"
|
|
11
|
+
) %>
|
|
12
|
+
<% elsif time %>
|
|
13
|
+
<div class="text-black/60"><%= "#{time_ago_in_words(time)} ago" %></div>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|