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,75 +4,24 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class FileGalleryComponent < Panda::Core::Base
7
- prop :files, _Nilable(Object), default: -> { [] }
8
- prop :selected_file, _Nilable(Object), default: nil
9
-
10
- def view_template
11
- if @files.any?
12
- render_gallery
13
- else
14
- render_empty_state
15
- end
7
+ def initialize(selected_file: nil, files: [], blob_categories: {}, **attrs)
8
+ @files = files
9
+ @selected_file = selected_file
10
+ @blob_categories = blob_categories
11
+ super(**attrs)
16
12
  end
17
13
 
14
+ attr_reader :files, :selected_file, :blob_categories
15
+
18
16
  private
19
17
 
20
18
  def render_gallery
21
- section do
22
- h2(id: "gallery-heading", class: "sr-only") { "Files" }
23
- ul(
24
- role: "list",
25
- class: "grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8"
26
- ) do
27
- @files.each do |file|
28
- render_file_item(file)
29
- end
30
- end
31
- end
19
+ # Implemented in ERB template
32
20
  end
33
21
 
34
22
  def render_file_item(file)
35
- is_selected = @selected_file && @selected_file.id == file.id
36
-
37
- li(class: "relative") do
38
- div(
39
- class: file_container_classes(is_selected),
40
- style: "cursor: pointer;"
41
- ) do
42
- if file.image?
43
- img(
44
- src: url_for(file),
45
- alt: file.filename.to_s,
46
- class: file_image_classes(is_selected)
47
- )
48
- else
49
- render_file_icon(file)
50
- end
51
-
52
- button(
53
- type: "button",
54
- class: "absolute inset-0 focus:outline-hidden",
55
- data: {
56
- action: "click->file-gallery#selectFile",
57
- file_id: file.id,
58
- file_url: url_for(file),
59
- file_name: file.filename.to_s,
60
- file_size: file.byte_size,
61
- file_type: file.content_type,
62
- file_created: file.created_at.to_s
63
- }
64
- ) do
65
- span(class: "sr-only") { "View details for #{file.filename}" }
66
- end
67
- end
68
-
69
- p(class: "pointer-events-none mt-2 block truncate text-sm font-medium text-gray-900 dark:text-white") do
70
- plain file.filename.to_s
71
- end
72
- p(class: "pointer-events-none block text-sm font-medium text-gray-500 dark:text-gray-400") do
73
- plain number_to_human_size(file.byte_size)
74
- end
75
- end
23
+ @selected_file && @selected_file.id == file.id
24
+ # Implemented in ERB template
76
25
  end
77
26
 
78
27
  def file_container_classes(selected)
@@ -80,56 +29,33 @@ module Panda
80
29
  focus = if selected
81
30
  "outline-2 outline-offset-2 outline-panda-dark dark:outline-panda-light outline"
82
31
  else
83
- "focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-indigo-600 dark:focus-within:outline-indigo-500"
32
+ "focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary-600 dark:focus-within:outline-primary-500"
84
33
  end
85
34
  "#{base} #{focus}"
86
35
  end
87
36
 
88
37
  def file_image_classes(selected)
89
- base = "pointer-events-none aspect-10/7 rounded-lg object-cover outline -outline-offset-1 outline-black/5 dark:outline-white/10"
38
+ base = "pointer-events-none aspect-square object-contain p-1 outline -outline-offset-1 outline-black/5 dark:outline-white/10"
90
39
  hover = selected ? "" : "group-hover:opacity-75"
91
40
  "#{base} #{hover}"
92
41
  end
93
42
 
94
43
  def render_file_icon(file)
