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,22 @@
1
+ <div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
2
+ <%= form_with url: url, method: :get, local: true, class: "flex flex-col gap-3 sm:flex-row sm:items-center w-full" do %>
3
+ <div class="flex-1 max-w-md grid grid-cols-1">
4
+ <%= text_field_tag search_name, search_value,
5
+ placeholder: search_placeholder,
6
+ class: "col-start-1 row-start-1 block w-full h-9 rounded-md bg-white py-1.5 pr-3 pl-9 text-sm text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-600 dark:bg-white/5 dark:text-white dark:ring-white/10 dark:placeholder:text-gray-500 dark:focus:outline-primary-500" %>
7
+ <i class="fa-solid fa-magnifying-glass pointer-events-none col-start-1 row-start-1 ml-3 size-4 self-center text-gray-400 dark:text-gray-500"></i>
8
+ </div>
9
+
10
+ <div class="flex gap-2 items-center">
11
+ <% filters.each do |filter| %>
12
+ <%= filter %>
13
+ <% end %>
14
+
15
+ <%= submit_tag "Filter", class: "h-9 rounded-md bg-white px-3 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-700 dark:text-white dark:ring-gray-600 dark:hover:bg-gray-600 cursor-pointer" %>
16
+
17
+ <% if show_clear %>
18
+ <%= link_to "Clear", clear_url, class: "rounded-md px-3 py-1.5 text-sm font-semibold text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300" %>
19
+ <% end %>
20
+ </div>
21
+ <% end %>
22
+ </div>
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class SearchFilterBarComponent < Panda::Core::Base
7
+ renders_many :filters
8
+
9
+ def initialize(url:, search_name: :q, search_value: nil, search_placeholder: "Search...", clear_url: nil, show_clear: false, **attrs)
10
+ @url = url
11
+ @search_name = search_name
12
+ @search_value = search_value
13
+ @search_placeholder = search_placeholder
14
+ @clear_url = clear_url || url
15
+ @show_clear = show_clear
16
+ super(**attrs)
17
+ end
18
+
19
+ attr_reader :url, :search_name, :search_value, :search_placeholder, :clear_url, :show_clear
20
+
21
+ def select_classes
22
+ "h-9 w-auto rounded-md border-0 py-1.5 pl-3 pr-10 text-sm text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary-600 dark:bg-gray-700 dark:text-white dark:ring-gray-600"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ <div data-controller="clipboard"
2
+ data-clipboard-secret-value="<%= value %>"
3
+ data-clipboard-masked-value="<%= masked %>"
4
+ class="bg-gray-50 rounded-lg p-3 font-mono text-sm">
5
+ <div class="flex items-center gap-3">
6
+ <code data-clipboard-target="display" class="text-gray-700 break-all flex-1"><%= display_value %></code>
7
+ <button type="button"
8
+ data-action="click->clipboard#toggleReveal"
9
+ data-clipboard-target="revealButton"
10
+ class="<%= button_classes %>"
11
+ aria-label="Reveal secret value">
12
+ <i data-clipboard-target="revealIcon" class="fa-solid fa-eye"></i>
13
+ <span data-clipboard-target="revealText">Reveal</span>
14
+ </button>
15
+ <button type="button"
16
+ data-action="click->clipboard#copy"
17
+ data-clipboard-target="copyButton"
18
+ class="<%= button_classes %>"
19
+ aria-label="Copy secret value to clipboard">
20
+ <i data-clipboard-target="copyIcon" class="fa-solid fa-copy"></i>
21
+ <span data-clipboard-target="copyText">Copy</span>
22
+ </button>
23
+ </div>
24
+ </div>
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class SecretFieldComponent < Panda::Core::Base
7
+ def initialize(value:, masked: true, **attrs)
8
+ @value = value
9
+ @masked = masked
10
+ super(**attrs)
11
+ end
12
+
13
+ attr_reader :value, :masked
14
+
15
+ def display_value
16
+ return value unless masked && value.present?
17
+
18
+ if value.length > 4
19
+ "\u2022" * 12 + value[-4..]
20
+ else
21
+ "\u2022" * value.length
22
+ end
23
+ end
24
+
25
+ def button_classes
26
+ "shrink-0 btn btn-secondary transition"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,160 @@
1
+ <nav <%= tag.attributes(**attrs) %>>
2
+ <ul role="list" class="flex flex-col flex-1">
3
+ <%# Logo area — uses admin_logo config or falls back to admin_title text %>
4
+ <li class="hidden lg:block mt-4 mb-4 px-2">
5
+ <% if admin_logo %>
6
+ <div class="flex items-center justify-between">
7
+ <div class="flex items-center gap-2">
8
+ <%= safe_html(admin_logo) %>
9
+ </div>
10
+ <% if admin_logo_actions.any? || admin_settings_path %>
11
+ <div class="flex items-center gap-1">
12
+ <% admin_logo_actions.each do |action| %>
13
+ <%= helpers.link_to action[:path], class: "flex items-center justify-center w-7 h-7 rounded-lg text-white/40 hover:text-white hover:bg-white/10 transition-colors", title: action[:title] do %>
14
+ <i class="<%= action[:icon] %> text-sm"></i>
15
+ <% end %>
16
+ <% end %>
17
+ <% if admin_settings_path %>
18
+ <%= helpers.link_to admin_settings_path, class: "flex items-center justify-center w-7 h-7 rounded-lg text-white/40 hover:text-white hover:bg-white/10 transition-colors", title: "Settings" do %>
19
+ <i class="fa-solid fa-gear text-sm"></i>
20
+ <% end %>
21
+ <% end %>
22
+ </div>
23
+ <% end %>
24
+ </div>
25
+ <% else %>
26
+ <a class="p-0 text-xl font-medium text-white"><%= Panda::Core.config.admin_title || "Panda Admin" %></a>
27
+ <% end %>
28
+ </li>
29
+
30
+ <%= render Panda::Core::Admin::SearchBarComponent.new %>
31
+
32
+ <%
33
+ nav_items = Panda::Core::NavigationRegistry.build(@user, helpers: helpers)
34
+
35
+ top_items = nav_items.select { |item| item[:position] != :bottom }
36
+ bottom_items = nav_items.select { |item| item[:position] == :bottom }
37
+
38
+ def collect_paths(items)
39
+ items.flat_map do |item|
40
+ paths = item[:path] ? [item[:path]] : []
41
+ paths += collect_paths(item[:children] || [])
42
+ paths
43
+ end
44
+ end
45
+
46
+ all_paths = collect_paths(nav_items).compact
47
+ current_path = helpers.request&.path || "/"
48
+ active_path = all_paths
49
+ .sort_by { |path| -path.length }
50
+ .find { |path| current_path == path || current_path.starts_with?(path + "/") }
51
+ %>
52
+
53
+ <% if sectioned?(top_items) %>
54
+ <%# Section-aware navigation — insert headings when section changes, preserving order %>
55
+ <% current_section = nil %>
56
+ <% section_count = 0 %>
57
+ <% top_items.each_with_index do |item, index| %>
58
+ <% item_section = item[:section].presence %>
59
+ <% if item_section && item_section != current_section %>
60
+ <% current_section = item_section %>
61
+ <li class="px-3 <%= section_count > 0 ? (compact_navigation? ? 'mt-3' : 'mt-5') : '' %> <%= compact_navigation? ? 'mb-0.5' : 'mb-1' %>">
62
+ <span class="text-[10px] font-bold uppercase tracking-widest text-white/40 sidebar-section-label"><%= current_section %></span>
63
+ </li>
64
+ <% section_count += 1 %>
65
+ <% end %>
66
+ <%
67
+ has_children = item[:children].present?
68
+ if has_children
69
+ is_active = item[:children].any? { |child| child[:path] == active_path }
70
+ else
71
+ is_active = item[:path] == active_path
72
+ end
73
+ %>
74
+ <%= render Panda::Core::Admin::Navigation::ItemComponent.new(
75
+ label: item[:label],
76
+ icon: item[:icon],
77
+ path: item[:path],
78
+ active: is_active,
79
+ menu_id: "sub-menu-#{section_count}-#{index}",
80
+ target: item[:target],
81
+ badge: item[:badge],
82
+ badge_color: item[:badge_color],
83
+ compact: compact_navigation?
84
+ ) do |nav_item|
85
+ if has_children
86
+ item[:children].each do |child|
87
+ child_is_active = child[:path] == active_path
88
+ nav_item.with_sub_item(label: child[:label], path: child[:path], active: child_is_active, target: child[:target])
89
+ end
90
+ end
91
+ end %>
92
+ <% end %>
93
+ <% else %>
94
+ <%# Legacy flat navigation (no section keys) %>
95
+ <% top_items.each_with_index do |item, index| %>
96
+ <%
97
+ has_children = item[:children].present?
98
+ if has_children
99
+ is_active = item[:children].any? { |child| child[:path] == active_path }
100
+ else
101
+ is_active = item[:path] == active_path
102
+ end
103
+ %>
104
+ <%= render Panda::Core::Admin::Navigation::ItemComponent.new(
105
+ label: item[:label],
106
+ icon: item[:icon],
107
+ path: item[:path],
108
+ active: is_active,
109
+ menu_id: "sub-menu-#{index}",
110
+ target: item[:target],
111
+ badge: item[:badge],
112
+ badge_color: item[:badge_color],
113
+ compact: compact_navigation?
114
+ ) do |nav_item|
115
+ if has_children
116
+ item[:children].each do |child|
117
+ child_is_active = child[:path] == active_path
118
+ nav_item.with_sub_item(label: child[:label], path: child[:path], active: child_is_active, target: child[:target])
119
+ end
120
+ end
121
+ end %>
122
+ <% end %>
123
+ <% end %>
124
+
125
+ <% bottom_items.each do |section| %>
126
+ <%
127
+ is_section_active = (section[:children] || []).any? { |child|
128
+ child[:path] && current_path.starts_with?(child[:path])
129
+ }
130
+ %>
131
+ <%= render Panda::Core::Admin::Navigation::UserMenuComponent.new(user: helpers.current_user, active: is_section_active) do |user_menu|
132
+ (section[:children] || []).each do |item|
133
+ item_path = item[:path]
134
+ if item_path.nil? && item[:path_helper]
135
+ helper_name = item[:path_helper].to_sym
136
+ item_path = helpers.panda_core.public_send(helper_name) if helpers.panda_core.respond_to?(helper_name)
137
+ end
138
+
139
+ item_active = item_path && current_path.starts_with?(item_path)
140
+ user_menu.with_sub_item(
141
+ label: item[:label],
142
+ path: item_path,
143
+ active: item_active,
144
+ method: item[:method],
145
+ button_options: item[:button_options] || {}
146
+ )
147
+ end
148
+ end %>
149
+ <% end %>
150
+
151
+ <% sidebar_footer = Panda::Core.config.admin_sidebar_footer %>
152
+ <li class="px-2 py-3 border-t border-white/10">
153
+ <% if sidebar_footer %>
154
+ <%= safe_html(sidebar_footer) %>
155
+ <% else %>
156
+ <span class="text-xs text-white/30">Panda Core v<%= Panda::Core::VERSION %></span>
157
+ <% end %>
158
+ </li>
159
+ </ul>
160
+ </nav>
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Sidebar navigation component for admin pages
7
+ # Displays hierarchical navigation with toggle for nested items.
8
+ # Supports optional section grouping via the :section key on nav items.
9
+ class SidebarComponent < Panda::Core::Base
10
+ def initialize(user:, **attrs)
11
+ @user = user
12
+ super(**attrs)
13
+ end
14
+
15
+ attr_reader :user
16
+
17
+ def admin_logo
18
+ Panda::Core.config.admin_logo
19
+ end
20
+
21
+ def admin_settings_path
22
+ Panda::Core.config.admin_settings_path
23
+ end
24
+
25
+ def compact_navigation?
26
+ Panda::Core.config.compact_navigation
27
+ end
28
+
29
+ def admin_logo_actions
30
+ Panda::Core.config.admin_logo_actions || []
31
+ end
32
+
33
+ def sectioned?(items)
34
+ items.any? { |item| item[:section].present? }
35
+ end
36
+
37
+ def safe_html(value)
38
+ raw = value.respond_to?(:call) ? value.call : value
39
+ helpers.sanitize(raw, tags: %w[svg path ellipse circle rect line polyline polygon g defs use
40
+ span a i div img br strong em b], attributes: %w[
41
+ viewBox fill stroke stroke-width stroke-linecap stroke-linejoin opacity cx cy rx ry r
42
+ x y x1 y1 x2 y2 width height d points transform xmlns class href style id
43
+ data-turbo-track rel
44
+ ])
45
+ end
46
+
47
+ private
48
+
49
+ def default_attrs
50
+ {class: "flex flex-col flex-1"}
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,67 @@
1
+ <%# Backdrop overlay - clicks close the slideover %>
2
+ <div class="hidden fixed inset-0 bg-gray-500/75 transition-opacity duration-500 ease-in-out dark:bg-gray-900/50 z-40"
3
+ data-toggle-target="toggleable"
4
+ data-action="click->toggle#toggle"
5
+ data-transition-enter="transition-opacity duration-500 ease-in-out"
6
+ data-transition-enter-from="opacity-0"
7
+ data-transition-enter-to="opacity-100"
8
+ data-transition-leave="transition-opacity duration-500 ease-in-out"
9
+ data-transition-leave-from="opacity-100"
10
+ data-transition-leave-to="opacity-0"></div>
11
+
12
+ <%# Slideover panel %>
13
+ <div id="slideover"
14
+ class="<%= slideover_classes %>"
15
+ data-toggle-target="toggleable"
16
+ data-transition-enter="transform transition ease-in-out duration-500 sm:duration-700"
17
+ data-transition-enter-from="translate-x-full"
18
+ data-transition-enter-to="translate-x-0"
19
+ data-transition-leave="transform transition ease-in-out duration-500 sm:duration-700"
20
+ data-transition-leave-from="translate-x-0"
21
+ data-transition-leave-to="translate-x-full">
22
+
23
+ <%# Main container %>
24
+ <div class="relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800">
25
+ <%# Header with title and close button %>
26
+ <div class="bg-gradient-admin px-4 py-6 sm:px-6">
27
+ <div class="flex items-center justify-between">
28
+ <h2 class="text-base font-semibold text-white" id="slideover-title">
29
+ <%= @title %>
30
+ </h2>
31
+ <div class="ml-3 flex items-center">
32
+ <%= content_tag :button,
33
+ type: "button",
34
+ data: {
35
+ action: "click->toggle#toggle touch->toggle#toggle"
36
+ },
37
+ class: "inline-flex items-center gap-1 rounded-full bg-white/10 px-2 py-1 text-xs text-white/80 hover:text-white cursor-pointer" do %>
38
+ <span>Close</span>
39
+ <i class="fa-solid fa-xmark"></i>
40
+ <% end %>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <%# Scrollable content area %>
46
+ <div class="flex-1 overflow-y-auto">
47
+ <% if @content_html %>
48
+ <%= raw(@content_html) %>
49
+ <% elsif @content_block %>
50
+ <%= @content_block.call %>
51
+ <% end %>
52
+ </div>
53
+
54
+ <%# Sticky footer (if footer content exists) %>
55
+ <% if footer_slot? || @footer_html || @footer_block %>
56
+ <div class="flex shrink-0 justify-end gap-x-3 border-t border-gray-200 px-4 py-4 dark:border-white/10">
57
+ <% if footer_slot? %>
58
+ <%= footer_slot %>
59
+ <% elsif @footer_html %>
60
+ <%= raw(@footer_html) %>
61
+ <% elsif @footer_block %>
62
+ <%= @footer_block.call %>
63
+ <% end %>
64
+ </div>
65
+ <% end %>
66
+ </div>
67
+ </div>
@@ -4,79 +4,25 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class SlideoverComponent < Panda::Core::Base
7
- prop :title, String, default: "Settings"
8
- prop :open, _Nilable(_Boolean), default: -> { false }
7
+ renders_one :footer_slot
9
8
 
