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
@@ -0,0 +1,51 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "Preview Import", level: 1) %>
3
+
4
+ <%= render Panda::Core::Admin::PanelComponent.new do %>
5
+ <p class="text-sm text-gray-600 mb-4">
6
+ Showing first <%= @preview_rows.size %> rows. Review the mapped data below before importing.
7
+ </p>
8
+
9
+ <div class="overflow-x-auto">
10
+ <table class="min-w-full divide-y divide-gray-200">
11
+ <thead>
12
+ <tr>
13
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Row</th>
14
+ <% @mapping.each do |csv_col, field_name| %>
15
+ <% defn = @field_definitions.find { |d| d.name.to_s == field_name } %>
16
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">
17
+ <%= defn&.label || field_name %>
18
+ </th>
19
+ <% end %>
20
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
21
+ </tr>
22
+ </thead>
23
+ <tbody class="divide-y divide-gray-100">
24
+ <% @preview_rows.each_with_index do |row, i| %>
25
+ <% attrs, errors = @import_session.importable_class.import_row(row, @mapping) %>
26
+ <tr class="<%= errors.any? ? 'bg-red-50' : '' %>">
27
+ <td class="px-3 py-2 text-sm text-gray-500"><%= i + 2 %></td>
28
+ <% @mapping.each do |csv_col, field_name| %>
29
+ <td class="px-3 py-2 text-sm text-gray-900"><%= row[csv_col] %></td>
30
+ <% end %>
31
+ <td class="px-3 py-2 text-sm">
32
+ <% if errors.any? %>
33
+ <span class="text-red-600"><%= errors.join(", ") %></span>
34
+ <% else %>
35
+ <span class="text-green-600"><i class="fa-solid fa-check"></i> OK</span>
36
+ <% end %>
37
+ </td>
38
+ </tr>
39
+ <% end %>
40
+ </tbody>
41
+ </table>
42
+ </div>
43
+
44
+ <div class="flex items-center gap-3 mt-6">
45
+ <%= button_to import_admin_import_session_path(@import_session), method: :post, class: "inline-flex items-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90 transition cursor-pointer" do %>
46
+ <i class="fa-solid fa-file-import mr-2"></i> Start Import
47
+ <% end %>
48
+ <%= link_to "Back to mapping", column_map_admin_import_session_path(@import_session), class: "text-sm text-gray-500 hover:text-gray-700" %>
49
+ </div>
50
+ <% end %>
51
+ <% end %>
@@ -0,0 +1,87 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "Import Results", level: 1) %>
3
+
4
+ <%= render Panda::Core::Admin::PanelComponent.new do %>
5
+ <%# Status badge %>
6
+ <div class="mb-6">
7
+ <% case @import_session.status %>
8
+ <% when "complete" %>
9
+ <span class="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-3 py-1 text-sm font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
10
+ <i class="fa-solid fa-check-circle"></i> Complete
11
+ </span>
12
+ <% when "importing" %>
13
+ <span class="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700 ring-1 ring-inset ring-blue-600/20">
14
+ <i class="fa-solid fa-spinner fa-spin"></i> Importing...
15
+ </span>
16
+ <% when "failed" %>
17
+ <span class="inline-flex items-center gap-1.5 rounded-full bg-red-50 px-3 py-1 text-sm font-medium text-red-700 ring-1 ring-inset ring-red-600/20">
18
+ <i class="fa-solid fa-exclamation-circle"></i> Failed
19
+ </span>
20
+ <% end %>
21
+ </div>
22
+
23
+ <%# Stats %>
24
+ <div class="grid grid-cols-2 gap-4 sm:grid-cols-4 mb-6">
25
+ <div class="rounded-lg bg-gray-50 p-4">
26
+ <div class="text-2xl font-semibold text-gray-900"><%= @import_session.total_rows %></div>
27
+ <div class="text-sm text-gray-500">Total rows</div>
28
+ </div>
29
+ <div class="rounded-lg bg-green-50 p-4">
30
+ <div class="text-2xl font-semibold text-green-700"><%= @import_session.imported_count %></div>
31
+ <div class="text-sm text-green-600">Imported</div>
32
+ </div>
33
+ <div class="rounded-lg bg-red-50 p-4">
34
+ <div class="text-2xl font-semibold text-red-700"><%= @import_session.error_count %></div>
35
+ <div class="text-sm text-red-600">Errors</div>
36
+ </div>
37
+ <div class="rounded-lg bg-gray-50 p-4">
38
+ <div class="text-2xl font-semibold text-gray-900"><%= @import_session.skipped_count %></div>
39
+ <div class="text-sm text-gray-500">Skipped</div>
40
+ </div>
41
+ </div>
42
+
43
+ <%# Progress bar %>
44
+ <% if @import_session.importing? %>
45
+ <div class="mb-6">
46
+ <div class="flex justify-between text-sm text-gray-600 mb-1">
47
+ <span>Progress</span>
48
+ <span><%= @import_session.progress_percentage %>%</span>
49
+ </div>
50
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
51
+ <div class="bg-primary h-2.5 rounded-full transition-all" style="width: <%= @import_session.progress_percentage %>%"></div>
52
+ </div>
53
+ </div>
54
+ <% end %>
55
+
56
+ <%# Errors log %>
57
+ <% if @import_session.errors_log.any? %>
58
+ <div class="mt-6">
59
+ <h3 class="text-sm font-medium text-gray-900 mb-3">Errors (<%= @import_session.errors_log.size %> rows)</h3>
60
+ <div class="max-h-64 overflow-y-auto rounded-lg border border-red-200">
61
+ <table class="min-w-full divide-y divide-red-100">
62
+ <thead class="bg-red-50">
63
+ <tr>
64
+ <th class="px-3 py-2 text-left text-xs font-medium text-red-700">Row</th>
65
+ <th class="px-3 py-2 text-left text-xs font-medium text-red-700">Errors</th>
66
+ </tr>
67
+ </thead>
68
+ <tbody class="divide-y divide-red-50">
69
+ <% @import_session.errors_log.each do |entry| %>
70
+ <tr>
71
+ <td class="px-3 py-2 text-sm text-gray-900"><%= entry["row"] %></td>
72
+ <td class="px-3 py-2 text-sm text-red-600"><%= Array(entry["errors"]).join(", ") %></td>
73
+ </tr>
74
+ <% end %>
75
+ </tbody>
76
+ </table>
77
+ </div>
78
+ </div>
79
+ <% end %>
80
+
81
+ <div class="mt-6">
82
+ <a href="javascript:history.back()" class="text-sm text-gray-500 hover:text-gray-700">
83
+ <i class="fa-solid fa-arrow-left mr-1"></i> Back
84
+ </a>
85
+ </div>
86
+ <% end %>
87
+ <% end %>
@@ -1,60 +1,72 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.heading(text: "My Profile", level: 1) %>
2
+ <% component.with_heading_slot(text: "My Profile", level: 1) %>
3
3
 
