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
@@ -0,0 +1,16 @@
1
+ <nav aria-label="Breadcrumb" id="panda-breadcrumbs" <% if @attrs.any? %><%= tag.attributes(@attrs) %><% end %>>
2
+ <ol role="list">
3
+ <li class="inline-block">
4
+ <a href="<%= Panda::Core.config.admin_path %>" class="text-black/60 hover:text-black/80">
5
+ <i class="fa fa-solid fa-house fa-fw inline-block w-4 text-center"></i>
6
+ <span class="sr-only">Home</span>
7
+ </a>
8
+ </li>
9
+ <% @breadcrumbs.each do |crumb| %>
10
+ <li class="inline-block">
11
+ <i class="fa-solid fa-chevron-right fa-fw inline-block w-4 text-center font-light text-black/60 hover:text-black/80 px-2"></i>
12
+ <a href="<%= crumb.path %>" class="text-sm font-normal text-black/60 hover:text-black/80"><%= crumb.name %></a>
13
+ </li>
14
+ <% end %>
15
+ </ol>
16
+ </nav>
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Breadcrumbs navigation component for admin pages
7
+ # Displays hierarchical navigation starting from admin home
8
+ class BreadcrumbsComponent < Panda::Core::Base
9
+ def initialize(breadcrumbs: [], **attrs)
10
+ @breadcrumbs = breadcrumbs
11
+ super(**attrs)
12
+ end
13
+
14
+ attr_reader :breadcrumbs
15
+
16
+ private
17
+
18
+ def default_attrs
19
+ {
20
+ class: "px-4 w-full sm:px-6 py-2.5"
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ <% if @as_button %>
2
+ <%= content_tag :button, @attrs do %>
3
+ <%= render_content %>
4
+ <% end %>
5
+ <% else %>
6
+ <%= content_tag :a, @attrs do %>
7
+ <%= render_content %>
8
+ <% end %>
9
+ <% end %>
@@ -4,26 +4,19 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class ButtonComponent < Panda::Core::Base
7
- prop :text, String, default: "Button"
8
- prop :action, _Nilable(Symbol), default: -> {}
9
- prop :href, _Nilable(String), default: -> { "#" }
10
- prop :icon, _Nilable(String), default: -> {}
11
- prop :size, Symbol, default: :regular
12
- prop :id, _Nilable(String), default: -> {}
13
- prop :as_button, _Boolean, default: -> { false }
14
-
15
- def view_template
16
- if @as_button
17
- button(**@attrs) do
18
- render_content
19
- end
20
- else
21
- a(**@attrs) do
22
- render_content
23
- end
24
- end
7
+ def initialize(text: "Button", action: nil, href: "#", icon: nil, size: :regular, id: nil, as_button: false, **attrs)
8
+ @text = text
9
+ @action = action
10
+ @href = href
11
+ @icon = icon
12
+ @size = size
13
+ @id = id
14
+ @as_button = as_button
15
+ super(**attrs)
25
16
  end
26
17
 
18
+ attr_reader :text, :action, :href, :icon, :size, :id, :as_button
19
+
27
20
  def default_attrs
28
21
  base = {
29
22
  class: button_classes,
@@ -40,10 +33,22 @@ module Panda
40
33
  private
41
34
 
42
35
  def render_content
43
- if computed_icon
44
- i(class: "fa-solid fa-#{computed_icon}")
36
+ icon_html = if computed_icon
37
+ content_tag(:i, "", class: "fa-solid fa-#{computed_icon}")
38
+ else
39
+ "".html_safe
45
40
  end
46
- plain @text.titleize
41
+ text_html = @text.titleize
42
+
43
+ if icon_right?
44
+ (text_html + icon_html).html_safe
45
+ else
46
+ (icon_html + text_html).html_safe
47
+ end
48
+ end
49
+
50
+ def icon_right?
51
+ @action.in?(%i[save create add new])
47
52
  end
48
53
 
49
54
  def computed_icon
@@ -51,35 +56,35 @@ module Panda
51
56
  end
52
57
 
53
58
  def button_classes
54
- base = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 cursor-pointer "
59
+ 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 "
55
60
  base + size_classes + action_classes
56
61
  end
57
62
 
58
63
  def size_classes
59
64
  case @size
60
65
  when :small, :sm
61
- "gap-x-1.5 px-2.5 py-1.5 text-sm "
66
+ "gap-x-1.5 px-3 py-1.5 text-xs "
62
67
  when :medium, :regular, :md
63
- "gap-x-1.5 px-3 py-2 text-base "
68
+ "gap-x-2 px-4 py-2 text-sm "
64
69
  when :large, :lg
65
- "gap-x-2 px-3.5 py-2.5 text-lg "
70
+ "gap-x-2 px-5 py-2.5 text-base "
66
71
  else
67
- "gap-x-1.5 px-3 py-2 text-base "
72
+ "gap-x-2 px-4 py-2 text-sm "
68
73
  end
69
74
  end
70
75
 
71
76
  def action_classes
72
77
  case @action
73
- when :save, :create
74
- "text-white bg-mid hover:bg-mid/80"
78
+ when :save, :create, :add, :new
79
+ "text-white bg-primary-500 hover:bg-primary-600 focus-visible:outline-primary-600"
75
80
  when :save_inactive
76
- "text-white bg-gray-400"
81
+ "text-white bg-gray-300 cursor-not-allowed"
77
82
  when :secondary
78
- "text-gray-700 border-2 border-gray-500 bg-white hover:bg-gray-100 active:bg-gray-200 transition-colors"
83
+ "text-gray-700 border border-gray-200 bg-white hover:bg-gray-50"
79
84
  when :delete, :destroy, :danger
80
- "text-red-600 border border-red-600 bg-red-100 hover:bg-red-200 hover:text-red-700 focus-visible:outline-red-300"
85
+ "text-error-600 border border-error-200 bg-error-50 hover:bg-error-100 focus-visible:outline-error-300"
81
86
  else
82
- "text-gray-700 border-2 border-gray-500 bg-white hover:bg-gray-100 active:bg-gray-200 transition-colors"
87
+ "text-gray-700 border border-gray-200 bg-white hover:bg-gray-50"
83
88
  end
84
89
  end
85
90
 
@@ -0,0 +1,17 @@
1
+ <%= content_tag :div, **attrs do %>
2
+ <div class="flex items-start gap-3">
3
+ <i class="fa-solid <%= icon_class %> mt-0.5"></i>
4
+ <div>
5
+ <% if title.present? %>
6
+ <div class="text-sm font-medium"><%= title %></div>
7
+ <% end %>
8
+ <div class="text-sm">
9
+ <% if content? %>
10
+ <%= content %>
11
+ <% else %>
12
+ <%= text %>
13
+ <% end %>
14
+ </div>
15
+ </div>
16
+ </div>
17
+ <% end %>
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Lightweight callout for inline notices (info, warning, success, error).
7
+ # Intended for static, in-form guidance (not toast/flash).
8
+ class CalloutComponent < Panda::Core::Base
9
+ def initialize(text: nil, title: nil, kind: :info, icon: nil, **attrs)
10
+ @text = text
11
+ @title = title
12
+ @kind = kind
13
+ @icon = icon
14
+ super(**attrs)
15
+ end
16
+
17
+ attr_reader :text, :title, :kind, :icon
18
+
19
+ def default_attrs
20
+ {
21
+ class: "rounded-2xl border px-4 py-3 text-sm #{tone_classes}"
22
+ }
23
+ end
24
+
25
+ def icon_class
26
+ icon || default_icon
27
+ end
28
+
29
+ def default_icon
30
+ case kind
31
+ when :success
32
+ "fa-circle-check"
33
+ when :warning
34
+ "fa-triangle-exclamation"
35
+ when :error, :alert
36
+ "fa-circle-xmark"
37
+ else
38
+ "fa-circle-info"
39
+ end
40
+ end
41
+
42
+ def tone_classes
43
+ case kind
44
+ when :success
45
+ "bg-emerald-50 text-emerald-700 border-emerald-200"
46
+ when :warning
47
+ "bg-amber-50 text-amber-700 border-amber-200"
48
+ when :error, :alert
49
+ "bg-rose-50 text-rose-700 border-rose-200"
50
+ else
51
+ "bg-gray-50 text-gray-700 border-gray-200"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ <%= content_tag :pre, **attrs do %>
2
+ <code>
3
+ <% if content? %>
4
+ <%= content %>
5
+ <% else %>
6
+ <%= code %>
7
+ <% end %>
8
+ </code>
9
+ <% end %>
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Code block component for snippets and examples.
7
+ class CodeBlockComponent < Panda::Core::Base
8
+ def initialize(code: nil, **attrs)
9
+ @code = code
10
+ super(**attrs)
11
+ end
12
+
13
+ attr_reader :code
14
+
15
+ def default_attrs
16
+ {
17
+ class: "rounded-xl bg-gray-900/95 p-3 text-xs text-emerald-200 font-mono overflow-x-auto"
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ <div class="relative" data-controller="toggle">
2
+ <main class="overflow-auto flex-1 h-full min-h-full max-h-full">
3
+ <div class="overflow-auto px-2 pt-2 mx-auto sm:px-6 lg:px-6">
4
+ <% if heading_slot? %>
5
+ <%= heading_slot %>
6
+ <% end %>
7
+
8
+ <% if tab_bar_slot? %>
9
+ <%= tab_bar_slot %>
10
+ <% end %>
11
+
12
+ <section class="<%= section_classes %>">
13
+ <div class="flex-1 mt-4 w-full h-full">
14
+ <% if body_slot? %>
15
+ <%= body_slot %>
16
+ <% elsif content.present? %>
17
+ <%= content %>
18
+ <% end %>
19
+ </div>
20
+ </section>
21
+ </div>
22
+ </main>
23
+
24
+ <% if slideover_slot? %>
25
+ <%= slideover_slot %>
26
+ <% end %>
27
+ </div>
@@ -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>