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.
Files changed (255) 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 +413 -131
  6. data/app/builders/panda/core/form_builder.rb +129 -27
  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 +15 -19
  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 +37 -32
  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 +34 -69
  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 +85 -8
  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 +96 -18
  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 -67
  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 -38
  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 +6 -3
  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 +4 -3
  223. data/lib/panda/core/engine/autoload_config.rb +21 -9
  224. data/lib/panda/core/engine/omniauth_config.rb +114 -32
  225. data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
  226. data/lib/panda/core/engine/route_config.rb +1 -1
  227. data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
  228. data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
  229. data/lib/panda/core/engine.rb +90 -25
  230. data/lib/panda/core/middleware.rb +35 -0
  231. data/lib/panda/core/module_registry.rb +21 -13
  232. data/lib/panda/core/navigation_registry.rb +455 -0
  233. data/lib/panda/core/oauth_providers.rb +27 -0
  234. data/lib/panda/core/permission_registry.rb +89 -0
  235. data/lib/panda/core/search_registry.rb +69 -0
  236. data/lib/panda/core/seeds/file_categories.rb +30 -0
  237. data/lib/panda/core/shared/inflections_config.rb +1 -5
  238. data/lib/panda/core/testing/rails_helper.rb +87 -0
  239. data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
  240. data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
  241. data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
  242. data/lib/panda/core/version.rb +1 -1
  243. data/lib/panda/core/widget_registry.rb +50 -0
  244. data/lib/panda/core.rb +20 -0
  245. data/lib/tasks/panda/core/file_categories.rake +41 -0
  246. data/public/panda-core-assets/panda-core-0.12.2.css +2 -0
  247. data/public/panda-core-assets/panda-core-0.12.3.css +2 -0
  248. data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
  249. data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
  250. data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
  251. data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
  252. data/public/panda-core-assets/panda-core.css +2 -2
  253. data/public/panda-core-assets/vanilla-calendar.css +47 -0
  254. metadata +205 -26
  255. data/lib/panda/core/engine/inflections_config.rb +0 -23
@@ -4,33 +4,25 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class FormErrorComponent < Panda::Core::Base
7
- prop :errors, _Nilable(_Union(ActiveModel::Errors, Array)), default: -> {}
8
- prop :model, _Nilable(Object), default: -> {}
9
-
10
- def view_template
11
- return unless should_render?
12
-
13
- div(**@attrs) do
14
- div(class: "text-sm text-red-600") do
15
- error_messages.each do |message|
16
- p { message }
17
- end
18
- end
19
- end
7
+ def initialize(model:, **attrs)
8
+ @model = model
9
+ super(**attrs)
20
10
  end
21
11
 
12
+ attr_reader :model
13
+
22
14
  def default_attrs
23
15
  {
24
16
  class: "mb-4 p-4 bg-red-50 border border-red-200 rounded-md"
25
17
  }
26
18
  end
27
19
 
28
- private
29
-
30
- def should_render?
20
+ def render?
31
21
  error_messages.any?
32
22
  end
33
23
 
24
+ private
25
+
34
26
  def error_messages
35
27
  @error_messages ||= if @model&.respond_to?(:errors)
36
28
  @model.errors.full_messages