4
- <%= form_with model: user,
4
+ <%= panda_form_with model: user,
5
5
  url: admin_my_profile_path,
6
6
  method: :patch,
7
7
  local: true,
8
+ html: { multipart: true },
8
9
  data: { controller: "theme-form" } do |f| %>
9
10
  <%= render Panda::Core::Admin::FormErrorComponent.new(model: user) %>
10
11
 
11
- <div class="space-y-6">
12
- <!-- Avatar Section -->
13
- <div class="col-span-full">
14
- <%= f.label :avatar, "Profile Picture", class: "block text-sm/6 font-medium text-gray-900 dark:text-white" %>
15
- <div class="mt-2 flex items-center gap-x-3">
16
- <% if user.avatar.attached? %>
17
- <%= image_tag main_app.url_for(user.avatar), alt: user.name, class: "w-48 h-48 max-w-48 rounded-full object-cover" %>
18
- <% else %>
19
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="w-48 h-48 text-gray-300 dark:text-gray-500">
12
+ <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
13
+ <% panel.with_heading_slot { "Profile Settings" } %>
14
+ <div class="space-y-4">
15
+ <!-- Avatar Section -->
16
+ <div class="col-span-full" data-controller="avatar-upload">
17
+ <span id="avatar-label" class="block text-sm/6 font-medium text-gray-900 dark:text-white">Profile Picture</span>
18
+ <div class="mt-2 flex items-center gap-x-3">
19
+ <% avatar = user.avatar_url %>
20
+ <img src="<%= avatar.presence || 'about:blank' %>"
21
+ alt="<%= user.name %>"
22
+ class="w-48 h-48 max-w-48 rounded-full object-cover<%= ' hidden' unless avatar.present? %>"
23
+ data-avatar-upload-target="preview">
24
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"
25
+ class="w-48 h-48 text-gray-300 dark:text-gray-500<%= ' hidden' if avatar.present? %>"
26
+ data-avatar-upload-target="placeholder">
20
27
  <path d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" clip-rule="evenodd" fill-rule="evenodd" />
