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.
Files changed (251) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +53 -0
  4. data/app/assets/builds/panda_core_admin.css +2 -0
  5. data/app/assets/tailwind/application.css +236 -36
  6. data/app/builders/panda/core/form_builder.rb +120 -18
  7. data/app/components/panda/core/UI/badge.html.erb +10 -0
  8. data/app/components/panda/core/UI/badge.rb +14 -30
  9. data/app/components/panda/core/UI/button.html.erb +5 -0
  10. data/app/components/panda/core/UI/button.rb +11 -15
  11. data/app/components/panda/core/UI/card.html.erb +3 -0
  12. data/app/components/panda/core/UI/card.rb +7 -6
  13. data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
  14. data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
  15. data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
  16. data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
  17. data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
  18. data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
  19. data/app/components/panda/core/admin/button_component.html.erb +9 -0
  20. data/app/components/panda/core/admin/button_component.rb +36 -31
  21. data/app/components/panda/core/admin/callout_component.html.erb +17 -0
  22. data/app/components/panda/core/admin/callout_component.rb +57 -0
  23. data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
  24. data/app/components/panda/core/admin/code_block_component.rb +23 -0
  25. data/app/components/panda/core/admin/container_component.html.erb +27 -0
  26. data/app/components/panda/core/admin/container_component.rb +11 -77
  27. data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
  28. data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
  29. data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
  30. data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
  31. data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
  32. data/app/components/panda/core/admin/delete_button_component.rb +41 -0
  33. data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
  34. data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
  35. data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
  36. data/app/components/panda/core/admin/dropdown_component.rb +65 -0
  37. data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
  38. data/app/components/panda/core/admin/empty_state_component.rb +25 -0
  39. data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
  40. data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
  41. data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
  42. data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
  43. data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
  44. data/app/components/panda/core/admin/flash_message_component.rb +33 -60
  45. data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
  46. data/app/components/panda/core/admin/form_error_component.rb +8 -16
  47. data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
  48. data/app/components/panda/core/admin/form_footer_component.rb +65 -0
  49. data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
  50. data/app/components/panda/core/admin/form_input_component.rb +21 -13
  51. data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
  52. data/app/components/panda/core/admin/form_section_component.rb +64 -0
  53. data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
  54. data/app/components/panda/core/admin/form_select_component.rb +25 -26
  55. data/app/components/panda/core/admin/heading_component.html.erb +26 -0
  56. data/app/components/panda/core/admin/heading_component.rb +23 -60
  57. data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
  58. data/app/components/panda/core/admin/login_form_component.rb +28 -0
  59. data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
  60. data/app/components/panda/core/admin/main_layout_component.rb +19 -0
  61. data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
  62. data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
  63. data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
  64. data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
  65. data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
  66. data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
  67. data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
  68. data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
  69. data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
  70. data/app/components/panda/core/admin/page_header_component.rb +19 -35
  71. data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
  72. data/app/components/panda/core/admin/pagination_component.rb +56 -0
  73. data/app/components/panda/core/admin/panel_component.html.erb +19 -0
  74. data/app/components/panda/core/admin/panel_component.rb +4 -29
  75. data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
  76. data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
  77. data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
  78. data/app/components/panda/core/admin/search_bar_component.rb +31 -0
  79. data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
  80. data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
  81. data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
  82. data/app/components/panda/core/admin/secret_field_component.rb +31 -0
  83. data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
  84. data/app/components/panda/core/admin/sidebar_component.rb +55 -0
  85. data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
  86. data/app/components/panda/core/admin/slideover_component.rb +31 -70
  87. data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
  88. data/app/components/panda/core/admin/statistics_component.rb +6 -8
  89. data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
  90. data/app/components/panda/core/admin/tab_bar_component.rb +32 -67
  91. data/app/components/panda/core/admin/table_component.html.erb +82 -0
  92. data/app/components/panda/core/admin/table_component.rb +54 -87
  93. data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
  94. data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
  95. data/app/components/panda/core/admin/tag_component.html.erb +1 -0
  96. data/app/components/panda/core/admin/tag_component.rb +51 -20
  97. data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
  98. data/app/components/panda/core/admin/tag_input_component.rb +22 -0
  99. data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
  100. data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
  101. data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
  102. data/app/components/panda/core/admin/user_activity_component.rb +9 -22
  103. data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
  104. data/app/components/panda/core/admin/user_display_component.rb +19 -22
  105. data/app/components/panda/core/base.rb +35 -42
  106. data/app/components/panda/core/shared/footer_component.html.erb +2 -0
  107. data/app/components/panda/core/shared/footer_component.rb +11 -0
  108. data/app/components/panda/core/shared/header_component.html.erb +32 -0
  109. data/app/components/panda/core/shared/header_component.rb +19 -0
  110. data/app/constraints/panda/core/admin_constraint.rb +2 -2
  111. data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
  112. data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
  113. data/app/controllers/panda/core/admin/base_controller.rb +62 -3
  114. data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
  115. data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
  116. data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
  117. data/app/controllers/panda/core/admin/files_controller.rb +230 -0
  118. data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
  119. data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
  120. data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
  121. data/app/controllers/panda/core/admin/search_controller.rb +15 -0
  122. data/app/controllers/panda/core/admin/sessions_controller.rb +84 -10
  123. data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
  124. data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
  125. data/app/controllers/panda/core/admin/users_controller.rb +184 -0
  126. data/app/controllers/panda/core/application_controller.rb +13 -1
  127. data/app/helpers/panda/core/active_storage_helper.rb +24 -0
  128. data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
  129. data/app/helpers/panda/core/form_helper.rb +15 -0
  130. data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
  131. data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
  132. data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
  133. data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
  134. data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
  135. data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
  136. data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
  137. data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
  138. data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
  139. data/app/javascript/panda/core/controllers/index.js +36 -0
  140. data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
  141. data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
  142. data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
  143. data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
  144. data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
  145. data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
  146. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
  147. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
  148. data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
  149. data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
  150. data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
  151. data/app/models/concerns/panda/core/has_metadata.rb +164 -0
  152. data/app/models/concerns/panda/core/importable.rb +79 -0
  153. data/app/models/concerns/panda/core/searchable.rb +66 -0
  154. data/app/models/concerns/panda/core/taggable.rb +37 -0
  155. data/app/models/panda/core/feature_flag.rb +56 -0
  156. data/app/models/panda/core/file_categorization.rb +14 -0
  157. data/app/models/panda/core/file_category.rb +45 -0
  158. data/app/models/panda/core/import_session.rb +79 -0
  159. data/app/models/panda/core/presence.rb +24 -0
  160. data/app/models/panda/core/tag.rb +33 -0
  161. data/app/models/panda/core/tagging.rb +14 -0
  162. data/app/models/panda/core/user.rb +94 -16
  163. data/app/models/panda/core/user_activity.rb +42 -0
  164. data/app/models/panda/core/user_session.rb +35 -0
  165. data/app/services/panda/core/attach_avatar_service.rb +15 -3
  166. data/app/services/panda/core/csv_import_service.rb +65 -0
  167. data/app/services/panda/core/file_categorizer.rb +52 -0
  168. data/app/services/panda/core/file_parser.rb +76 -0
  169. data/app/services/panda/core/invite_user_service.rb +44 -0
  170. data/app/services/panda/core/presence_service.rb +49 -0
  171. data/app/views/layouts/panda/core/admin.html.erb +6 -76
  172. data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
  173. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
  174. data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
  175. data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
  176. data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
  177. data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
  178. data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
  179. data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
  180. data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
  181. data/app/views/panda/core/admin/files/index.html.erb +61 -0
  182. data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
  183. data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
  184. data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
  185. data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
  186. data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
  187. data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
  188. data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
  189. data/app/views/panda/core/admin/sessions/new.html.erb +1 -37
  190. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
  191. data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
  192. data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
  193. data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
  194. data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
  195. data/app/views/panda/core/admin/tags/index.html.erb +41 -0
  196. data/app/views/panda/core/admin/tags/new.html.erb +4 -0
  197. data/app/views/panda/core/admin/users/activity.html.erb +90 -0
  198. data/app/views/panda/core/admin/users/edit.html.erb +48 -0
  199. data/app/views/panda/core/admin/users/index.html.erb +161 -0
  200. data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
  201. data/app/views/panda/core/admin/users/show.html.erb +224 -0
  202. data/app/views/panda/core/shared/_footer.html.erb +1 -2
  203. data/app/views/panda/core/shared/_header.html.erb +4 -22
  204. data/config/brakeman.ignore +68 -31
  205. data/config/importmap.rb +13 -11
  206. data/config/routes.rb +49 -2
  207. data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
  208. data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
  209. data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
  210. data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
  211. data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
  212. data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
  213. data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
  214. data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
  215. data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
  216. data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
  217. data/lib/generators/panda/core/install_generator.rb +32 -0
  218. data/lib/generators/panda/core/templates/panda.rb +63 -0
  219. data/lib/panda/core/asset_loader.rb +5 -2
  220. data/lib/panda/core/authentication.rb +3 -3
  221. data/lib/panda/core/configuration.rb +195 -1
  222. data/lib/panda/core/engine/admin_controller_config.rb +3 -6
  223. data/lib/panda/core/engine/autoload_config.rb +21 -9
  224. data/lib/panda/core/engine/omniauth_config.rb +108 -38
  225. data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
  226. data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
  227. data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
  228. data/lib/panda/core/engine.rb +90 -25
  229. data/lib/panda/core/middleware.rb +35 -0
  230. data/lib/panda/core/navigation_registry.rb +455 -0
  231. data/lib/panda/core/oauth_providers.rb +27 -0
  232. data/lib/panda/core/permission_registry.rb +89 -0
  233. data/lib/panda/core/search_registry.rb +69 -0
  234. data/lib/panda/core/seeds/file_categories.rb +30 -0
  235. data/lib/panda/core/shared/inflections_config.rb +1 -5
  236. data/lib/panda/core/testing/rails_helper.rb +87 -0
  237. data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
  238. data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
  239. data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
  240. data/lib/panda/core/version.rb +1 -1
  241. data/lib/panda/core/widget_registry.rb +50 -0
  242. data/lib/panda/core.rb +17 -8
  243. data/lib/tasks/panda/core/file_categories.rake +41 -0
  244. data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
  245. data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
  246. data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
  247. data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
  248. data/public/panda-core-assets/panda-core.css +2 -2
  249. data/public/panda-core-assets/vanilla-calendar.css +47 -0
  250. metadata +203 -26
  251. data/lib/panda/core/engine/inflections_config.rb +0 -23
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Sidebar navigation component for admin pages
7
+ # Displays hierarchical navigation with toggle for nested items.
8
+ # Supports optional section grouping via the :section key on nav items.
9
+ class SidebarComponent < Panda::Core::Base
10
+ def initialize(user:, **attrs)
11
+ @user = user
12
+ super(**attrs)
13
+ end
14
+
15
+ attr_reader :user
16
+
17
+ def admin_logo
18
+ Panda::Core.config.admin_logo
19
+ end
20
+
21
+ def admin_settings_path
22
+ Panda::Core.config.admin_settings_path
23
+ end
24
+
25
+ def compact_navigation?
26
+ Panda::Core.config.compact_navigation
27
+ end
28
+
29
+ def admin_logo_actions
30
+ Panda::Core.config.admin_logo_actions || []
31
+ end
32
+
33
+ def sectioned?(items)
34
+ items.any? { |item| item[:section].present? }
35
+ end
36
+
37
+ def safe_html(value)
38
+ raw = value.respond_to?(:call) ? value.call : value
39
+ helpers.sanitize(raw, tags: %w[svg path ellipse circle rect line polyline polygon g defs use
40
+ span a i div img br strong em b], attributes: %w[
41
+ viewBox fill stroke stroke-width stroke-linecap stroke-linejoin opacity cx cy rx ry r
42
+ x y x1 y1 x2 y2 width height d points transform xmlns class href style id
43
+ data-turbo-track rel
44
+ ])
45
+ end
46
+
47
+ private
48
+
49
+ def default_attrs
50
+ {class: "flex flex-col flex-1"}
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,67 @@
1
+ <%# Backdrop overlay - clicks close the slideover %>
2
+ <div class="hidden fixed inset-0 bg-gray-500/75 transition-opacity duration-500 ease-in-out dark:bg-gray-900/50 z-40"
3
+ data-toggle-target="toggleable"
4
+ data-action="click->toggle#toggle"
5
+ data-transition-enter="transition-opacity duration-500 ease-in-out"
6
+ data-transition-enter-from="opacity-0"
7
+ data-transition-enter-to="opacity-100"
8
+ data-transition-leave="transition-opacity duration-500 ease-in-out"
9
+ data-transition-leave-from="opacity-100"
10
+ data-transition-leave-to="opacity-0"></div>
11
+
12
+ <%# Slideover panel %>
13
+ <div id="slideover"
14
+ class="<%= slideover_classes %>"
15
+ data-toggle-target="toggleable"
16
+ data-transition-enter="transform transition ease-in-out duration-500 sm:duration-700"
17
+ data-transition-enter-from="translate-x-full"
18
+ data-transition-enter-to="translate-x-0"
19
+ data-transition-leave="transform transition ease-in-out duration-500 sm:duration-700"
20
+ data-transition-leave-from="translate-x-0"
21
+ data-transition-leave-to="translate-x-full">
22
+
23
+ <%# Main container %>
24
+ <div class="relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800">
25
+ <%# Header with title and close button %>
26
+ <div class="bg-gradient-admin px-4 py-6 sm:px-6">
27
+ <div class="flex items-center justify-between">
28
+ <h2 class="text-base font-semibold text-white" id="slideover-title">
29
+ <%= @title %>
30
+ </h2>
31
+ <div class="ml-3 flex items-center">
32
+ <%= content_tag :button,
33
+ type: "button",
34
+ data: {
35
+ action: "click->toggle#toggle touch->toggle#toggle"
36
+ },
37
+ class: "inline-flex items-center gap-1 rounded-full bg-white/10 px-2 py-1 text-xs text-white/80 hover:text-white cursor-pointer" do %>
38
+ <span>Close</span>
39
+ <i class="fa-solid fa-xmark"></i>
40
+ <% end %>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <%# Scrollable content area %>
46
+ <div class="flex-1 overflow-y-auto">
47
+ <% if @content_html %>
48
+ <%= raw(@content_html) %>
49
+ <% elsif @content_block %>
50
+ <%= @content_block.call %>
51
+ <% end %>
52
+ </div>
53
+
54
+ <%# Sticky footer (if footer content exists) %>
55
+ <% if footer_slot? || @footer_html || @footer_block %>
56
+ <div class="flex shrink-0 justify-end gap-x-3 border-t border-gray-200 px-4 py-4 dark:border-white/10">
57
+ <% if footer_slot? %>
58
+ <%= footer_slot %>
59
+ <% elsif @footer_html %>
60
+ <%= raw(@footer_html) %>
61
+ <% elsif @footer_block %>
62
+ <%= @footer_block.call %>
63
+ <% end %>
64
+ </div>
65
+ <% end %>
66
+ </div>
67
+ </div>
@@ -4,79 +4,25 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class SlideoverComponent < Panda::Core::Base
7
- prop :title, String, default: "Settings"
8
- prop :open, _Nilable(_Boolean), default: -> { false }
7
+ renders_one :footer_slot
9
8
 
