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,165 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input", "preview", "dropzone", "fileInfo", "removeButton"]
5
+
6
+ connect() {
7
+ // Setup drag and drop handlers
8
+ if (this.hasDropzoneTarget) {
9
+ this.setupDragAndDrop()
10
+ }
11
+ }
12
+
13
+ setupDragAndDrop() {
14
+ const dropzone = this.dropzoneTarget
15
+
16
+ // Prevent default drag behaviors
17
+ ;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
18
+ dropzone.addEventListener(eventName, this.preventDefaults.bind(this), false)
19
+ document.body.addEventListener(eventName, this.preventDefaults.bind(this), false)
20
+ })
21
+
22
+ // Highlight drop zone when item is dragged over it
23
+ ;['dragenter', 'dragover'].forEach(eventName => {
24
+ dropzone.addEventListener(eventName, this.highlight.bind(this), false)
25
+ })
26
+
27
+ ;['dragleave', 'drop'].forEach(eventName => {
28
+ dropzone.addEventListener(eventName, this.unhighlight.bind(this), false)
29
+ })
30
+
31
+ // Handle dropped files
32
+ dropzone.addEventListener('drop', this.handleDrop.bind(this), false)
33
+ }
34
+
35
+ preventDefaults(e) {
36
+ e.preventDefault()
37
+ e.stopPropagation()
38
+ }
39
+
40
+ highlight(e) {
41
+ this.dropzoneTarget.classList.add('border-primary-600', 'bg-primary-50')
42
+ }
43
+
44
+ unhighlight(e) {
45
+ this.dropzoneTarget.classList.remove('border-primary-600', 'bg-primary-50')
46
+ }
47
+
48
+ handleDrop(e) {
49
+ const dt = e.dataTransfer
50
+ const files = dt.files
51
+
52
+ if (files.length > 0) {
53
+ // Update the file input with the dropped file
54
+ this.inputTarget.files = files
55
+ this.handleFileSelect()
56
+ }
57
+ }
58
+
59
+ // Triggered when user selects file via input or drag-drop
60
+ handleFileSelect() {
61
+ const file = this.inputTarget.files[0]
62
+
63
+ if (!file) {
64
+ return
65
+ }
66
+
67
+ // Show file preview if it's an image
68
+ if (file.type.startsWith('image/')) {
69
+ this.showImagePreview(file)
70
+ } else {
71
+ this.showFileInfo(file)
72
+ }
73
+ }
74
+
75
+ showImagePreview(file) {
76
+ const reader = new FileReader()
77
+
78
+ reader.onload = (e) => {
79
+ if (this.hasPreviewTarget) {
80
+ // Create or update preview image
81
+ const existingImage = this.previewTarget.querySelector('img')
82
+ if (existingImage) {
83
+ existingImage.src = e.target.result
84
+ } else {
85
+ const img = document.createElement('img')
86
+ img.src = e.target.result
87
+ img.className = 'max-w-xs rounded border border-gray-300 dark:border-gray-600'
88
+ this.previewTarget.innerHTML = ''
89
+ this.previewTarget.appendChild(img)
90
+ }
91
+ this.previewTarget.classList.remove('hidden')
92
+ }
93
+
94
+ // Show file info with remove button
95
+ this.showFileInfo(file, true)
96
+ }
97
+
98
+ reader.readAsDataURL(file)
99
+ }
100
+
101
+ showFileInfo(file, withPreview = false) {
102
+ if (!this.hasFileInfoTarget) {
103
+ return
104
+ }
105
+
106
+ const fileSize = this.humanFileSize(file.size)
107
+ const fileName = file.name
108
+
109
+ this.fileInfoTarget.innerHTML = `
110
+ <div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
111
+ <div class="flex items-center gap-x-3 flex-1 min-w-0">
112
+ <svg class="size-8 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
113
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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" />
114
+ </svg>
115
+ <div class="flex-1 min-w-0">
116
+ <p class="text-sm font-medium text-gray-900 dark:text-white truncate">${fileName}</p>
117
+ <p class="text-xs text-gray-500 dark:text-gray-400">${fileSize}</p>
118
+ </div>
119
+ </div>
120
+ <button type="button"
121
+ data-action="click->file-upload#removeFile"
122
+ class="flex-shrink-0 ml-3 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
123
+ <svg class="size-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
124
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
125
+ </svg>
126
+ </button>
127
+ </div>
128
+ `
129
+ this.fileInfoTarget.classList.remove('hidden')
130
+
131
+ // Hide the dropzone upload area
132
+ if (this.hasDropzoneTarget) {
133
+ this.dropzoneTarget.classList.add('hidden')
134
+ }
135
+ }
136
+
137
+ removeFile() {
138
+ // Clear the file input
139
+ this.inputTarget.value = ''
140
+
141
+ // Hide preview and file info
142
+ if (this.hasPreviewTarget) {
143
+ this.previewTarget.classList.add('hidden')
144
+ this.previewTarget.innerHTML = ''
145
+ }
146
+
147
+ if (this.hasFileInfoTarget) {
148
+ this.fileInfoTarget.classList.add('hidden')
149
+ this.fileInfoTarget.innerHTML = ''
150
+ }
151
+
152
+ // Show the dropzone again
153
+ if (this.hasDropzoneTarget) {
154
+ this.dropzoneTarget.classList.remove('hidden')
155
+ }
156
+ }
157
+
158
+ humanFileSize(bytes) {
159
+ if (bytes === 0) return '0 Bytes'
160
+ const k = 1024
161
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
162
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
163
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
164
+ }
165
+ }
@@ -0,0 +1,184 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input", "results", "group"]
5
+ static values = {
6
+ url: String,
7
+ open: { type: Boolean, default: false },
8
+ index: { type: Number, default: -1 }
9
+ }
10
+
11
+ connect() {
12
+ this.debounceTimer = null
13
+ this.handleOutsideClick = this.handleOutsideClick.bind(this)
14
+ this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this)
15
+ document.addEventListener("click", this.handleOutsideClick)
16
+ document.addEventListener("keydown", this.handleGlobalKeydown)
17
+ }
18
+
19
+ disconnect() {
20
+ document.removeEventListener("click", this.handleOutsideClick)
21
+ document.removeEventListener("keydown", this.handleGlobalKeydown)
22
+ if (this.debounceTimer) clearTimeout(this.debounceTimer)
23
+ }
24
+
25
+ // Cmd+K / Ctrl+K to focus search
26
+ handleGlobalKeydown(event) {
27
+ if ((event.metaKey || event.ctrlKey) && event.key === "k") {
28
+ event.preventDefault()
29
+ this.inputTarget.focus()
30
+ this.inputTarget.select()
31
+ }
32
+ }
33
+
34
+ onInput() {
35
+ const query = this.inputTarget.value.trim()
36
+
37
+ if (this.debounceTimer) clearTimeout(this.debounceTimer)
38
+
39
+ if (query.length < 2) {
40
+ this.close()
41
+ return
42
+ }
43
+
44
+ this.debounceTimer = setTimeout(() => this.search(query), 200)
45
+ }
46
+
47
+ async search(query) {
48
+ try {
49
+ const url = `${this.urlValue}?q=${encodeURIComponent(query)}`
50
+ const response = await fetch(url, {
51
+ headers: { "Accept": "application/json" }
52
+ })
53
+
54
+ if (!response.ok) return
55
+
56
+ const data = await response.json()
57
+ this.renderResults(data.groups)
58
+ } catch (e) {
59
+ console.warn("[panda-core] Search failed:", e)
60
+ }
61
+ }
62
+
63
+ renderResults(groups) {
64
+ const container = this.resultsTarget
65
+
66
+ if (!groups || groups.length === 0) {
67
+ container.innerHTML = `
68
+ <div class="px-4 py-6 text-center text-sm text-white/80">
69
+ No results found
70
+ </div>
71
+ `
72
+ this.open()
73
+ return
74
+ }
75
+
76
+ let html = ""
77
+ groups.forEach(group => {
78
+ const icon = group.icon ? `<i class="${this.escapeAttr(group.icon)} mr-2.5"></i>` : ""
79
+ html += `<div class="px-3 pt-3 pb-1"><span class="text-xs font-semibold text-white/40 uppercase tracking-wider">${icon}${this.escapeHtml(group.name)}</span></div>`
80
+
81
+ group.results.forEach(result => {
82
+ const desc = result.description
83
+ ? `<span class="text-xs text-white/40 truncate">${this.escapeHtml(result.description)}</span>`
84
+ : ""
85
+ html += `
86
+ <a href="${this.escapeAttr(result.href)}" class="search-result flex flex-col gap-0.5 px-3 py-2 rounded-lg hover:bg-white/15 transition-colors cursor-pointer" data-action="keydown->global-search#onResultKeydown">
87
+ <span class="text-sm text-white/90 truncate">${this.escapeHtml(result.name)}</span>
88
+ ${desc}
89
+ </a>
90
+ `
91
+ })
92
+ })
93
+
94
+ container.innerHTML = html
95
+ this.indexValue = -1
96
+ this.open()
97
+ }
98
+
99
+ open() {
100
+ this.resultsTarget.classList.remove("hidden")
101
+ this.openValue = true
102
+ }
103
+
104
+ close() {
105
+ this.resultsTarget.classList.add("hidden")
106
+ this.resultsTarget.innerHTML = ""
107
+ this.openValue = false
108
+ this.indexValue = -1
109
+ }
110
+
111
+ onInputKeydown(event) {
112
+ if (!this.openValue) return
113
+
114
+ const results = this.resultsTarget.querySelectorAll(".search-result")
115
+ if (results.length === 0) return
116
+
117
+ switch (event.key) {
118
+ case "ArrowDown":
119
+ event.preventDefault()
120
+ this.indexValue = Math.min(this.indexValue + 1, results.length - 1)
121
+ this.highlightResult(results)
122
+ break
123
+ case "ArrowUp":
124
+ event.preventDefault()
125
+ this.indexValue = Math.max(this.indexValue - 1, 0)
126
+ this.highlightResult(results)
127
+ break
128
+ case "Enter":
129
+ event.preventDefault()
130
+ if (this.indexValue >= 0 && results[this.indexValue]) {
131
+ results[this.indexValue].click()
132
+ }
133
+ break
134
+ case "Escape":
135
+ event.preventDefault()
136
+ this.close()
137
+ this.inputTarget.blur()
138
+ break
139
+ }
140
+ }
141
+
142
+ onResultKeydown(event) {
143
+ if (event.key === "Escape") {
144
+ this.close()
145
+ this.inputTarget.focus()
146
+ }
147
+ }
148
+
149
+ highlightResult(results) {
150
+ results.forEach((el, i) => {
151
+ if (i === this.indexValue) {
152
+ el.classList.add("bg-white/10")
153
+ el.scrollIntoView({ block: "nearest" })
154
+ } else {
155
+ el.classList.remove("bg-white/10")
156
+ }
157
+ })
158
+ }
159
+
160
+ onFocus() {
161
+ // If there are already results rendered, show them
162
+ if (this.resultsTarget.innerHTML.trim() !== "") {
163
+ this.open()
164
+ }
165
+ }
166
+
167
+ handleOutsideClick(event) {
168
+ if (this.openValue && !this.element.contains(event.target)) {
169
+ this.close()
170
+ }
171
+ }
172
+
173
+ escapeHtml(text) {
174
+ if (!text) return ""
175
+ const div = document.createElement("div")
176
+ div.textContent = text
177
+ return div.innerHTML
178
+ }
179
+
180
+ escapeAttr(text) {
181
+ if (!text) return ""
182
+ return text.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
183
+ }
184
+ }
@@ -10,6 +10,42 @@ application.register("image-cropper", ImageCropperController)
10
10
  import NavigationToggleController from "./navigation_toggle_controller.js"