21
28
  </svg>
22
- <% end %>
23
- <label for="<%= f.field_id(:avatar) %>" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 cursor-pointer dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20">
24
- Change
25
- </label>
26
- <%= f.file_field :avatar,
27
- accept: "image/png,image/jpeg,image/jpg,image/gif,image/webp",
28
- class: "sr-only" %>
29
+ <div>
30
+ <label for="user_avatar" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 cursor-pointer dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20">
31
+ Change
32
+ </label>
33
+ <% if user.avatar.respond_to?(:blob) && user.avatar.blob.present? %>
34
+ <% blob = user.avatar.blob %>
35
+ <p class="mt-2 text-xs text-gray-600 dark:text-gray-400">
36
+ Current: <%= blob.filename %> (<%= number_to_human_size(blob.byte_size) %>)
37
+ </p>
38
+ <% end %>
39
+ </div>
40
+ <input type="file"
41
+ id="user_avatar"
42
+ name="user[avatar]"
43
+ accept="image/png,image/jpeg,image/jpg,image/gif,image/webp"
44
+ class="sr-only"
45
+ aria-labelledby="avatar-label"
46
+ data-avatar-upload-target="input"
47
+ data-action="change->avatar-upload#handleFileSelect">
48
+ </div>
49
+ <!-- File info (shown when a new file is selected) -->
50
+ <div class="hidden mt-3 flex items-center gap-x-3 text-sm text-gray-600 dark:text-gray-400" data-avatar-upload-target="fileInfo">
51
+ <span data-avatar-upload-target="fileName"></span>
52
+ <span data-avatar-upload-target="fileSize"></span>
53
+ <button type="button"
54
+ class="text-red-600 hover:text-red-500 dark:text-red-400 dark:hover:text-red-300 font-medium"
55
+ data-action="avatar-upload#remove">
56
+ Remove
57
+ </button>
58
+ </div>
29
59
  </div>
30
- <% if user.avatar.attached? %>
31
- <p class="mt-2 text-xs text-gray-600 dark:text-gray-400">
32
- Current: <%= user.avatar.filename %> (<%= number_to_human_size(user.avatar.byte_size) %>)
33
- </p>
34
- <% end %>
35
- </div>
36
60
 
37
- <div class="field">
38
- <%= f.label :name %>
39
61
  <%= f.text_field :name %>
40
- </div>
41
-
42
- <div class="field">
43
- <%= f.label :email %>
44
62
  <%= f.email_field :email %>
45
- </div>
46
-
47
- <div class="field">
48
- <%= f.label :current_theme, "Theme" %>
49
63
  <%= f.select :current_theme,
50
64
  options_for_select(Panda::Core.config.available_themes || [["Default", "default"], ["Sky", "sky"]], user.current_theme),
51
- {},
65
+ { label: "Theme" },
52
66
  data: { action: "change->theme-form#updateTheme" } %>
53
67
  </div>
54
- </div>
68
+ <% end %>
55
69
 
56
- <%= f.submit "Update Profile",
57
- class: "btn btn-primary mt-4",
58
- data: { disable_with: "Saving..." } %>
70
+ <%= render Panda::Core::Admin::FormFooterComponent.new(submit_text: "Update Profile") %>
59
71
  <% end %>