95
- div(class: "flex items-center justify-center h-full") do
96
- div(class: "text-center") do
97
- svg(
98
- class: "mx-auto h-12 w-12 text-gray-400",
99
- fill: "none",
100
- viewBox: "0 0 24 24",
101
- stroke: "currentColor",
102
- aria: {hidden: "true"}
103
- ) do
104
- path(
105
- stroke_linecap: "round",
106
- stroke_linejoin: "round",
107
- d: "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
108
- )
109
- end
110
- p(class: "mt-1 text-xs text-gray-500 uppercase") { file.content_type&.split("/")&.last || "file" }
111
- end
112
- end
44
+ # Implemented in ERB template
113
45
  end
114
46
 
115
47
  def render_empty_state
116
- div(class: "text-center py-12 border border-dashed rounded-lg") do
117
- svg(
118
- class: "mx-auto h-12 w-12 text-gray-400",
119
- fill: "none",
120
- viewBox: "0 0 24 24",
121
- stroke: "currentColor",
122
- aria: {hidden: "true"}
123
- ) do
124
- path(
125
- stroke_linecap: "round",
126
- stroke_linejoin: "round",
127
- d: "M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
128
- )
129
- end
130
- h3(class: "mt-2 text-sm font-semibold text-gray-900") { "No files" }
131
- p(class: "mt-1 text-sm text-gray-500") { "Get started by uploading a file." }
132
- end
48
+ # Implemented in ERB template
49
+ end
50
+
51
+ # Build the metadata subtitle for a file in the gallery grid
52
+ def file_metadata(file)
53
+ parts = []
54
+ variant_count = file.variant_records.size
55
+ parts << "#{variant_count} #{"variant".pluralize(variant_count)}" if variant_count > 0
56
+ category = @blob_categories[file.id]
57
+ parts << category.name if category
58
+ parts.any? ? parts.join(" \u00B7 ") : number_to_human_size(file.byte_size)
133
59
  end
134
60
 
135
61
  # Helper method to generate URL for ActiveStorage attachment
@@ -139,6 +65,8 @@ module Panda
139
65
  else
140
66
  "#"
141
67
  end
68
+ rescue
69
+ "#"
142
70
  end
143
71
 
144
72
  # Helper method for human-readable file sizes
@@ -0,0 +1,6 @@
1
+ <div class="flex flex-wrap gap-2 mb-4">
2
+ <%= link_to @all_label, @url_helper.call(nil), class: pill_classes(@active_value.blank?) %>
3
+ <% @items.each do |item| %>
4
+ <%= link_to item[:label], @url_helper.call(item[:value]), class: pill_classes(@active_value == item[:value]) %>
5
+ <% end %>
6
+ </div>
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class FilterPillsComponent < Panda::Core::Base
7
+ def initialize(items:, url_helper:, param_name: :category, active_value: nil, all_label: "All", **attrs)
8
+ @items = items
9
+ @url_helper = url_helper
10
+ @param_name = param_name
11
+ @active_value = active_value
12
+ @all_label = all_label
13
+ super(**attrs)
14
+ end
15
+
16
+ attr_reader :items, :url_helper, :param_name, :active_value, :all_label
17
+
18
+ def pill_classes(active)
19
+ base = "inline-flex items-center rounded-full px-3 py-1.5 text-xs font-medium"
20
+ if active
21
+ "#{base} bg-primary-100 text-primary-700"
22
+ else
23
+ "#{base} bg-gray-100 text-gray-700 hover:bg-gray-200"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ <div aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50">
2
+ <div class="flex w-full flex-col items-center space-y-4 sm:items-end">
3
+ <%= content_tag :div, notification_attrs do %>
4
+ <div class="flex items-center">
5
+ <%= render_icon %>
6
+ <%= render_content %>
7
+ <%= render_close_button %>
8
+ </div>
9
+ <% end %>
10
+ </div>
11
+ </div>
@@ -4,37 +4,19 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class FlashMessageComponent < Panda::Core::Base
7
- prop :message, String
8
- prop :kind, Symbol
9
- prop :temporary, _Boolean, default: true
10
- prop :subtitle, _Nilable(String), default: -> {}
11
-
12
- def view_template
13
- # Global notification container (fixed position)
14
- div(
15
- aria: {live: "assertive"},
16
- class: "pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50"
17
- ) do
18
- div(class: "flex w-full flex-col items-center space-y-4 sm:items-end") do
19
- # Notification panel with Tailwind Plus styling
20
- div(**notification_attrs) do
21
- div(class: "p-4") do
22
- div(class: "flex items-start") do
23
- render_icon
24
- render_content
25
- render_close_button
26
- end
27
- end
28
- end
29
- end
30
- end
7
+ def initialize(message: "", kind: :default, temporary: true, subtitle: nil, **attrs)
8
+ @message = message
9
+ @kind = kind
10
+ @temporary = temporary
11
+ @subtitle = subtitle
12
+ super(**attrs)
31
13
  end