10
- def view_template(&block)
11
- # Capture block content
12
- if block_given?
13
- if defined?(view_context) && view_context
14
- @content_html = view_context.capture(&block)
15
- else
16
- @content_block = block
17
- end
18
- end
19
-
20
- div(
21
- **default_attrs,
22
- data: {
23
- toggle_target: "toggleable",
24
- transition_enter: "transform transition ease-in-out duration-500 sm:duration-700",
25
- transition_enter_from: "translate-x-full",
26
- transition_enter_to: "translate-x-0",
27
- transition_leave: "transform transition ease-in-out duration-500 sm:duration-700",
28
- transition_leave_from: "translate-x-0",
29
- transition_leave_to: "translate-x-full"
30
- }
31
- ) do
32
- # Main container
33
- div(class: "relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800") do
34
- # Header with title and close button
35
- div(class: "bg-gradient-admin px-4 py-6 sm:px-6") do
36
- div(class: "flex items-center justify-between") do
37
- h2(class: "text-base font-semibold text-white", id: "slideover-title") do
38
- plain @title
39
- end
40
- div(class: "ml-3 flex h-7 items-center") do
41
- button(
42
- type: "button",
43
- data: {action: "click->toggle#toggle touch->toggle#toggle"},
44
- class: "relative rounded-md text-white/80 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
45
- ) do
46
- span(class: "absolute -inset-2.5")
47
- span(class: "sr-only") { "Close panel" }
48
- # SVG close icon
49
- svg(viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "1.5", aria_hidden: "true", class: "size-6") do
50
- path(d: "M6 18 18 6M6 6l12 12", stroke_linecap: "round", stroke_linejoin: "round")
51
- end
52
- end
53
- end
54
- end
55
- end
9
+ def initialize(title: "Settings", open: false, **attrs)
10
+ @title = title
11
+ @open = open
12
+ super(**attrs)
13
+ end
56
14
 