60
72
  <% end %>
@@ -0,0 +1,72 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do %>
2
+ <%= render Panda::Core::Admin::HeadingComponent.new(level: 1, text: "Login & Security") %>
3
+
4
+ <!-- Connected Accounts -->
5
+ <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
6
+ <% panel.with_heading_slot do %>
7
+ <div class="flex flex-col">
8
+ <span>Connected Accounts</span>
9
+ <span class="text-sm font-normal text-white/70">Manage your OAuth authentication providers</span>
10
+ </div>
11
+ <% end %>
12
+ <% panel.with_body_slot do %>
13
+ <div class="space-y-4">
14
+ <% enabled_providers = Panda::Core.config.authentication_providers %>
15
+ <% if enabled_providers.empty? %>
16
+ <p class="text-sm text-gray-500">No authentication providers are currently enabled.</p>
17
+ <% else %>
18
+ <% enabled_providers.each do |provider_name, _config| %>
19
+ <%= render Panda::Core::Admin::MyProfile::ConnectedAccountComponent.new(
20
+ provider: provider_name,
21
+ user: user
22
+ ) %>
23
+ <% end %>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
27
+ <% end %>
28
+
29
+ <!-- Login History -->
30
+ <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
31
+ <% panel.with_heading_slot do %>
32
+ <div class="flex flex-col">
33
+ <span>Login History</span>
34
+ <span class="text-sm font-normal text-white/70">Recent login activity for your account</span>
35
+ </div>
36
+ <% end %>
37
+ <% panel.with_body_slot do %>
38
+ <div class="space-y-4">
39
+ <p class="text-sm text-gray-500">Login history tracking will be implemented in a future release.</p>
40
+ <!-- Future implementation:
41
+ - Table showing date/time, location, IP address, device/browser
42
+ - Filter/search capabilities
43
+ - Export functionality
44
+ -->
45
+ </div>
46
+ <% end %>
47
+ <% end %>
48
+
49
+ <!-- Two-Factor Authentication (Future) -->
50
+ <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
51
+ <% panel.with_heading_slot do %>
52
+ <div class="flex flex-col">
53
+ <span>Two-Factor Authentication</span>
54
+ <span class="text-sm font-normal text-white/70">Add an extra layer of security to your account</span>
55
+ </div>
56
+ <% end %>
57
+ <% panel.with_body_slot do %>
58
+ <div class="space-y-4">
59
+ <p class="text-sm text-gray-500">
60
+ Two-factor authentication will be available in a future release.
61
+ Track progress in <a href="https://github.com/tastybamboo/panda-core/issues/34" class="text-blue-600 hover:text-blue-800 underline" target="_blank">GitHub Issue #34</a>.
62
+ </p>
63
+ <!-- Future implementation:
64
+ - TOTP setup with QR code
65
+ - Backup codes generation
66
+ - SMS fallback option (optional)
67
+ - Recovery options
68
+ -->
69
+ </div>
70
+ <% end %>
71
+ <% end %>
72
+ <% end %>
@@ -1,37 +1 @@
1
- <div class="flex flex-col justify-center py-12 px-6 min-h-full text-center lg:px-8">
2
- <div class="text-center sm:mx-auto sm:w-full sm:max-w-sm">
3
- <% if Panda::Core.config.login_logo_path %>
4
- <img src="<%= Panda::Core.config.login_logo_path %>" class="py-2 mx-auto w-auto h-32">
5
- <% end %>
6
- <h2 class="mt-10 mb-6 text-2xl font-bold text-center text-white">
7
- <%= Panda::Core.config.login_page_title || "Sign in to your account" %>
8
- </h2>
9
- </div>
10
- <% if @providers&.any? %>
11
- <% @providers.each do |provider| %>
12
- <% provider_config = Panda::Core.config.authentication_providers[provider] %>
13
- <% provider_path = provider_config&.dig(:path_name) || provider %>
14
- <div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
15
- <%= form_tag "#{Panda::Core.config.admin_path}/auth/#{provider_path}", method: "post", data: {turbo: false} do %>
16
- <button type="submit" id="button-sign-in-<%= provider_path %>" class="inline-flex gap-x-2 items-center py-2.5 px-3.5 mx-auto mb-4 bg-white text-gray-900 rounded-md border min-w-56 border-neutral-400 hover:bg-gray-50">
17
- <i class="<%= oauth_provider_non_brand?(provider) ? 'fa-solid' : 'fa-brands' %> fa-<%= oauth_provider_icon(provider) %> text-xl mr-1"></i>
18
- Sign in with <%= oauth_provider_name(provider, provider_config) %>
19
- </button>
20
- <% end %>
21
- </div>
22
- <% end %>
23
- <% else %>
24
- <div class="rounded-md bg-yellow-50 p-4 sm:mx-auto sm:w-full sm:max-w-sm">
25
- <div class="flex">
26
- <div class="ml-3">
27
- <h3 class="text-sm font-medium text-yellow-800">
28
- No authentication providers configured
29
- </h3>
30
- <div class="mt-2 text-sm text-yellow-700">
31
- <p>Please configure at least one authentication provider in your Panda Core configuration.</p>
32
- </div>
33
- </div>
34
- </div>
35
- </div>
36
- <% end %>
37
- </div>
1
+ <%= render Panda::Core::Admin::LoginFormComponent.new(providers: @providers) %>
@@ -1,18 +1 @@
1
- <% breadcrumb_styles = "text-black/60 hover:text-black/80" %>
2
-
3
- <nav aria-label="Breadcrumb" id="panda-breadcrumbs" class="px-4 w-full sm:px-6 py-2.5">
4
- <ol role="list">
5
- <li class="inline-block">
6
- <a href="<%= Panda::Core.config.admin_path %>" class="<%= breadcrumb_styles %>">
7
- <i class="fa fa-solid fa-house fa-fw inline-block w-4 text-center"></i>
8
- <span class="sr-only">Home</span>
9
- </a>
10
- </li>
11
- <% breadcrumbs.each do |crumb| %>
12
- <li class="inline-block">
13
- <i class="fa-solid fa-chevron-right fa-fw inline-block w-4 text-center font-light <%= breadcrumb_styles %> px-2"></i>
14
- <a href="<%= crumb.path %>" class="text-sm font-normal <%= breadcrumb_styles %>"><%= crumb.name %></a>
15
- </li>
16
- <% end %>
17
- </ol>
18
- </nav>
1
+ <%= render Panda::Core::Admin::BreadcrumbsComponent.new(breadcrumbs: breadcrumbs) %>
@@ -1,99 +1 @@
1
- <nav class="flex flex-col flex-1">
2
- <ul role="list" class="flex flex-col flex-1">
3
- <a class="block p-0 mt-4 mb-4 ml-2 text-xl font-medium text-white"><%= Panda::Core.config.admin_title || "Panda Admin" %></a>
4
- <%
5
- # Get navigation items in original order for display
6
- nav_items = Panda::Core.config.admin_navigation_items&.call(current_user) || []
7
-
8
- # Helper to recursively collect all paths from nav items
9
- def collect_paths(items)
10
- items.flat_map do |item|
11
- paths = item[:path] ? [item[:path]] : []
12
- paths += collect_paths(item[:children] || [])
13
- paths
14
- end
15
- end
16
-
17
- # Find the most specific matching path by checking longest paths first
18
- all_paths = collect_paths(nav_items)
19
- active_path = all_paths
20
- .sort_by { |path| -path.length }
21
- .find { |path| request.path == path || request.path.starts_with?(path + "/") }
22
- %>
23
- <% nav_items.each_with_index do |item, index| %>
24
- <li>
25
- <%
26
- has_children = item[:children].present?
27
-
28
- # For items with children, check if any child is active
29
- if has_children
30
- is_active = item[:children].any? do |child|
31
- child[:path] && (request.path == child[:path] || request.path.starts_with?(child[:path] + "/"))
32
- end
33
- else
34
- is_active = item[:path] == active_path
35
- end
36
- %>
37
-
38
- <% if has_children %>
39
- <div class="space-y-1" data-controller="navigation-toggle">
40
- <button type="button"
41
- data-navigation-toggle-target="button"
42
- data-action="click->navigation-toggle#toggle"
43
- aria-controls="sub-menu-<%= index %>"
44
- aria-expanded="false"
45
- class="<%= is_active ? 'bg-primary-500 text-white' : 'text-white hover:bg-primary-500/60' %> transition-all group flex items-center w-full gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal">
46
- <span class="text-center w-6"><i class="<%= item[:icon] %> text-xl fa-fw"></i></span>
47
- <span class="flex-1 text-left"><%= item[:label] %></span>
48
- <i class="fa-solid fa-chevron-right text-xs transition-transform duration-150 ease-in-out"
49
- data-navigation-toggle-target="icon"></i>
50
- </button>
51
- <div id="sub-menu-<%= index %>"
52
- class="space-y-1 hidden"
53
- style="display: none;"
54
- data-navigation-toggle-target="menu">
55
- <% item[:children].each do |child| %>
56
- <%
57
- child_is_active = child[:path] && (request.path == child[:path] || request.path.starts_with?(child[:path] + "/"))
58
- %>
59
- <%= link_to child[:path], class: "#{child_is_active ? 'bg-primary-500 text-white' : 'text-white hover:bg-primary-500/60'} group flex items-center w-full py-2 pr-2 pl-11 rounded-md text-sm font-normal transition-all" do %>
60
- <%= child[:label] %>
61
- <% end %>
62
- <% end %>
63
- </div>
64
- </div>
65
- <% else %>
66
- <%= link_to item[:path], class: "#{is_active ? "bg-primary-500 text-white relative flex items-center transition-all py-3 px-2 mb-2 rounded-md group gap-x-3 text-base leading-6 font-normal" : "text-white hover:bg-primary-500/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal"}" do %>
67
- <span class="text-center w-6"><i class="<%= item[:icon] %> text-xl fa-fw"></i></span>
68
- <span><%= item[:label] %></span>
69
- <% end %>
70
- <% end %>
71
- </li>
72
- <% end %>
73
- <li>
74
- <%= button_to panda_core.admin_logout_path, method: :delete, id: "logout-link", data: { turbo: false }, class: "text-white hover:bg-primary-500/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal w-full" do %>
75
- <span class="text-center w-6"><i class="text-xl fa-solid fa-door-open fa-fw"></i></span>
76
- <span>Logout</span>
77
- <% end %>
78
- </li>
79
- <li class="mt-auto">
80
- <%
81
- # Check if we're in the my_profile section
82
- is_my_profile_active = request.path.starts_with?("#{Panda::Core.config.admin_path}/my_profile")
83
- %>
84
- <%= link_to panda_core.admin_my_profile_path, class: "#{is_my_profile_active ? 'bg-primary-500 text-white' : 'text-white hover:bg-primary-500/60'} transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal w-full", title: "My Profile" do %>
85
- <% if current_user.avatar.attached? %>
86
- <span class="text-center w-6"><%= image_tag main_app.url_for(current_user.avatar), alt: current_user.name, class: "w-auto h-7 rounded-full object-cover" %></span>
87
- <% elsif !current_user.image_url.to_s.empty? %>
88
- <span class="text-center w-6"><img src="<%= current_user.image_url %>" class="w-auto h-7 rounded-full"></span>
89
- <% else %>
90
- <span class="text-center w-6"><i class="text-xl fa-solid fa-circle-user fa-fw"></i></span>
91
- <% end %>
92
- <span><%= current_user.name %></span>
93
- <% end %>
94
- </li>
95
- <li class="px-2 py-3">
96
- <span class="text-xs text-white">Panda Core v<%= Panda::Core::VERSION %></span>
97
- </li>
98
- </ul>
99
- </nav>
1
+ <%= render Panda::Core::Admin::SidebarComponent.new(user: current_user) %>
@@ -3,9 +3,9 @@
3
3
  <div class="fixed inset-0 flex">
