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,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-2 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-primary-400 focus:ring-primary-600 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
@@ -0,0 +1,41 @@
1
+ <div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
2
+ <div class="flex items-center gap-4">
3
+ <%# Provider icon %>
4
+ <div class="flex items-center justify-center w-12 h-12 rounded-lg bg-gray-100">
5
+ <i class="<%= provider_info[:icon] %> text-2xl" style="color: <%= provider_info[:color] %>;"></i>
6
+ </div>
7
+
8
+ <%# Provider details %>
9
+ <div>
10
+ <h3 class="font-medium text-gray-900"><%= provider_info[:name] %></h3>
11
+ <% if connected? %>
12
+ <p class="text-sm text-green-600">
13
+ <i class="fa-solid fa-check-circle"></i>
14
+ Connected
15
+ </p>
16
+ <% else %>
17
+ <p class="text-sm text-gray-500">Not connected</p>
18
+ <% end %>
19
+ </div>
20
+ </div>
21
+
22
+ <%# Action button %>
23
+ <div>
24
+ <% if connected? %>
25
+ <%= render Panda::Core::Admin::ButtonComponent.new(
26
+ text: "Connected",
27
+ action: :secondary,
28
+ as_button: true,
29
+ disabled: true
30
+ ) %>
31
+ <% else %>
32
+ <%= render Panda::Core::Admin::ButtonComponent.new(
33
+ text: "Connect",
34
+ action: :primary,
35
+ href: "#",
36
+ disabled: true,
37
+ title: "OAuth re-connection coming soon"
38
+ ) %>
39
+ <% end %>
40
+ </div>
41
+ </div>
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ module MyProfile
7
+ class ConnectedAccountComponent < Panda::Core::Base
8
+ def initialize(provider:, user:)
9
+ @provider = provider
10
+ @user = user
11
+ @provider_info = Panda::Core::OAuthProviders.info(provider)
12
+ # TODO: Track which provider user logged in with
13
+ # For now, we don't track the provider, so we can't show connection status
14
+ @is_connected = user.respond_to?(:oauth_provider) && user.oauth_provider == provider.to_s
15
+ super()
16
+ end
17
+
18
+ attr_reader :provider_info
19
+
20
+ def connected?
21
+ @is_connected
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ <li>
2
+ <% if expandable? %>
3
+ <div class="<%= margin_class %>" data-controller="navigation-toggle">
4
+ <div data-navigation-toggle-target="wrapper"
5
+ class="<%= 'rounded-xl bg-white/10 overflow-hidden' if active? %>">
6
+ <button type="button"
7
+ data-navigation-toggle-target="button"
8
+ data-action="click->navigation-toggle#toggle"
9
+ aria-controls="<%= menu_id %>"
10
+ aria-expanded="false"
11
+ class="<%= parent_classes %>">
12
+ <span class="text-center w-6"><i class="<%= icon %> text-xl fa-fw"></i></span>
13
+ <span class="flex-1 text-left"><%= label %></span>
14
+ <%= badge_tag %>
15
+ <i class="fa-solid fa-chevron-right text-xs transition-transform duration-150 ease-in-out"
16
+ data-navigation-toggle-target="icon"></i>
17
+ </button>
18
+ <div id="<%= menu_id %>"
19
+ class="overflow-hidden transition-all duration-200 ease-out <%= active? ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0' %>"
20
+ data-navigation-toggle-target="menu">
21
+ <% sub_items.each do |sub_item| %>
22
+ <%= sub_item %>
23
+ <% end %>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <% else %>
28
+ <%= helpers.link_to path, class: link_classes, target: target do %>
29
+ <span class="text-center w-6"><i class="<%= icon %> text-xl fa-fw"></i></span>
30
+ <span class="flex-1"><%= label %></span>
31
+ <%= badge_tag %>
32
+ <% end %>
33
+ <% end %>
34
+ </li>
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ module Navigation
7
+ # A top-level navigation item
8
+ # Can be a simple link or an expandable menu with children
9
+ class ItemComponent < Panda::Core::Base
10
+ BASE_CLASSES = "transition-all group flex items-center gap-x-3 px-3 " \
11
+ "text-sm font-medium cursor-pointer"
12
+ SPACIOUS_PADDING = "py-2.5"
13
+ COMPACT_PADDING = "py-1.5"
14
+ ACTIVE_CLASSES = "bg-primary-500/20 text-white rounded-xl"
15
+ EXPANDED_BUTTON_CLASSES = "bg-white/15 text-white rounded-t-xl"
16
+ INACTIVE_CLASSES = "text-white/80 hover:bg-white/5 rounded-xl"
17
+
18
+ renders_many :sub_items, SubItemComponent
19
+
20
+ def initialize(label:, icon:, path: nil, active: false, menu_id: nil, target: nil, badge: nil, badge_color: nil, compact: false, **attrs) # rubocop:disable Metrics/ParameterLists
21
+ @label = label
22
+ @icon = icon
23
+ @path = path
24
+ @active = active
25
+ @menu_id = menu_id
26
+ @target = target
27
+ @badge = badge
28
+ @badge_color = badge_color
29
+ @compact = compact
30
+ super(**attrs)
31
+ end
32
+
33
+ attr_reader :label, :icon, :path, :active, :menu_id, :target, :badge, :badge_color
34
+
35
+ def active?
36
+ @active
37
+ end
38
+
39
+ def expandable?
40
+ sub_items.any?
41
+ end
42
+
43
+ def padding_class
44
+ @compact ? COMPACT_PADDING : SPACIOUS_PADDING
45
+ end
46
+
47
+ def margin_class
48
+ @compact ? "mb-0.5" : "mb-2"
49
+ end
50
+
51
+ def parent_classes
52
+ "#{BASE_CLASSES} #{padding_class} w-full #{active? ? EXPANDED_BUTTON_CLASSES : INACTIVE_CLASSES}"
53
+ end
54
+
55
+ def link_classes
56
+ "#{BASE_CLASSES} #{padding_class} rounded-xl #{margin_class} #{active? ? "#{ACTIVE_CLASSES} relative" : INACTIVE_CLASSES}"
57
+ end
58
+
59
+ def badge_tag
60
+ return unless badge
61
+ helpers.content_tag(:span, badge,
62
+ class: "ml-auto px-1.5 py-0.5 text-[10px] font-semibold rounded-full text-white",
63
+ style: "background-color: #{badge_color || "#52B788"}")
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,9 @@
1
+ <% if button? %>
2
+ <%= helpers.button_to path, method: method, **button_options, class: "#{item_classes} text-left" do %>
3
+ <%= label %>
4
+ <% end %>
5
+ <% else %>
6
+ <%= helpers.link_to path, class: item_classes, target: target do %>
7
+ <%= label %>
8
+ <% end %>
9
+ <% end %>
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ module Navigation
7
+ # A child item within an expandable navigation menu
8
+ # Can render as a link or a button (for logout, etc.)
9
+ class SubItemComponent < Panda::Core::Base
10
+ BASE_CLASSES = "group flex items-center w-full px-3 py-2 cursor-pointer " \
11
+ "text-sm font-medium transition-colors"
12
+ ACTIVE_CLASSES = "bg-primary-500/20 text-white"
13
+ INACTIVE_CLASSES = "text-white/70 hover:bg-white/5"
14
+
15
+ # rubocop:disable Metrics/ParameterLists
16
+ def initialize(label:, path: nil, active: false, method: nil, button_options: {}, target: nil, **attrs)
17
+ @label = label
18
+ @path = path
19
+ @active = active
20
+ @method = method
21
+ @button_options = button_options
22
+ @target = target
23
+ super(**attrs)
24
+ end
25
+ # rubocop:enable Metrics/ParameterLists
26
+
27
+ attr_reader :label, :path, :active, :method, :button_options, :target
28
+
29
+ def active?
30
+ @active
31
+ end
32
+
33
+ def button?
34
+ method.present?
35
+ end
36
+
37
+ def item_classes
38
+ "#{BASE_CLASSES} #{active? ? ACTIVE_CLASSES : INACTIVE_CLASSES}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ <li class="mt-auto">
2
+ <div class="mb-2" data-controller="navigation-toggle">
3
+ <div data-navigation-toggle-target="wrapper"
4
+ class="<%= 'rounded-xl bg-white/10 overflow-hidden' if active? %>">
5
+ <button type="button"
6
+ data-navigation-toggle-target="button"
7
+ data-action="click->navigation-toggle#toggle"
8
+ aria-controls="user-menu"
9
+ aria-expanded="false"
10
+ class="<%= button_classes %>">
11
+ <% avatar = user.avatar_url %>
12
+ <% if avatar.present? %>
13
+ <span class="text-center w-6"><img src="<%= avatar %>" alt="<%= user.name %>" class="w-auto h-7 rounded-full object-cover"></span>
14
+ <% else %>
15
+ <span class="text-center w-6"><i class="text-xl fa-solid fa-circle-user fa-fw"></i></span>
16
+ <% end %>
17
+ <span class="flex-1 text-left"><%= user.name %></span>
18
+ <i class="fa-solid fa-chevron-right text-xs transition-transform duration-150 ease-in-out"
19
+ data-navigation-toggle-target="icon"></i>
20
+ </button>
21
+ <div id="user-menu"
22
+ class="overflow-hidden transition-all duration-200 ease-out <%= active? ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0' %>"
23
+ data-navigation-toggle-target="menu">
24
+ <% sub_items.each do |sub_item| %>
25
+ <%= sub_item %>
26
+ <% end %>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </li>