10
- def view_template(&block)
11
- # Capture block content
12
- if block_given?
13
- if defined?(view_context) && view_context
14
- @content_html = view_context.capture(&block)
15
- else
16
- @content_block = block
17
- end
18
- end
19
-
20
- div(
21
- **default_attrs,
22
- data: {
23
- toggle_target: "toggleable",
24
- transition_enter: "transform transition ease-in-out duration-500 sm:duration-700",
25
- transition_enter_from: "translate-x-full",
26
- transition_enter_to: "translate-x-0",
27
- transition_leave: "transform transition ease-in-out duration-500 sm:duration-700",
28
- transition_leave_from: "translate-x-0",
29
- transition_leave_to: "translate-x-full"
30
- }
31
- ) do
32
- # Main container
33
- div(class: "relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800") do
34
- # Header with title and close button
35
- div(class: "bg-gradient-admin px-4 py-6 sm:px-6") do
36
- div(class: "flex items-center justify-between") do
37
- h2(class: "text-base font-semibold text-white", id: "slideover-title") do
38
- plain @title
39
- end
40
- div(class: "ml-3 flex h-7 items-center") do
41
- button(
42
- type: "button",
43
- data: {action: "click->toggle#toggle touch->toggle#toggle"},
44
- class: "relative rounded-md text-white/80 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
45
- ) do
46
- span(class: "absolute -inset-2.5")
47
- span(class: "sr-only") { "Close panel" }
48
- # SVG close icon
49
- svg(viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "1.5", aria_hidden: "true", class: "size-6") do
50
- path(d: "M6 18 18 6M6 6l12 12", stroke_linecap: "round", stroke_linejoin: "round")
51
- end
52
- end
53
- end
54
- end
55
- end
9
+ def initialize(title: "Settings", open: false, **attrs)
10
+ @title = title
11
+ @open = open
12
+ super(**attrs)
13
+ end
56
14
 