@@ -0,0 +1,16 @@
1
+ <div <%= tag.attributes(**attrs) %>>
2
+ <% if content? %>
3
+ <%= content %>
4
+ <% end %>
5
+
6
+ <% if cancel_path %>
7
+ <%= link_to "Cancel", cancel_path, class: cancel_link_classes %>
8
+ <% end %>
9
+
10
+ <%= tag.button type: "submit", class: submit_button_classes, data: submit_data_attrs do %>
11
+ <% if computed_icon %>
12
+ <i class="fa-solid <%= computed_icon %>"></i>
13
+ <% end %>
14
+ <%= submit_text %>
15
+ <% end %>
16
+ </div>
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Standardized form footer component with submit button(s) and optional secondary actions.
7
+ #
8
+ # @example Basic usage
9
+ # <%= render Panda::Core::Admin::FormFooterComponent.new(submit_text: "Save") %>
10
+ #
11
+ # @example With icon
12
+ # <%= render Panda::Core::Admin::FormFooterComponent.new(
13
+ # submit_text: "Create Page",
14
+ # icon: "fa-plus"
15
+ # ) %>
16
+ #
17
+ # @example With cancel link
18
+ # <%= render Panda::Core::Admin::FormFooterComponent.new(
19
+ # submit_text: "Update",
20
+ # cancel_path: admin_cms_pages_path
21
+ # ) %>
22
+ #
23
+ # @example With block for custom secondary actions
24
+ # <%= render Panda::Core::Admin::FormFooterComponent.new(submit_text: "Save") do %>
25
+ # <%= link_to "Preview", preview_path, class: "text-sm text-gray-600" %>
26
+ # <% end %>
27
+ #
28
+ class FormFooterComponent < Panda::Core::Base
29
+ def initialize(submit_text: "Save", icon: nil, cancel_path: nil, submit_action: nil, **attrs)
30
+ @submit_text = submit_text
31
+ @icon = icon
32
+ @cancel_path = cancel_path
33
+ @submit_action = submit_action
34
+ super(**attrs)
35
+ end
36
+
37
+ attr_reader :submit_text, :icon, :cancel_path, :submit_action
38
+
39
+ def default_attrs
40
+ {
41
+ class: "flex justify-end gap-x-3 mt-2"
42
+ }
43
+ end
44
+
45
+ def submit_button_classes
46
+ "inline-flex items-center gap-x-2 justify-center rounded-xl bg-primary-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
47
+ end
48
+
49
+ def cancel_link_classes
50
+ "inline-flex items-center justify-center rounded-xl px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-900"
51
+ end
52
+
53
+ def computed_icon
54
+ @icon
55
+ end
56
+
57
+ def submit_data_attrs
58
+ attrs = {disable_with: "Saving..."}
59
+ attrs[:action] = submit_action if submit_action
60
+ attrs
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1 @@
1
+ <%= tag.input(**attrs) %>
@@ -4,18 +4,19 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class FormInputComponent < Panda::Core::Base
7
- prop :name, String
8
- prop :value, _Nilable(String), default: -> {}
9
- prop :type, Symbol, default: :text
10
- prop :placeholder, _Nilable(String), default: -> {}
11
- prop :required, _Boolean, default: -> { false }
12
- prop :disabled, _Boolean, default: -> { false }
13
- prop :autocomplete, _Nilable(String), default: -> {}
14
-
15
- def view_template
16
- input(**@attrs)
7
+ def initialize(name: "", value: nil, placeholder: nil, autocomplete: nil, type: :text, required: false, disabled: false, **attrs)
8
+ @name = name
9
+ @value = value
10
+ @type = type
11
+ @placeholder = placeholder
12
+ @required = required
13
+ @disabled = disabled
14
+ @autocomplete = autocomplete
15
+ super(**attrs)
17
16
  end
18
17
 
18
+ attr_reader :name, :value, :type, :placeholder, :required, :disabled, :autocomplete
19
+
19
20
  def default_attrs
