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