57
- # Scrollable content area
58
- div(class: "flex-1 overflow-y-auto") do
59
- if @content_html
60
- raw(@content_html)
61
- elsif @content_block
62
- instance_eval(@content_block)
63
- end
64
- end
15
+ attr_reader :title, :open
65
16
 
66
- # Sticky footer (if footer content exists)
67
- if @footer_html || @footer_block
68
- div(class: "flex shrink-0 justify-end gap-x-3 border-t border-gray-200 px-4 py-4 dark:border-white/10") do
69
- if @footer_html
70
- raw(@footer_html)
71
- elsif @footer_block
72
- instance_eval(&@footer_block)
73
- end
74
- end
75
- end
76
- end
17
+ def before_render
18
+ # Store main content if provided
19
+ # In ViewComponent, content is already rendered HTML (SafeBuffer), not a block
20
+ if content.present?
21
+ @content_html = content
77
22
  end
78
23
  end
79
24
 
25
+ # Legacy method for backwards compatibility
80
26
  def footer(&block)
81
27
  if defined?(view_context) && view_context
82
28
  @footer_html = view_context.capture(&block)
@@ -85,8 +31,6 @@ module Panda
85
31
  end
86
32
  end
87
33
 
88
- alias_method :with_footer, :footer
89
-
90
34
  private