57
- # Scrollable content area
58
- div(class: "flex-1 overflow-y-auto") do
59
- if @content_html
60
- raw(@content_html)
61
- elsif @content_block
62
- instance_eval(@content_block)
63
- end
64
- end
15
+ attr_reader :title, :open
65
16
 
66
- # Sticky footer (if footer content exists)
67
- if @footer_html || @footer_block
68
- div(class: "flex shrink-0 justify-end gap-x-3 border-t border-gray-200 px-4 py-4 dark:border-white/10") do
69
- if @footer_html
70
- raw(@footer_html)
71
- elsif @footer_block
72
- instance_eval(&@footer_block)
73
- end
74
- end
75
- end
76
- end
17
+ def before_render
18
+ # Store main content if provided
19
+ # In ViewComponent, content is already rendered HTML (SafeBuffer), not a block
20
+ if content.present?
21
+ @content_html = content
77
22
  end
78
23
  end
79
24
 
25
+ # Legacy method for backwards compatibility
80
26
  def footer(&block)
81
27
  if defined?(view_context) && view_context
82
28
  @footer_html = view_context.capture(&block)
@@ -85,8 +31,6 @@ module Panda
85
31
  end
86
32
  end
87
33
 
88
- alias_method :with_footer, :footer
89
-
90
34
  private