11
11
  application.register("navigation-toggle", NavigationToggleController)
12
12
 
13
+ import MobileSidebarController from "./mobile_sidebar_controller.js"
14
+ application.register("mobile-sidebar", MobileSidebarController)
15
+
16
+ import RowLinkController from "./row_link_controller.js"
17
+ application.register("row-link", RowLinkController)
18
+
19
+ import CollapsibleItemController from "./collapsible_item_controller.js"
20
+ application.register("collapsible-item", CollapsibleItemController)
21
+
22
+ import CustomSelectController from "./custom_select_controller.js"
23
+ application.register("custom-select", CustomSelectController)
24
+
25
+ import ClipboardController from "./clipboard_controller.js"
26
+ application.register("clipboard", ClipboardController)
27
+
28
+ import AvatarUploadController from "./avatar_upload_controller.js"
29
+ application.register("avatar-upload", AvatarUploadController)
30
+
31
+ import GlobalSearchController from "./global_search_controller.js"
32
+ application.register("global-search", GlobalSearchController)
33
+
34
+ import TagInputController from "./tag_input_controller.js"
35
+ application.register("tag-input", TagInputController)
36
+
37
+ import DatepickerController from "./datepicker_controller.js"
38
+ application.register("datepicker", DatepickerController)
39
+
40
+ import DaterangeController from "./daterange_controller.js"
41
+ application.register("daterange", DaterangeController)
42
+
43
+ import FileGalleryController from "./file_gallery_controller.js"
44
+ application.register("file-gallery", FileGalleryController)
45
+
46
+ import FileUploadController from "./file_upload_controller.js"
47
+ application.register("file-upload", FileUploadController)
48
+
13
49
  // Import and register TailwindCSS Stimulus Components