91
35
 
92
36
  def default_attrs
@@ -97,10 +41,27 @@ module Panda
97
41
  end
98
42
 
99
43
  def slideover_classes
100
- base = "ml-auto block size-full max-w-md transform absolute right-0 h-full z-50"
44
+ # Use fixed positioning to escape parent overflow clipping
45
+ base = "ml-auto block size-full max-w-md transform fixed right-0 top-0 h-full z-50"
101
46
  visibility = @open ? "" : "hidden"
102
47
  [base, visibility].compact.join(" ")
103
48
  end
49
+
50
+ def close_icon
51
+ content_tag(:svg,
52
+ viewBox: "0 0 24 24",
53
+ fill: "none",
54
+ stroke: "currentColor",
55
+ stroke_width: "1.5",
56
+ aria: {hidden: "true"},
57
+ class: "size-6") do
58
+ tag.path(
59
+ d: "M6 18 18 6M6 6l12 12",
60
+ stroke_linecap: "round",
61
+ stroke_linejoin: "round"
62
+ )
63
+ end
64
+ end
104
65
  end
105
66
  end
106
67
  end
@@ -0,0 +1,4 @@
1
+ <div class="overflow-hidden p-4 bg-white rounded-2xl border border-gray-200">
2
+ <dt class="text-sm font-medium text-gray-500 truncate"><%= @metric %></dt>
3
+ <dd class="mt-1 text-2xl font-semibold tracking-tight text-gray-900"><%= @value %></dd>
4
+ </div>
@@ -4,15 +4,13 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class StatisticsComponent < Panda::Core::Base
7
- prop :metric, String
8
- prop :value, _Nilable(_Union(String, Integer, Float))
9
-
10
- def view_template
11
- div(class: "overflow-hidden p-4 bg-gradient-to-br rounded-lg border-2 from-primary-50/20 to-primary-50 border-primary-400") do
12
- dt(class: "text-base font-medium truncate text-primary-900") { @metric }
13
- dd(class: "mt-1 text-3xl font-medium tracking-tight text-primary-900") { @value }
14
- end
7
+ def initialize(metric: "", value: nil, **attrs)
8
+ @metric = metric
9
+ @value = value
10
+ super(**attrs)
15
11
  end