4
4
  <div class="relative flex w-full max-w-xs flex-1 flex-col bg-gray-800">
5
5
  <div class="absolute top-0 right-0 -mr-12 pt-2">
6
- <button type="button" class="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
7
- <span class="sr-only">Close sidebar</span>
8
- <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
6
+ <button type="button" class="ml-1 flex items-center gap-1 rounded-full bg-white/10 px-2 py-1 text-xs text-white/80 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
7
+ <span>Close</span>
8
+ <svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
9
9
  <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
10
10
  </svg>
11
11
  </button>
@@ -0,0 +1,21 @@
1
+ <%= render Panda::Core::Admin::PanelComponent.new do %>
2
+ <%= panda_form_with(model: tag, url: tag.persisted? ? admin_tag_path(tag) : admin_tags_path) do |f| %>
3
+ <div class="space-y-4">
4
+ <%= f.text_field :name, label: "Name" %>
5
+
6
+ <div>
7
+ <%= f.label :colour, "Colour" %>
8
+ <div class="flex items-center gap-3 mt-1">
9
+ <input type="color" name="tag[colour]" value="<%= tag.colour || '#6b7280' %>"
10
+ class="h-10 w-14 rounded border border-gray-300 cursor-pointer">
11
+ <span class="text-sm text-gray-500">Choose a colour for the tag badge</span>
12
+ </div>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="mt-6 flex items-center gap-3">
17
+ <%= f.submit tag.persisted? ? "Update Tag" : "Create Tag", class: "inline-flex items-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90 transition cursor-pointer" %>
18
+ <%= link_to "Cancel", admin_tags_path, class: "text-sm text-gray-500 hover:text-gray-700" %>
19
+ </div>
20
+ <% end %>
21
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "Edit Tag", level: 1) %>
3
+ <%= render "form", tag: @tag %>
4
+ <% end %>
@@ -0,0 +1,41 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "Tags", level: 1) do %>
3
+ <%= link_to new_admin_tag_path, class: "inline-flex items-center gap-1.5 rounded-lg bg-primary px-3.5 py-2 text-sm font-medium text-white hover:bg-primary/90 transition" do %>
4
+ <i class="fa-solid fa-plus"></i> New Tag
5
+ <% end %>
6
+ <% end %>
7
+
8
+ <% if @tags.any? %>
9
+ <%= render Panda::Core::Admin::TableComponent.new(term: "tag", rows: @tags, icon: "fa-solid fa-tag") do |table| %>
10
+ <% table.column("Tag", width: "40%") do |tag| %>
11
+ <div class="flex items-center gap-2">
12
+ <span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: <%= tag.display_colour %>"></span>
13
+ <span class="font-medium"><%= tag.name %></span>
14
+ </div>
15
+ <% end %>
16
+
17
+ <% table.column("Used", width: "20%") do |tag| %>
18
+ <span class="text-sm text-gray-500"><%= pluralize(tag.taggings_count, "record") %></span>
19
+ <% end %>
20
+
21
+ <% table.column("", width: "40%") do |tag| %>
22
+ <div class="flex justify-end gap-2">
23
+ <%= link_to edit_admin_tag_path(tag), class: "inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700" do %>
24
+ <i class="fa-solid fa-pen-to-square"></i> Edit
25
+ <% end %>
26
+ <%= button_to admin_tag_path(tag), method: :delete, class: "inline-flex items-center gap-1 text-sm text-red-500 hover:text-red-700", data: {turbo_confirm: "Delete tag \"#{tag.name}\"?"} do %>
27
+ <i class="fa-solid fa-trash"></i> Delete
28
+ <% end %>
29
+ </div>
30
+ <% end %>
31
+ <% end %>
32
+ <% else %>
33
+ <%= render Panda::Core::Admin::EmptyStateComponent.new(
34
+ title: "No tags yet",
35
+ description: "Tags help you organise and filter your records.",
36
+ icon: "fa-solid fa-tag",
37
+ action_text: "Create your first tag",
38
+ action_path: new_admin_tag_path
39
+ ) %>
40
+ <% end %>
41
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "New Tag", level: 1) %>
3
+ <%= render "form", tag: @tag %>
4
+ <% end %>