14
50
  // These are needed for UI components like slideover, modals, alerts, etc.
15
51
  import { Alert, Autosave, ColorPreview, Dropdown, Modal, Tabs, Popover, Toggle, Slideover } from "../tailwindcss-stimulus-components.js"
@@ -0,0 +1,58 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["sidebar", "backdrop", "hamburger"]
5
+
6
+ connect() {
7
+ this._isOpen = false
8
+ this._mediaQuery = window.matchMedia("(min-width: 1024px)")
9
+ this._handleBreakpoint = this._handleBreakpoint.bind(this)
10
+ this._mediaQuery.addEventListener("change", this._handleBreakpoint)
11
+ }
12
+
13
+ disconnect() {
14
+ this._mediaQuery.removeEventListener("change", this._handleBreakpoint)
15
+ }
16
+
17
+ toggle() {
18
+ if (this._isOpen) {
19
+ this.close()
20
+ } else {
21
+ this.open()
22
+ }
23
+ }
24
+
25
+ open() {
26
+ this._isOpen = true
27
+ this._enableTransition()
28
+ this.sidebarTarget.classList.remove("max-h-16")
29
+ this.sidebarTarget.classList.add("max-h-screen")
30
+ this.backdropTarget.classList.remove("hidden")
31
+ this.hamburgerTarget.setAttribute("aria-expanded", "true")
32
+ }
33
+
34
+ close() {
35
+ this._isOpen = false
36
+ this._enableTransition()
37
+ this.sidebarTarget.classList.remove("max-h-screen")
38
+ this.sidebarTarget.classList.add("max-h-16")
39
+ this.backdropTarget.classList.add("hidden")
40
+ this.hamburgerTarget.setAttribute("aria-expanded", "false")
41
+ }
42
+
43
+ // Apply transition only during active mobile toggles to prevent
44
+ // Turbo page visits from animating the sidebar height on desktop.
45
+ _enableTransition() {
46
+ const el = this.sidebarTarget
47
+ el.classList.add("transition-all", "ease-in-out", "duration-300")
48
+ el.addEventListener("transitionend", () => {
49
+ el.classList.remove("transition-all", "ease-in-out", "duration-300")
50
+ }, { once: true })
51
+ }
52
+
53
+ _handleBreakpoint(event) {
54
+ if (event.matches && this._isOpen) {
55
+ this.close()
56
+ }
57
+ }
58
+ }
@@ -1,33 +1,34 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
- // Navigation toggle controller for expandable menu items
4
- // Usage:
5
- // <div data-controller="navigation-toggle">
6
- // <button data-navigation-toggle-target="button"
7
- // data-action="click->navigation-toggle#toggle"
8
- // aria-controls="sub-menu-1"
9
- // aria-expanded="false">
10
- // <span>Menu Item</span>
11
- // <i data-navigation-toggle-target="icon" class="fa-solid fa-chevron-right"></i>
12
- // </button>
13
- // <div id="sub-menu-1" data-navigation-toggle-target="menu" class="hidden">
14
- // <a href="#">Sub Item 1</a>
15
- // <a href="#">Sub Item 2</a>
16
- // </div>
17
- // </div>
18
3
  export default class extends Controller {
19
- static targets = ["button", "menu", "icon"]
4
+ static targets = ["button", "menu", "icon", "wrapper"]
5
+
6
+ // Classes toggled on the button when expanding/collapsing
7
+ static expandedButtonClasses = ["rounded-t-xl", "bg-white/15", "text-white"]
8
+ static collapsedButtonClasses = ["rounded-xl", "hover:bg-white/5", "text-white/80"]
20
9
 
21
10
  connect() {
22
- // Ensure menu starts in correct state
23
- // Check if this menu should be expanded by default (if a child is active)
24
- const hasActiveChild = this.menuTarget.querySelector(".bg-primary-500")
11
+ // Suppress transitions on connect to prevent visual jump during Turbo navigation
12
+ const menu = this.menuTarget
13
+ menu.classList.remove("transition-all", "duration-200", "ease-out")
14
+
15
+ const hasActiveChild = menu.querySelector('[class*="bg-primary-500"]')
25
16
  if (hasActiveChild) {
26
17
  this.expand()
27
18
  } else {
28
- // Explicitly ensure the menu is collapsed if no active child
29
19
  this.collapse()
30
20
  }
21
+
22
+ // Re-enable transitions for user interactions; store RAF ID so disconnect() can cancel it
23
+ this._rafId = requestAnimationFrame(() => {
24
+ menu.classList.add("transition-all", "duration-200", "ease-out")
25
+ })
26
+ }
27
+
28
+ disconnect() {
29
+ if (this._rafId) {
30
+ cancelAnimationFrame(this._rafId)
31
+ }
31
32
  }
32
33
 
33
34
  toggle(event) {
@@ -45,20 +46,44 @@ export default class extends Controller {
45
46
  }
46
47
 
47
48
  expand() {
48
- this.menuTarget.classList.remove("hidden")
49
- this.menuTarget.style.display = ""
49
+ // Menu animation
50
+ this.menuTarget.classList.remove("max-h-0", "opacity-0")
51
+ this.menuTarget.classList.add("max-h-96", "opacity-100")
52
+
50
53
  this.buttonTarget.setAttribute("aria-expanded", "true")
51
54
 
55
+ // Button styling: collapsed → expanded
56
+ this.constructor.collapsedButtonClasses.forEach(c => this.buttonTarget.classList.remove(c))
57
+ this.constructor.expandedButtonClasses.forEach(c => this.buttonTarget.classList.add(c))
58
+
59
+ // Wrapper background
60
+ if (this.hasWrapperTarget) {
61
+ this.wrapperTarget.classList.add("rounded-xl", "bg-white/10", "overflow-hidden")
62
+ }
63
+
64
+ // Chevron rotation
52
65
  if (this.hasIconTarget) {
53
66
  this.iconTarget.classList.add("rotate-90")
54
67
  }
55
68
  }
56
69
 
57
70
  collapse() {
58
- this.menuTarget.classList.add("hidden")
59
- this.menuTarget.style.display = "none"
71
+ // Menu animation
72
+ this.menuTarget.classList.remove("max-h-96", "opacity-100")
73
+ this.menuTarget.classList.add("max-h-0", "opacity-0")
74
+
60
75
  this.buttonTarget.setAttribute("aria-expanded", "false")
61
76
 
77
+ // Button styling: expanded → collapsed
78
+ this.constructor.expandedButtonClasses.forEach(c => this.buttonTarget.classList.remove(c))
79
+ this.constructor.collapsedButtonClasses.forEach(c => this.buttonTarget.classList.add(c))
80
+
81
+ // Wrapper background
82
+ if (this.hasWrapperTarget) {
83
+ this.wrapperTarget.classList.remove("rounded-xl", "bg-white/10", "overflow-hidden")
84
+ }
85
+
86
+ // Chevron rotation
62
87
  if (this.hasIconTarget) {
63
88
  this.iconTarget.classList.remove("rotate-90")
64
89
  }
@@ -0,0 +1,27 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Handles clickable table rows that contain a block-link anchor.
4
+ //
5
+ // Uses event delegation on the table container rather than CSS ::after
6
+ // pseudo-elements, which don't work reliably with display: table-row
7
+ // in Safari (position: relative on table-row is undefined per spec).
8
+ export default class extends Controller {
9
+ click(event) {
10
+ // Don't interfere with actual links (except block-link) or buttons
11
+ if (event.target.closest("a:not(.block-link), button, input")) return
12
+
13
+ const row = event.target.closest(".table-row")
14
+ if (!row) return
15
+
16
+ const link = row.querySelector("a.block-link")
17
+ if (!link) return
18
+
19
+ event.preventDefault()
20
+
21
+ if (typeof Turbo !== "undefined") {
22
+ Turbo.visit(link.href)
23
+ } else {
24
+ window.location.href = link.href
25
+ }
26
+ }
27
+ }