12
+
13
+ attr_reader :metric, :value
16
14
  end
17
15
  end
18
16
  end
@@ -0,0 +1,48 @@
1
+ <div class="mt-3">
2
+ <%# Mobile select dropdown %>
3
+ <div class="sm:hidden">
4
+ <%= tag.label "Select a tab", for: "tabs", class: "sr-only" %>
5
+ <%= tag.select id: "tabs",
6
+ name: "tabs",
7
+ onchange: "window.location.href=this.value",
8
+ class: "block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-primary-400 focus:border-primary-600 focus:ring-primary-600" do %>
9
+ <% @tabs.each_with_index do |tab, index| %>
10
+ <%= tag.option tab[:name], value: tab[:url] || "#", selected: tab_current?(tab, index) %>
11
+ <% end %>
12
+ <% end %>
13
+ </div>
14
+
15
+ <%# Desktop tab navigation %>
16
+ <div class="hidden sm:block">
17
+ <div class="flex items-center border-b border-gray-200">
18
+ <nav class="flex flex-1 -mb-px space-x-6 xl:space-x-8" aria-label="Tabs">
19
+ <% @tabs.each_with_index do |tab, index| %>
20
+ <%= content_tag :a,
21
+ tab[:name],
22
+ href: tab[:url] || "#",
23
+ class: tab_classes(tab, index),
24
+ aria: {current: tab_current?(tab, index) ? "page" : nil} %>
25
+ <% end %>
26
+ </nav>
27
+
28
+ <%# View toggle buttons %>
29
+ <div class="hidden items-center p-0.5 ml-6 bg-gray-100 rounded-lg sm:flex">
30
+ <%# List view button %>
31
+ <%= content_tag :button,
32
+ type: "button",
33
+ class: view_button_classes(false) do %>
34
+ <%= list_view_icon %>
35
+ <span class="sr-only">Use list view</span>
36
+ <% end %>
37
+
38
+ <%# Grid view button %>
39
+ <%= content_tag :button,
40
+ type: "button",
41
+ class: view_button_classes(true) do %>
42
+ <%= grid_view_icon %>
43
+ <span class="sr-only">Use grid view</span>
44
+ <% end %>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
@@ -4,95 +4,60 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class TabBarComponent < Panda::Core::Base
7
- prop :tabs, Array, default: -> { [].freeze }
8
-
9
- def view_template
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 render_mobile_select
19
- div(class: "sm:hidden") do
20
- label(for: "tabs", class: "sr-only") { "Select a tab" }
21
- select(
22
- id: "tabs",
23
- name: "tabs",
24
- class: "block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-primary-400 focus:border-primary-600 focus:ring-primary-600"
25
- ) do
26
- @tabs.each do |tab|
27
- option { tab[:name] }
28
- end
29
- end
30
- end
16
+ def any_tab_current?
17
+ @any_tab_current ||= @tabs.any? { |tab| tab[:current] }
31
18
  end