91
35
 
92
36
  def default_attrs
@@ -97,10 +41,27 @@ module Panda
97
41
  end
98
42
 
99
43
  def slideover_classes
100
- base = "ml-auto block size-full max-w-md transform absolute right-0 h-full z-50"
44
+ # Use fixed positioning to escape parent overflow clipping
45
+ base = "ml-auto block size-full max-w-md transform fixed right-0 top-0 h-full z-50"
101
46
  visibility = @open ? "" : "hidden"
102
47
  [base, visibility].compact.join(" ")
103
48
  end
49
+
50
+ def close_icon
51
+ content_tag(:svg,
52
+ viewBox: "0 0 24 24",
53
+ fill: "none",
54
+ stroke: "currentColor",
55
+ stroke_width: "1.5",
56
+ aria: {hidden: "true"},
57
+ class: "size-6") do
58
+ tag.path(
59
+ d: "M6 18 18 6M6 6l12 12",
60
+ stroke_linecap: "round",
61
+ stroke_linejoin: "round"
62
+ )
63
+ end
64
+ end
104
65
  end
105
66
  end
106
67
  end
@@ -0,0 +1,4 @@
1
+ <div class="overflow-hidden p-4 bg-white rounded-2xl border border-gray-200">
2
+ <dt class="text-sm font-medium text-gray-500 truncate"><%= @metric %></dt>
3
+ <dd class="mt-1 text-2xl font-semibold tracking-tight text-gray-900"><%= @value %></dd>
4
+ </div>
@@ -4,15 +4,13 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class StatisticsComponent < Panda::Core::Base
7
- prop :metric, String
8
- prop :value, _Nilable(_Union(String, Integer, Float))
9
-
10
- def view_template
11
- div(class: "overflow-hidden p-4 bg-gradient-to-br rounded-lg border-2 from-light/20 to-light border-mid") do
12
- dt(class: "text-base font-medium truncate text-dark") { @metric }
13
- dd(class: "mt-1 text-3xl font-medium tracking-tight text-dark") { @value }
14
- end
7
+ def initialize(metric: "", value: nil, **attrs)
8
+ @metric = metric
9
+ @value = value
10
+ super(**attrs)
15
11
  end