20
21
  base_attrs = {
21
22
  type: @type.to_s,
@@ -36,12 +37,19 @@ module Panda
36
37
  private
37
38
 
38
39
  def input_classes
39
- classes = "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset placeholder:text-gray-300 focus:ring-1 focus:ring-inset sm:leading-6"
40
+ # Border/radius/focus-ring are themeable via the --panda-input-*
41
+ # tokens (see app/assets/tailwind/application.css). The var()
42
+ # fallbacks resolve to today's rounded-xl/border-gray-300/
43
+ # outline-primary-500 exactly, so this is a no-op unless a host
44
+ # app sets data-theme.
45
+ classes = "block w-full h-11 rounded-[var(--panda-input-radius,var(--radius-xl))] border border-[var(--panda-input-border,var(--color-gray-300))] bg-white px-3 py-2 text-gray-900 " \
46
+ "placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-[var(--panda-input-focus-ring,var(--color-primary-500))] " \
47
+ "dark:bg-white/5 dark:text-white dark:border-white/10 dark:placeholder:text-gray-500"
40
48
 
41
49
  if @disabled
42
- classes + " ring-gray-300 focus:ring-gray-300 bg-gray-50 cursor-not-allowed"
50
+ classes + " border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed"
43
51
  else
44
- classes + " ring-mid focus:ring-dark hover:cursor-pointer"
52
+ classes
45
53
  end
46
54
  end
47
55
  end
@@ -0,0 +1,16 @@
1
+ <div <%= tag.attributes(**attrs) %>>
2
+ <h3 class="<%= heading_classes %>">
3
+ <% if icon %>
4
+ <i class="fa-solid <%= icon %>"></i>
5
+ <% end %>
6
+ <%= title %>
7
+ </h3>
8
+
9
+ <% if description %>
10
+ <p class="<%= description_classes %>"><%= description %></p>
11
+ <% end %>
12
+
13
+ <div class="<%= content_classes %>">
14
+ <%= content %>
15
+ </div>
16
+ </div>
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Standardized form section component for grouping related form fields.
7
+ # Provides a consistent heading style for form sections without the heavy
8
+ # visual weight of PanelComponent.
9
+ #
10
+ # @example Basic usage
11
+ # <%= render Panda::Core::Admin::FormSectionComponent.new(title: "SEO Settings") do %>
12
+ # <%= f.text_field :seo_title %>
13
+ # <%= f.text_area :seo_description %>
14
+ # <% end %>
15
+ #
16
+ # @example With description
17
+ # <%= render Panda::Core::Admin::FormSectionComponent.new(
18
+ # title: "Notification Settings",
19
+ # description: "Configure how you receive notifications"
20
+ # ) do %>
21
+ # ...
22
+ # <% end %>
23
+ #
24
+ # @example With icon
25
+ # <%= render Panda::Core::Admin::FormSectionComponent.new(
26
+ # title: "Menu Items",
27
+ # icon: "fa-bars"
28
+ # ) do %>
29
+ # ...
30
+ # <% end %>
31
+ #
32
+ class FormSectionComponent < Panda::Core::Base
33
+ def initialize(title:, description: nil, icon: nil, border_top: true, **attrs)
34
+ @title = title
35
+ @description = description
36
+ @icon = icon
37
+ @border_top = border_top
38
+ super(**attrs)
39
+ end
40
+
41
+ attr_reader :title, :description, :icon, :border_top
42
+
43
+ def default_attrs
44
+ base_classes = "mt-6 pt-4"
45
+ base_classes += " border-t border-gray-200" if border_top
46
+
47
+ {class: base_classes}
48
+ end
49
+
50
+ def heading_classes
51
+ "text-sm font-semibold text-gray-700 flex items-center gap-2"
52
+ end
53
+
54
+ def description_classes
55
+ "mt-1 text-xs text-gray-500"
56
+ end
57
+
58
+ def content_classes
59
+ "mt-4 space-y-4"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ <%= content_tag :select, @attrs do %>
2
+ <%= render_prompt if @prompt %>
3
+ <%= render_blank if @include_blank && !@prompt %>
4
+ <%= render_options %>
5
+ <% end %>
@@ -4,22 +4,18 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class FormSelectComponent < Panda::Core::Base
7
- prop :name, String
8
- prop :options, Array
9
- prop :selected, _Nilable(_Union(String, Integer)), default: -> {}
10
- prop :prompt, _Nilable(String), default: -> {}
11
- prop :required, _Boolean, default: -> { false }
12
- prop :disabled, _Boolean, default: -> { false }
13
- prop :include_blank, _Boolean, default: -> { false }
14
-
15
- def view_template
16
- select(**@attrs) do
17
- render_prompt if @prompt
18
- render_blank if @include_blank && !@prompt
19
- render_options
20
- end
7
+ def initialize(prompt:, name: "", options: [], required: false, disabled: false, include_blank: false, **attrs)
8
+ @name = name
9
+ @options = options
10
+ @prompt = prompt
11
+ @required = required
12
+ @disabled = disabled
13
+ @include_blank = include_blank
14
+ super(**attrs)
21
15
  end
22
16
 
17
+ attr_reader :name, :options, :prompt, :required, :disabled, :include_blank
18
+
23
19
  def default_attrs
24
20
  base_attrs = {
25
21
  name: @name,
@@ -36,31 +32,34 @@ module Panda
36
32
  private
37
33
 
38
34
  def select_classes
39
- classes = "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset focus:ring-1 focus:ring-inset sm:leading-6"
35
+ classes = "block w-full h-11 rounded-xl border border-gray-300 bg-white px-3 py-2 text-gray-900 " \
36
+ "focus:outline-2 focus:-outline-offset-2 focus:outline-primary-500 " \
37
+ "dark:bg-white/5 dark:text-white dark:border-white/10"
40
38
 
41
39
  if @disabled
42
- classes + " ring-gray-300 focus:ring-gray-300 bg-gray-50 cursor-not-allowed"
40
+ classes + " border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed"
43
41
  else
44
- classes + " ring-mid focus:ring-dark hover:cursor-pointer"
42
+ classes
45
43
  end
46
44
  end
47
45
 
48
46
  def render_prompt
49
- option(value: "", disabled: true, selected: @selected.nil?) { @prompt }
47
+ content_tag(:option, @prompt, value: "", disabled: true, selected: @selected.nil?)
50
48
  end
51
49
 
52
50
  def render_blank
53
- option(value: "") { "" }
51
+ content_tag(:option, "", value: "")
54
52
  end
55
53
 
56
54
  def render_options
57
- @options.each do |option_data|
58
- label, value = option_data
59
- option_attrs = {value: value.to_s}
60
- option_attrs[:selected] = true if value.to_s == @selected.to_s
61
-
62
- option(**option_attrs) { label.to_s }
63
- end
55
+ safe_join(
56
+ @options.map do |option_data|
57
+ label, value = option_data
58
+ option_attrs = {value: value.to_s}
59
+ option_attrs[:selected] = true if value.to_s == @selected.to_s
60
+ content_tag(:option, label.to_s, **option_attrs)
61
+ end
62
+ )
64
63
  end
65
64
  end
66
65
  end
@@ -0,0 +1,26 @@
1
+ <div class="heading-wrapper">
2
+ <% heading_tag = case level when 1 then :h1 when 2 then :h2 when 3 then :h3 when :panel then :h3 else :h2 end %>
3
+
4
+ <%= tag.send(heading_tag, class: heading_classes) do %>
5
+ <div class="grow flex items-center gap-x-2">
6
+ <% if icon.present? %>
7
+ <%= tag.i(class: icon) %>
8
+ <% end %>
9
+ <span><%= text %></span>
10
+ </div>
11
+ <% if buttons.any? || dropdown_buttons.any? %>
12
+ <span class="actions flex gap-x-2 mt-1 min-h-[2.5rem]">
13
+ <% dropdown_buttons.each do |dropdown_button| %>
14
+ <%= dropdown_button %>
15
+ <% end %>
16
+ <% buttons.each do |button| %>
17
+ <%= button %>
18
+ <% end %>
19
+ </span>
20
+ <% end %>
21
+ <% end %>
22
+
23
+ <% if meta %>
24
+ <%= tag.p(raw(meta), class: "text-sm text-black/60 -mt-1 mb-5") %>
25
+ <% end %>
26
+ </div>
@@ -4,78 +4,41 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class HeadingComponent < Panda::Core::Base
7
- prop :text, String
8
- prop :level, _Nilable(_Union(Integer, Symbol)), default: -> { 2 }
9
- prop :icon, String, default: ""
10
- prop :meta, _Nilable(String), default: -> {}
11
- prop :additional_styles, _Nilable(_Union(String, Array)), default: -> { "" }
12
-
13
- def view_template(&block)
14
- # Capture any buttons defined via block
15
- instance_eval(&block) if block_given?
16
-
17
- div(class: "heading-wrapper") do
18
- case @level
19
- when 1
20
- h1(class: heading_classes(@meta.present?)) { render_content }
21
- when 2
22
- h2(class: heading_classes(@meta.present?)) { render_content }
23
- when 3
24
- h3(class: heading_classes(@meta.present?)) { render_content }
25
- when :panel
26
- h3(class: panel_heading_classes) { @text }
27
- else
28
- h2(class: heading_classes(@meta.present?)) { render_content }
29
- end
30
-
31
- if @meta
32
- p(class: "text-sm text-black/60 -mt-1 mb-5") { raw(@meta) }
33
- end
34
- end
7
+ renders_many :buttons, Panda::Core::Admin::ButtonComponent
8
+ renders_many :dropdown_buttons, Panda::Core::Admin::DropdownButtonComponent
9
+
10
+ def initialize(text: "", icon: "", meta: nil, level: 2, additional_styles: nil, **attrs, &block)
11
+ @text = text
12
+ @icon = icon
13
+ @meta = meta
14
+ @level = level
15
+ @additional_styles = additional_styles
16
+ super(**attrs)
17
+ # Execute block if passed to new() directly (for tests/direct instantiation)
18
+ # This allows: HeadingComponent.new(text: "Pages") { |h| h.with_button(...) }
19
+ # Note: When used via render() with a block, ViewComponent handles the block separately
20
+ yield self if block_given?
35
21
  end
36
22
 
37
- def button(**props)
38
- @buttons ||= []
39
- @buttons << Panda::Core::Admin::ButtonComponent.new(**props)
40
- end
23
+ attr_reader :text, :icon, :meta, :level
41
24
 
42
25
  private
43
26
 
44
- def render_content
45
- div(class: "grow flex items-center gap-x-2") do
46
- i(class: @icon) if @icon.present?
47
- span { @text }
48
- end
49
-
50
- span(class: "actions flex gap-x-2 mt-1 min-h-[2.5rem]") do
51
- @buttons&.each { |btn| render(btn) }
52
- end
53
- end
54
-
55
- def heading_classes(has_meta = false)
56
- margin_bottom = has_meta ? "mb-0.5" : "mb-5"
57
- base = "flex text-black #{margin_bottom} -mt-2"
27
+ def heading_classes
28
+ margin_bottom = @meta.present? ? "mb-0.5" : "mb-5"
29
+ base = "flex items-center gap-2 text-gray-900 #{margin_bottom}"
58
30
  styles = case @level
59
31
  when 1
60
- "text-2xl font-medium"
32
+ "text-2xl font-semibold"
61
33
  when 2
62
- "text-xl font-medium"
34
+ "text-xl font-semibold"
63
35
  when 3
64
- "text-xl font-light"
36
+ "text-lg font-medium"
65
37
  else
66
- "text-xl font-medium"
38
+ "text-xl font-semibold"
67
39
  end
68
40
 
69
- [base, styles, *additional_styles_array].compact.join(" ")
70
- end
71
-
72
- def panel_heading_classes
73
- "text-base font-medium px-4 py-3 text-white"
74
- end
75
-
76
- def additional_styles_array
77
- return [] if @additional_styles.blank?
78
- @additional_styles.is_a?(String) ? @additional_styles.split(" ") : @additional_styles
41
+ [base, styles, @additional_styles].compact.join(" ")
79
42
  end
80
43
  end
81
44
  end
@@ -0,0 +1,38 @@
1
+ <div <%= tag.attributes(**attrs) %>>
2
+ <div class="text-center sm:mx-auto sm:w-full sm:max-w-sm">
3
+ <% if Panda::Core.config.login_logo_path %>
4
+ <img src="<%= Panda::Core.config.login_logo_path %>" class="py-2 mx-auto w-auto h-32">
5
+ <% end %>
6
+ <h2 class="mt-10 mb-6 text-2xl font-bold text-center text-white">
7
+ <%= Panda::Core.config.login_page_title || "Sign in to your account" %>
8
+ </h2>
9
+ </div>
10
+
11
+ <% if @providers&.any? %>
12
+ <% @providers.each do |provider| %>
13
+ <% provider_config = Panda::Core.config.authentication_providers[provider] %>
14
+ <% provider_path = provider_config&.dig(:path_name) || provider %>
15
+ <div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
16
+ <%= helpers.form_tag "#{Panda::Core.config.admin_path}/auth/#{provider_path}", method: "post", data: {turbo: false} do %>
17
+ <button type="submit" id="button-sign-in-<%= provider_path %>" class="inline-flex gap-x-2 items-center py-2.5 px-3.5 mx-auto mb-4 bg-white text-gray-900 rounded-md border min-w-56 border-neutral-400 hover:bg-gray-50">
18
+ <i class="<%= oauth_provider_non_brand?(provider) ? 'fa-solid' : 'fa-brands' %> fa-<%= oauth_provider_icon(provider) %> text-xl mr-1"></i>
19
+ Sign in with <%= oauth_provider_name(provider, provider_config) %>
20
+ </button>
21
+ <% end %>
22
+ </div>
23
+ <% end %>
24
+ <% else %>
25
+ <div class="rounded-md bg-yellow-50 p-4 sm:mx-auto sm:w-full sm:max-w-sm">
26
+ <div class="flex">
27
+ <div class="ml-3">
28
+ <h3 class="text-sm font-medium text-yellow-800">
29
+ No authentication providers configured
30
+ </h3>
31
+ <div class="mt-2 text-sm text-yellow-700">
32
+ <p>Please configure at least one authentication provider in your Panda Core configuration.</p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ <% end %>
38
+ </div>
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Login form component for OAuth provider authentication
7
+ # Displays configured authentication providers for user login
8
+ class LoginFormComponent < Panda::Core::Base
9
+ include Panda::Core::SessionsHelper
10
+
11
+ def initialize(providers: [], **attrs)
12
+ @providers = providers
13
+ super(**attrs)
14
+ end
15
+
16
+ attr_reader :providers
17
+
18
+ private
19
+
20
+ def default_attrs
21
+ {
22
+ class: "flex flex-col justify-center py-12 px-6 min-h-full text-center lg:px-8"
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,88 @@
1
+ <%= render Panda::Core::Shared::HeaderComponent.new(html_class: "h-full", body_class: "") %>
2
+
3
+ <div class="flex h-full" id="panda-container">
4
+ <div data-controller="mobile-sidebar" data-action="keydown.esc@window->mobile-sidebar#close">
5
+ <div data-mobile-sidebar-target="backdrop" class="hidden fixed inset-0 bg-gray-900/50 z-40 lg:hidden" data-action="click->mobile-sidebar#close"></div>
6
+ <div class="absolute top-0 w-full lg:flex lg:fixed lg:inset-y-0 z-50 lg:flex-col lg:w-72">
7
+ <div data-mobile-sidebar-target="sidebar" class="flex overflow-y-auto flex-col gap-y-5 px-4 pb-4 max-h-16 bg-gradient-admin lg:max-h-full grow">
8
+ <div class="flex items-center justify-between pt-4 -mb-5 lg:hidden">
9
+ <button data-mobile-sidebar-target="hamburger" data-action="click->mobile-sidebar#toggle" type="button" class="text-white/80 hover:text-white p-1" aria-expanded="false" aria-label="Toggle navigation">
10
+ <i class="fa-solid fa-bars text-xl"></i>
11
+ </button>
12
+ <span class="text-white font-medium text-lg"><%= Panda::Core.config.admin_title || "Panda Admin" %></span>
13
+ <span class="w-7"></span>
14
+ </div>
15
+ <%= render Panda::Core::Admin::SidebarComponent.new(user: @user) %>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ <div class="flex flex-col flex-1 mt-16 ml-0 lg:mt-0 lg:ml-72" id="panda-inner-container" <% if content_for :sidebar %> data-controller="toggle" data-action="keydown.esc@window->toggle#hide keydown.escape@window->toggle#hide" tabindex="-1"<% end %>>
20
+ <section id="panda-main" class="flex flex-row h-full">
21
+ <div class="flex-1 h-full overflow-y-auto pb-8" id="panda-primary-content">
22
+ <%= render Panda::Core::Admin::BreadcrumbsComponent.new(breadcrumbs: @breadcrumbs) %>
23
+ <%= render "panda/core/admin/shared/flash" %>
24
+ <%= content %>
25
+ </div>
26
+ <% if content_for :sidebar %>
27
+ <!-- Backdrop overlay -->
28
+ <div data-toggle-target="toggleable" class="hidden fixed inset-0 bg-gray-500/75 transition-opacity duration-500 ease-in-out dark:bg-gray-900/50 z-40"
29
+ data-transition-enter="transition-opacity duration-500 ease-in-out"
30
+ data-transition-enter-from="opacity-0"
31
+ data-transition-enter-to="opacity-100"
32
+ data-transition-leave="transition-opacity duration-500 ease-in-out"
33
+ data-transition-leave-from="opacity-100"
34
+ data-transition-leave-to="opacity-0"></div>
35
+
36
+ <!-- Slideover panel -->
37
+ <div data-toggle-target="toggleable" class="hidden ml-auto block size-full max-w-md transform fixed right-0 top-0 h-full z-50"
38
+ data-transition-enter="transform transition ease-in-out duration-500 sm:duration-700"
39
+ data-transition-enter-from="translate-x-full"
40
+ data-transition-enter-to="translate-x-0"
41
+ data-transition-leave="transform transition ease-in-out duration-500 sm:duration-700"
42
+ data-transition-leave-from="translate-x-0"
43
+ data-transition-leave-to="translate-x-full"
44
+ id="slideover">
45
+ <div class="relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800 dark:after:absolute dark:after:inset-y-0 dark:after:left-0 dark:after:w-px dark:after:bg-white/10">
46
+ <!-- Header -->
47
+ <div class="bg-gradient-admin px-4 py-3 sm:px-6">
48
+ <div class="flex items-center justify-between">
49
+ <h2 class="text-base font-semibold text-white" id="slideover-title"><%= content_for?(:sidebar_title) ? yield(:sidebar_title) : "Sidebar" %></h2>
50
+ <div class="ml-3 flex items-center">
51
+ <button type="button" data-action="click->toggle#toggle touch->toggle#toggle" class="flex items-center gap-x-1 rounded-full px-2 py-1 text-xs text-white/80 bg-white/10 hover:text-white hover:bg-white/20 transition-colors">
52
+ <span>Close</span>
53
+ <i class="fa-solid fa-xmark"></i>
54
+ </button>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- Content -->
60
+ <% if content_for?(:sidebar) %>
61
+ <div class="flex-1 overflow-y-auto">
62
+ <%= yield :sidebar %>
63
+ </div>
64
+ <% end %>
65
+
66
+ <!-- Footer (if present) -->
67
+ <% if content_for?(:sidebar_footer) %>
68
+ <div class="flex shrink-0 justify-end gap-x-3 border-t border-gray-200 px-4 py-4 dark:border-white/10">
69
+ <%= yield :sidebar_footer %>
70
+ </div>
71
+ <% end %>
72
+ </div>
73
+ </div>
74
+ <script>
75
+ window.addEventListener('keydown', function(e) {
76
+ if (e.key === 'Escape') {
77
+ document.querySelectorAll('[data-toggle-target="toggleable"]').forEach(function(el) {
78
+ el.classList.add('hidden');
79
+ });
80
+ }
81
+ });
82
+ </script>
83
+ <% end %>
84
+ </section>
85
+ </div>
86
+ </div>
87
+
88
+ <%= render Panda::Core::Shared::FooterComponent.new %>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Main admin layout component
7
+ # Handles the full admin page layout with sidebar, header, and main content
8
+ class MainLayoutComponent < ViewComponent::Base
9
+ def initialize(user:, breadcrumbs: [], **attrs)
10
+ super()
11
+ @user = user
12
+ @breadcrumbs = breadcrumbs
13
+ end
14
+
15
+ attr_reader :user, :breadcrumbs
16
+ end
17
+ end
18
+ end
19
+ end