32
19
 
33
- def render_desktop_tabs
34
- div(class: "hidden sm:block") do
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 render_tab(tab, is_current = false)
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 is_current || tab[:current]
26
+ classes += if tab_current?(tab, index)
49
27
  "border-primary-600 text-primary-600"
50
28
  else
51
29
  "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
52
30
  end
53
-
54
- a(
55
- href: tab[:url] || "#",
56
- class: classes,
57
- aria: {current: (is_current || tab[:current]) ? "page" : nil}
58
- ) { tab[:name] }
31
+ classes
59
32
  end
60
33
 
61
- def render_view_toggle
62
- div(class: "hidden items-center p-0.5 ml-6 bg-gray-100 rounded-lg sm:flex") do
63
- render_view_button(:list)
64
- render_view_button(:grid, selected: true)
65
- end
66
- end
67
-
68
- def render_view_button(type, selected: false)
34
+ def view_button_classes(selected)
69
35
  button_class = "p-1.5 text-gray-400 rounded-md focus:ring-2 focus:ring-inset focus:outline-none focus:ring-primary-600"
70
36
  button_class += if selected
71
37
  " ml-0.5 bg-white shadow-sm"
72
38
  else
73
39
  " hover:bg-white hover:shadow-sm"
74
40
  end
41
+ button_class
42
+ end
43
+
44
+ def list_view_icon
45
+ content_tag(:svg, class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do
46
+ tag.path(
47
+ fill_rule: "evenodd",
48
+ d: "M2 3.75A.75.75 0 012.75 3h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.166a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z",
49
+ clip_rule: "evenodd"
50
+ )
51
+ end
52
+ end
75
53
 
76
- button(type: "button", class: button_class) do
77
- if type == :list
78
- svg(class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do |s|
79
- s.path(
80
- fill_rule: "evenodd",
81
- 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",
82
- clip_rule: "evenodd"
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 %>