12
+
13
+ attr_reader :metric, :value
16
14
  end
17
15
  end
18
16
  end
@@ -0,0 +1,48 @@
1
+ <div class="mt-3">
2
+ <%# Mobile select dropdown %>
3
+ <div class="sm:hidden">
4
+ <%= tag.label "Select a tab", for: "tabs", class: "sr-only" %>
5
+ <%= tag.select id: "tabs",
6
+ name: "tabs",
7
+ onchange: "window.location.href=this.value",
8
+ class: "block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-primary-400 focus:border-primary-600 focus:ring-primary-600" do %>
9
+ <% @tabs.each_with_index do |tab, index| %>
10
+ <%= tag.option tab[:name], value: tab[:url] || "#", selected: tab_current?(tab, index) %>
11
+ <% end %>
12
+ <% end %>
13
+ </div>
14
+
15
+ <%# Desktop tab navigation %>
16
+ <div class="hidden sm:block">
17
+ <div class="flex items-center border-b border-gray-200">
18
+ <nav class="flex flex-1 -mb-px space-x-6 xl:space-x-8" aria-label="Tabs">
19
+ <% @tabs.each_with_index do |tab, index| %>
20
+ <%= content_tag :a,
21
+ tab[:name],
22
+ href: tab[:url] || "#",
23
+ class: tab_classes(tab, index),
24
+ aria: {current: tab_current?(tab, index) ? "page" : nil} %>
25
+ <% end %>
26
+ </nav>
27
+
28
+ <%# View toggle buttons %>
29
+ <div class="hidden items-center p-0.5 ml-6 bg-gray-100 rounded-lg sm:flex">
30
+ <%# List view button %>
31
+ <%= content_tag :button,
32
+ type: "button",
33
+ class: view_button_classes(false) do %>
34
+ <%= list_view_icon %>
35
+ <span class="sr-only">Use list view</span>
36
+ <% end %>
37
+
38
+ <%# Grid view button %>
39
+ <%= content_tag :button,
40
+ type: "button",
41
+ class: view_button_classes(true) do %>
42
+ <%= grid_view_icon %>
43
+ <span class="sr-only">Use grid view</span>
44
+ <% end %>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>