32
14
 
33
- private
15
+ attr_reader :message, :kind, :temporary, :subtitle
34
16
 
35
17
  def notification_attrs
36
18
  {
37
- class: "pointer-events-auto w-full max-w-sm translate-y-0 transform rounded-lg bg-white opacity-100 shadow-lg transition duration-300 ease-out sm:translate-x-0 dark:bg-gray-800 starting:translate-y-2 starting:opacity-0 starting:sm:translate-x-2 starting:sm:translate-y-0 #{border_color_css}",
19
+ class: "pointer-events-auto w-full max-w-md translate-y-0 transform rounded-2xl border px-4 py-3 shadow-lg transition duration-300 ease-out sm:translate-x-0 starting:translate-y-2 starting:opacity-0 starting:sm:translate-x-2 starting:sm:translate-y-0 #{tone_classes}",
38
20
  data: {
39
21
  controller: "alert",
40
22
  alert_dismiss_after_value: (@temporary ? "5000" : nil)
@@ -43,39 +25,30 @@ module Panda
43
25
  end
44
26
 
45
27
  def render_icon
46
- div(class: "shrink-0") do
47
- i(class: "fa-solid size-6 #{icon_css} #{icon_colour_css}")
28
+ content_tag(:div, class: "shrink-0") do
29
+ content_tag(:i, "", class: "fa-solid size-5 #{icon_css} #{icon_colour_css}")
48
30
  end
49
31
  end
50
32
 
51
33
  def render_content
52
- div(class: "ml-3 w-0 flex-1 pt-0.5") do
53
- p(class: "text-sm font-medium text-gray-900 dark:text-white flash-message-title") { @message }
54
- if @subtitle
55
- p(class: "mt-1 text-sm text-gray-500 dark:text-gray-400 flash-message-subtitle") { @subtitle }
34
+ content_tag(:div, class: "ml-2 w-0 flex-1") do
35
+ message_html = content_tag(:p, @message, class: "text-sm font-medium flash-message-title")
36
+ subtitle_html = if @subtitle
37
+ content_tag(:p, @subtitle, class: "mt-1 text-xs opacity-80 flash-message-subtitle")
38
+ else
39
+ "".html_safe
56
40
  end
41
+ (message_html + subtitle_html).html_safe
57
42
  end
58
43
  end
59
44
 
60
45
  def render_close_button
61
- div(class: "ml-4 flex shrink-0") do
62
- button(
46
+ content_tag(:div, class: "ml-4 flex shrink-0") do
47
+ content_tag(:button,
63
48
  type: "button",
64
- class: "inline-flex rounded-md text-gray-400 hover:text-gray-500 focus:outline-2 focus:outline-offset-2 focus:outline-blue-600 dark:hover:text-white dark:focus:outline-blue-500",
65
- data: {action: "alert#close"}
66
- ) do
67
- span(class: "sr-only") { "Close" }
68
- svg(
69
- viewBox: "0 0 20 20",
70
- fill: "currentColor",
71
- data: {slot: "icon"},
72
- aria: {hidden: "true"},
73
- class: "size-5"
74
- ) do |s|
75
- s.path(
76
- d: "M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
77
- )
78
- end
49
+ class: "inline-flex items-center cursor-pointer opacity-60 hover:opacity-100",
50
+ data: {action: "alert#close"}) do
51
+ content_tag(:i, "", class: "fa-solid fa-xmark")
79
52
  end
80
53
  end
81
54
  end
@@ -83,30 +56,30 @@ module Panda
83
56
  def icon_colour_css
84
57
  case @kind
85
58
  when :success
86
- "text-green-400 dark:text-green-500"
59
+ "text-emerald-600"
87
60
  when :alert, :error
88
- "text-red-400 dark:text-red-500"
61
+ "text-rose-600"
89
62
  when :warning
90
- "text-yellow-400 dark:text-yellow-500"
63
+ "text-amber-600"
91
64
  when :info, :notice
92
- "text-blue-400 dark:text-blue-500"
65
+ "text-sky-600"
93
66
  else
94
- "text-gray-400 dark:text-gray-500"
67
+ "text-gray-500"
95
68
  end
96
69
  end
97
70
 
98
- def border_color_css
71
+ def tone_classes
99
72
  case @kind
100
73
  when :success
101
- "ring-2 ring-green-400/20 dark:ring-green-500/30"
74
+ "bg-emerald-50 text-emerald-700 border-emerald-200"
102
75
  when :alert, :error
103
- "ring-2 ring-red-400/20 dark:ring-red-500/30"
76
+ "bg-rose-50 text-rose-700 border-rose-200"
104
77
  when :warning
105
- "ring-2 ring-yellow-400/20 dark:ring-yellow-500/30"
78
+ "bg-amber-50 text-amber-700 border-amber-200"
106
79
  when :info, :notice
107
- "ring-2 ring-blue-400/20 dark:ring-blue-500/30"
80
+ "bg-sky-50 text-sky-700 border-sky-200"
108
81
  else
109
- "ring-1 ring-gray-400/10 dark:ring-gray-500/20"
82
+ "bg-white text-gray-700 border-gray-200"
110
83
  end
111
84
  end
112
85
 
@@ -0,0 +1,7 @@
1
+ <%= content_tag :div, **attrs do %>
2
+ <%= content_tag :div, class: "text-sm text-red-600" do %>
3
+ <% error_messages.each do |message| %>
4
+ <p><%= message %></p>
5
+ <% end %>
6
+ <% end %>
7
+ <% end %>
@@ -4,33 +4,25 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class FormErrorComponent < Panda::Core::Base
7
- prop :errors, _Nilable(_Union(ActiveModel::Errors, Array)), default: -> {}
8
- prop :model, _Nilable(Object), default: -> {}
9
-
10
- def view_template
11
- return unless should_render?
12
-
13
- div(**@attrs) do
14
- div(class: "text-sm text-red-600") do
15
- error_messages.each do |message|
16
- p { message }
17
- end
18
- end
19
- end
7
+ def initialize(model:, **attrs)
8
+ @model = model
9
+ super(**attrs)
20
10
  end
21
11
 
12
+ attr_reader :model
13
+
22
14
  def default_attrs
23
15
  {
24
16
  class: "mb-4 p-4 bg-red-50 border border-red-200 rounded-md"
25
17
  }
26
18
  end
27
19
 
28
- private
29
-
30
- def should_render?
20
+ def render?
31
21
  error_messages.any?
32
22
  end
33
23
 
24
+ private
25
+
34
26
  def error_messages
35
27
  @error_messages ||= if @model&.respond_to?(:errors)
36
28
  @model.errors.full_messages
@@ -0,0 +1,16 @@
1
+ <div <%= tag.attributes(**attrs) %>>
2
+ <% if content? %>
3
+ <%= content %>
4
+ <% end %>
5
+
6
+ <% if cancel_path %>
7
+ <%= link_to "Cancel", cancel_path, class: cancel_link_classes %>
8
+ <% end %>
9
+
10
+ <%= tag.button type: "submit", class: submit_button_classes, data: submit_data_attrs do %>
11
+ <% if computed_icon %>
12
+ <i class="fa-solid <%= computed_icon %>"></i>
13
+ <% end %>
14
+ <%= submit_text %>
15
+ <% end %>
16
+ </div>
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Standardized form footer component with submit button(s) and optional secondary actions.
7
+ #
8
+ # @example Basic usage
9
+ # <%= render Panda::Core::Admin::FormFooterComponent.new(submit_text: "Save") %>
10
+ #
11
+ # @example With icon
12
+ # <%= render Panda::Core::Admin::FormFooterComponent.new(
13
+ # submit_text: "Create Page",
14
+ # icon: "fa-plus"
15
+ # ) %>
16
+ #
17
+ # @example With cancel link
18
+ # <%= render Panda::Core::Admin::FormFooterComponent.new(
19
+ # submit_text: "Update",
20
+ # cancel_path: admin_cms_pages_path
21
+ # ) %>
22
+ #
23
+ # @example With block for custom secondary actions
24
+ # <%= render Panda::Core::Admin::FormFooterComponent.new(submit_text: "Save") do %>
25
+ # <%= link_to "Preview", preview_path, class: "text-sm text-gray-600" %>
26
+ # <% end %>
27
+ #
28
+ class FormFooterComponent < Panda::Core::Base
29
+ def initialize(submit_text: "Save", icon: nil, cancel_path: nil, submit_action: nil, **attrs)
30
+ @submit_text = submit_text
31
+ @icon = icon
32
+ @cancel_path = cancel_path
33
+ @submit_action = submit_action
34
+ super(**attrs)
35
+ end
36
+
37
+ attr_reader :submit_text, :icon, :cancel_path, :submit_action
38
+
39
+ def default_attrs
40
+ {
41
+ class: "flex justify-end gap-x-3 mt-2"
42
+ }
43
+ end
44
+
45
+ def submit_button_classes
46
+ "inline-flex items-center gap-x-2 justify-center rounded-xl bg-primary-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
47
+ end
48
+
49
+ def cancel_link_classes
50
+ "inline-flex items-center justify-center rounded-xl px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-900"
51
+ end
52
+
53
+ def computed_icon
54
+ @icon
55
+ end
56
+
57
+ def submit_data_attrs
58
+ attrs = {disable_with: "Saving..."}
59
+ attrs[:action] = submit_action if submit_action
60
+ attrs
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1 @@
1
+ <%= tag.input(**attrs) %>
@@ -4,18 +4,19 @@ module Panda
4
4
  module Core
5
5
  module Admin
6
6
  class FormInputComponent < Panda::Core::Base
7
- prop :name, String
8
- prop :value, _Nilable(String), default: -> {}
9
- prop :type, Symbol, default: :text
10
- prop :placeholder, _Nilable(String), default: -> {}
11
- prop :required, _Boolean, default: -> { false }
12
- prop :disabled, _Boolean, default: -> { false }
13
- prop :autocomplete, _Nilable(String), default: -> {}
14
-
15
- def view_template
16
- input(**@attrs)
7
+ def initialize(name: "", value: nil, placeholder: nil, autocomplete: nil, type: :text, required: false, disabled: false, **attrs)
8
+ @name = name
9
+ @value = value
10
+ @type = type
11
+ @placeholder = placeholder
12
+ @required = required
13
+ @disabled = disabled
14
+ @autocomplete = autocomplete
15
+ super(**attrs)
17
16
  end
18
17
 
18
+ attr_reader :name, :value, :type, :placeholder, :required, :disabled, :autocomplete
19
+
19
20
  def default_attrs
20
21
  base_attrs = {
21
22
  type: @type.to_s,
@@ -36,12 +37,19 @@ module Panda
36
37
  private
37
38
 
38
39
  def input_classes
39
- classes = "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset placeholder:text-gray-400 focus:ring-2 focus:ring-inset sm:leading-6"
40
+ # Border/radius/focus-ring are themeable via the --panda-input-*
41
+ # tokens (see app/assets/tailwind/application.css). The var()
42
+ # fallbacks resolve to today's rounded-xl/border-gray-300/
43
+ # outline-primary-500 exactly, so this is a no-op unless a host
44
+ # app sets data-theme.
45
+ classes = "block w-full h-11 rounded-[var(--panda-input-radius,var(--radius-xl))] border border-[var(--panda-input-border,var(--color-gray-300))] bg-white px-3 py-2 text-gray-900 " \
46
+ "placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-[var(--panda-input-focus-ring,var(--color-primary-500))] " \
47
+ "dark:bg-white/5 dark:text-white dark:border-white/10 dark:placeholder:text-gray-500"
40
48
 
41
49
  if @disabled
42
- classes + " ring-gray-300 focus:ring-gray-300 bg-gray-50 cursor-not-allowed"
50
+ classes + " border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed"
43
51
  else
44
- classes + " ring-primary-400 focus:ring-primary-600 hover:cursor-pointer"
52
+ classes
45
53
  end
46
54
  end
47
55
  end
@@ -0,0 +1,16 @@
1
+ <div <%= tag.attributes(**attrs) %>>
2
+ <h3 class="<%= heading_classes %>">
3
+ <% if icon %>
4
+ <i class="fa-solid <%= icon %>"></i>
5
+ <% end %>
6
+ <%= title %>
7
+ </h3>
8
+
9
+ <% if description %>
10
+ <p class="<%= description_classes %>"><%= description %></p>
11
+ <% end %>
12
+
13
+ <div class="<%= content_classes %>">
14
+ <%= content %>
15
+ </div>
16
+ </div>
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Standardized form section component for grouping related form fields.
7
+ # Provides a consistent heading style for form sections without the heavy
8
+ # visual weight of PanelComponent.
9
+ #
10
+ # @example Basic usage
11
+ # <%= render Panda::Core::Admin::FormSectionComponent.new(title: "SEO Settings") do %>
12
+ # <%= f.text_field :seo_title %>
13
+ # <%= f.text_area :seo_description %>
14
+ # <% end %>
15
+ #
16
+ # @example With description
17
+ # <%= render Panda::Core::Admin::FormSectionComponent.new(
18
+ # title: "Notification Settings",
19
+ # description: "Configure how you receive notifications"
20
+ # ) do %>
21
+ # ...
22
+ # <% end %>
23
+ #
24
+ # @example With icon
25
+ # <%= render Panda::Core::Admin::FormSectionComponent.new(
26
+ # title: "Menu Items",
27
+ # icon: "fa-bars"
28
+ # ) do %>
29
+ # ...
30
+ # <% end %>
31
+ #
32
+ class FormSectionComponent < Panda::Core::Base
33
+ def initialize(title:, description: nil, icon: nil, border_top: true, **attrs)
34
+ @title = title
35
+ @description = description
36
+ @icon = icon
37
+ @border_top = border_top
38
+ super(**attrs)
39
+ end
40
+
41
+ attr_reader :title, :description, :icon, :border_top
42
+
43
+ def default_attrs
44
+ base_classes = "mt-6 pt-4"
45
+ base_classes += " border-t border-gray-200" if border_top
46
+
47
+ {class: base_classes}
48
+ end
49
+
50
+ def heading_classes
51
+ "text-sm font-semibold text-gray-700 flex items-center gap-2"
52
+ end
53
+
54
+ def description_classes
55
+ "mt-1 text-xs text-gray-500"
56
+ end
57
+
58
+ def content_classes
59
+ "mt-4 space-y-4"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ <%= content_tag :select, @attrs do %>
2
+ <%= render_prompt if @prompt %>
3
+ <%= render_blank if @include_blank && !@prompt %>
4
+ <%= render_options %>
5
+ <% end %>