easy-admin-rails 0.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 (203) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/easy_admin.base.js +43505 -0
  6. data/app/assets/builds/easy_admin.base.js.map +7 -0
  7. data/app/assets/builds/easy_admin.css +6141 -0
  8. data/app/assets/config/easy_admin_manifest.js +1 -0
  9. data/app/assets/images/jsoneditor-icons.svg +749 -0
  10. data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
  11. data/app/components/easy_admin/base_component.rb +35 -0
  12. data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
  13. data/app/components/easy_admin/batch_action_form_component.rb +124 -0
  14. data/app/components/easy_admin/combined_filters_component.rb +232 -0
  15. data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
  16. data/app/components/easy_admin/context_menu_component.rb +161 -0
  17. data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
  18. data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
  19. data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
  20. data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
  21. data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
  22. data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
  23. data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
  24. data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
  25. data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
  26. data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
  27. data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
  28. data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
  29. data/app/components/easy_admin/dashboards/show_component.rb +163 -0
  30. data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
  31. data/app/components/easy_admin/date_picker_component.rb +188 -0
  32. data/app/components/easy_admin/fields/base_component.rb +101 -0
  33. data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
  34. data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
  35. data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
  36. data/app/components/easy_admin/fields/form/date_component.rb +55 -0
  37. data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
  38. data/app/components/easy_admin/fields/form/email_component.rb +55 -0
  39. data/app/components/easy_admin/fields/form/file_component.rb +190 -0
  40. data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
  41. data/app/components/easy_admin/fields/form/json_component.rb +81 -0
  42. data/app/components/easy_admin/fields/form/number_component.rb +55 -0
  43. data/app/components/easy_admin/fields/form/select_component.rb +326 -0
  44. data/app/components/easy_admin/fields/form/text_component.rb +55 -0
  45. data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
  46. data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
  47. data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
  48. data/app/components/easy_admin/fields/index/date_component.rb +13 -0
  49. data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
  50. data/app/components/easy_admin/fields/index/email_component.rb +24 -0
  51. data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
  52. data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
  53. data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
  54. data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
  55. data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
  56. data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
  57. data/app/components/easy_admin/fields/index/json_component.rb +23 -0
  58. data/app/components/easy_admin/fields/index/number_component.rb +20 -0
  59. data/app/components/easy_admin/fields/index/select_component.rb +25 -0
  60. data/app/components/easy_admin/fields/index/text_component.rb +20 -0
  61. data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
  62. data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
  63. data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
  64. data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
  65. data/app/components/easy_admin/fields/show/date_component.rb +13 -0
  66. data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
  67. data/app/components/easy_admin/fields/show/email_component.rb +19 -0
  68. data/app/components/easy_admin/fields/show/file_component.rb +304 -0
  69. data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
  70. data/app/components/easy_admin/fields/show/json_component.rb +45 -0
  71. data/app/components/easy_admin/fields/show/number_component.rb +20 -0
  72. data/app/components/easy_admin/fields/show/select_component.rb +25 -0
  73. data/app/components/easy_admin/fields/show/text_component.rb +17 -0
  74. data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
  75. data/app/components/easy_admin/filters_component.rb +120 -0
  76. data/app/components/easy_admin/form_tabs_component.rb +166 -0
  77. data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
  78. data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
  79. data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
  80. data/app/components/easy_admin/modal_frame_component.rb +26 -0
  81. data/app/components/easy_admin/navbar_component.rb +226 -0
  82. data/app/components/easy_admin/notification_component.rb +83 -0
  83. data/app/components/easy_admin/pagination_component.rb +188 -0
  84. data/app/components/easy_admin/quick_filters_component.rb +65 -0
  85. data/app/components/easy_admin/resource_pagination_component.rb +14 -0
  86. data/app/components/easy_admin/resources/index_component.rb +211 -0
  87. data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
  88. data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
  89. data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
  90. data/app/components/easy_admin/resources/table_component.rb +206 -0
  91. data/app/components/easy_admin/resources/table_row_component.rb +160 -0
  92. data/app/components/easy_admin/row_action_form_component.rb +127 -0
  93. data/app/components/easy_admin/scopes_component.rb +224 -0
  94. data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
  95. data/app/components/easy_admin/show_layout_component.rb +600 -0
  96. data/app/components/easy_admin/sidebar_component.rb +174 -0
  97. data/app/components/easy_admin/turbo/response_component.rb +40 -0
  98. data/app/components/easy_admin/turbo/stream_component.rb +28 -0
  99. data/app/controllers/easy_admin/application_controller.rb +66 -0
  100. data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
  101. data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
  102. data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
  103. data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
  104. data/app/controllers/easy_admin/passwords_controller.rb +15 -0
  105. data/app/controllers/easy_admin/registrations_controller.rb +52 -0
  106. data/app/controllers/easy_admin/resources_controller.rb +907 -0
  107. data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
  108. data/app/controllers/easy_admin/sessions_controller.rb +32 -0
  109. data/app/controllers/easy_admin/settings_controller.rb +94 -0
  110. data/app/helpers/easy_admin/application_helper.rb +4 -0
  111. data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
  112. data/app/helpers/easy_admin/fields_helper.rb +27 -0
  113. data/app/helpers/easy_admin/pagy_helper.rb +30 -0
  114. data/app/helpers/easy_admin/resources_helper.rb +39 -0
  115. data/app/javascript/easy_admin/application.js +12 -0
  116. data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
  117. data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
  118. data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
  119. data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
  120. data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
  121. data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
  122. data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
  123. data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
  124. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
  125. data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
  126. data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
  127. data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
  128. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
  129. data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
  130. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
  131. data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
  132. data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
  133. data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
  134. data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
  135. data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
  136. data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
  137. data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
  138. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
  139. data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
  140. data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
  141. data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
  142. data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
  143. data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
  144. data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
  145. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
  146. data/app/javascript/easy_admin/controllers.js +54 -0
  147. data/app/javascript/easy_admin.base.js +4 -0
  148. data/app/models/easy_admin/admin_user.rb +53 -0
  149. data/app/models/easy_admin/application_record.rb +5 -0
  150. data/app/views/easy_admin/dashboard/index.html.erb +3 -0
  151. data/app/views/easy_admin/dashboards/show.html.erb +7 -0
  152. data/app/views/easy_admin/passwords/edit.html.erb +42 -0
  153. data/app/views/easy_admin/passwords/new.html.erb +41 -0
  154. data/app/views/easy_admin/registrations/new.html.erb +65 -0
  155. data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
  156. data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
  157. data/app/views/easy_admin/resources/edit.html.erb +151 -0
  158. data/app/views/easy_admin/resources/index.html.erb +12 -0
  159. data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
  160. data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
  161. data/app/views/easy_admin/resources/new.html.erb +100 -0
  162. data/app/views/easy_admin/resources/show.html.erb +31 -0
  163. data/app/views/easy_admin/sessions/new.html.erb +55 -0
  164. data/app/views/easy_admin/settings/_form.html.erb +51 -0
  165. data/app/views/easy_admin/settings/index.html.erb +53 -0
  166. data/app/views/layouts/easy_admin/application.html.erb +48 -0
  167. data/app/views/layouts/easy_admin/auth.html.erb +34 -0
  168. data/config/initializers/easy_admin_card_factory.rb +27 -0
  169. data/config/initializers/pagy.rb +15 -0
  170. data/config/initializers/rack_mini_profiler.rb +67 -0
  171. data/config/routes.rb +70 -0
  172. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
  173. data/lib/easy-admin.rb +32 -0
  174. data/lib/easy_admin/action.rb +159 -0
  175. data/lib/easy_admin/batch_action.rb +134 -0
  176. data/lib/easy_admin/configuration.rb +75 -0
  177. data/lib/easy_admin/dashboard.rb +110 -0
  178. data/lib/easy_admin/dashboard_registry.rb +30 -0
  179. data/lib/easy_admin/delete_action.rb +22 -0
  180. data/lib/easy_admin/engine.rb +54 -0
  181. data/lib/easy_admin/field.rb +118 -0
  182. data/lib/easy_admin/resource.rb +806 -0
  183. data/lib/easy_admin/resource_registry.rb +22 -0
  184. data/lib/easy_admin/types/json_type.rb +25 -0
  185. data/lib/easy_admin/version.rb +3 -0
  186. data/lib/generators/easy_admin/auth_generator.rb +69 -0
  187. data/lib/generators/easy_admin/card/card_generator.rb +94 -0
  188. data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
  189. data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
  190. data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
  191. data/lib/generators/easy_admin/install_generator.rb +25 -0
  192. data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
  193. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
  194. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
  195. data/lib/generators/easy_admin/resource_generator.rb +43 -0
  196. data/lib/generators/easy_admin/templates/AUTH_README +35 -0
  197. data/lib/generators/easy_admin/templates/README +27 -0
  198. data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
  199. data/lib/generators/easy_admin/templates/devise.rb +267 -0
  200. data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
  201. data/lib/generators/easy_admin/templates/resource.rb +29 -0
  202. data/lib/tasks/easy_admin_tasks.rake +4 -0
  203. metadata +445 -0
@@ -0,0 +1,223 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { get, post } from "@rails/request.js"
3
+
4
+ export default class extends Controller {
5
+ static targets = ["checkbox", "actionBar", "actionBarContent", "counter", "selectAll", "selectedIds"]
6
+ static values = { resourceName: String }
7
+
8
+ connect() {
9
+ this.selectedIds = new Set()
10
+ this.updateUI()
11
+ }
12
+
13
+ toggleItem(event) {
14
+ const checkbox = event.target
15
+ const id = checkbox.value
16
+ const row = checkbox.closest('tr')
17
+
18
+ if (checkbox.checked) {
19
+ this.selectedIds.add(id)
20
+ // Add selected styling to the row
21
+ row?.classList.add('bg-blue-50', 'border-blue-200')
22
+ row?.classList.remove('hover:bg-gray-100')
23
+ } else {
24
+ this.selectedIds.delete(id)
25
+ // Remove selected styling from the row
26
+ row?.classList.remove('bg-blue-50', 'border-blue-200')
27
+ row?.classList.add('hover:bg-gray-100')
28
+ }
29
+
30
+ this.updateUI()
31
+ }
32
+
33
+ toggleAll(event) {
34
+ const checkAll = event.target.checked
35
+ const itemCheckboxes = this.checkboxTargets.filter(cb => !cb.dataset.selectAll)
36
+
37
+ itemCheckboxes.forEach(checkbox => {
38
+ checkbox.checked = checkAll
39
+ const row = checkbox.closest('tr')
40
+
41
+ if (checkAll) {
42
+ this.selectedIds.add(checkbox.value)
43
+ // Add selected styling to the row
44
+ row?.classList.add('bg-blue-50', 'border-blue-200')
45
+ row?.classList.remove('hover:bg-gray-100')
46
+ } else {
47
+ this.selectedIds.delete(checkbox.value)
48
+ // Remove selected styling from the row
49
+ row?.classList.remove('bg-blue-50', 'border-blue-200')
50
+ row?.classList.add('hover:bg-gray-100')
51
+ }
52
+ })
53
+
54
+ this.updateUI()
55
+ }
56
+
57
+ updateUI() {
58
+ const count = this.selectedIds.size
59
+
60
+ // Update counter
61
+ if (this.hasCounterTarget) {
62
+ this.counterTarget.textContent = count
63
+ }
64
+
65
+ // Show/hide action bar with iOS-style animation
66
+ if (this.hasActionBarContentTarget) {
67
+ if (count > 0) {
68
+ // Show the action bar with smooth animation
69
+ this.actionBarContentTarget.style.transform = 'translateY(0)'
70
+ this.actionBarContentTarget.style.opacity = '1'
71
+ } else {
72
+ // Hide with animation
73
+ this.actionBarContentTarget.style.transform = 'translateY(100%)'
74
+ this.actionBarContentTarget.style.opacity = '0'
75
+ }
76
+ }
77
+
78
+ // Update hidden input with selected IDs
79
+ if (this.hasSelectedIdsTarget) {
80
+ this.selectedIdsTarget.value = Array.from(this.selectedIds).join(',')
81
+ }
82
+
83
+ // Update select all checkbox state
84
+ if (this.hasSelectAllTarget) {
85
+ const itemCheckboxes = this.checkboxTargets.filter(cb => !cb.dataset.selectAll)
86
+ const allChecked = itemCheckboxes.length > 0 && itemCheckboxes.every(cb => cb.checked)
87
+ this.selectAllTarget.checked = allChecked
88
+ }
89
+ }
90
+
91
+ executeAction(event) {
92
+ const button = event.currentTarget
93
+ const actionClass = button.dataset.actionClass
94
+ const executionMode = button.dataset.executionMode
95
+
96
+ if (this.selectedIds.size === 0) {
97
+ alert('Please select at least one item')
98
+ return
99
+ }
100
+
101
+ if (executionMode === 'instant') {
102
+ this.executeInstantAction(actionClass, button)
103
+ } else if (executionMode === 'modal') {
104
+ this.openModalAction(actionClass)
105
+ }
106
+ }
107
+
108
+ async executeInstantAction(actionClass, button) {
109
+ // Always show confirmation for instant actions
110
+ const actionLabel = button.textContent.trim()
111
+ const selectedCount = this.selectedIds.size
112
+ const defaultMessage = `${selectedCount} selected item${selectedCount !== 1 ? 's' : ''}`
113
+ const confirmMessage = button.dataset.confirm || defaultMessage
114
+
115
+ // Determine if this is a dangerous action (delete, remove, etc.)
116
+ const isDangerous = actionLabel.toLowerCase().includes('delete') ||
117
+ actionLabel.toLowerCase().includes('remove') ||
118
+ actionLabel.toLowerCase().includes('destroy')
119
+
120
+ // Show confirmation modal via Turbo
121
+ const confirmed = await this.showConfirmationModal(actionLabel, confirmMessage, isDangerous)
122
+
123
+ if (!confirmed) {
124
+ return
125
+ }
126
+
127
+ // Show loading state
128
+ button.disabled = true
129
+ const originalText = button.textContent
130
+ button.textContent = 'Processing...'
131
+
132
+ // Prepare form data
133
+ const formData = new FormData()
134
+
135
+ // Add CSRF token
136
+ const csrfToken = document.querySelector('[name="csrf-token"]').content
137
+ formData.append('authenticity_token', csrfToken)
138
+
139
+ // Add action class and execution mode
140
+ formData.append('action_class', actionClass)
141
+ formData.append('execution_mode', 'instant')
142
+
143
+ // Add selected IDs
144
+ Array.from(this.selectedIds).forEach(id => {
145
+ formData.append('selected_ids[]', id)
146
+ })
147
+
148
+ // Submit via @rails/request.js
149
+ post(`/admin/${this.resourceNameValue}/batch_action`, {
150
+ body: formData,
151
+ responseKind: 'turbo-stream'
152
+ }).then(response => {
153
+ button.disabled = false
154
+ button.textContent = originalText
155
+
156
+ if (response.ok) {
157
+ // Dispatch event to clear selection
158
+ window.dispatchEvent(new CustomEvent('batch-action:completed'))
159
+ }
160
+ }).catch(error => {
161
+ button.disabled = false
162
+ button.textContent = originalText
163
+ console.error('Instant batch action failed:', error)
164
+ })
165
+ }
166
+
167
+ openModalAction(actionClass) {
168
+ // Open Turbo Frame modal for the batch action form
169
+ const url = `/admin/${this.resourceNameValue}/batch_action/${actionClass}/form`
170
+ const selectedIds = Array.from(this.selectedIds).join(',')
171
+
172
+ // Use @rails/request.js to load the modal
173
+ get(`${url}?selected_ids=${selectedIds}`, {
174
+ responseKind: 'turbo-stream'
175
+ })
176
+ }
177
+
178
+ clearSelection() {
179
+ this.selectedIds.clear()
180
+ this.checkboxTargets.forEach(cb => {
181
+ cb.checked = false
182
+ const row = cb.closest('tr')
183
+ // Remove selected styling from all rows
184
+ row?.classList.remove('bg-blue-50', 'border-blue-200')
185
+ row?.classList.add('hover:bg-gray-100')
186
+ })
187
+ this.updateUI()
188
+ }
189
+
190
+ async showConfirmationModal(title, message, isDangerous = false) {
191
+ return new Promise((resolve) => {
192
+ // Prepare the confirmation modal URL
193
+ const params = new URLSearchParams({
194
+ title: title,
195
+ message: message,
196
+ confirm_text: title,
197
+ cancel_text: 'Cancel',
198
+ danger: isDangerous
199
+ })
200
+
201
+ // Load the confirmation modal via Turbo
202
+ get(`/admin/confirmation_modal?${params.toString()}`, {
203
+ responseKind: 'turbo-stream'
204
+ })
205
+
206
+ // Listen for confirmation events
207
+ const handleConfirmed = () => {
208
+ document.removeEventListener('confirmation-modal:confirmed', handleConfirmed)
209
+ document.removeEventListener('confirmation-modal:cancelled', handleCancelled)
210
+ resolve(true)
211
+ }
212
+
213
+ const handleCancelled = () => {
214
+ document.removeEventListener('confirmation-modal:confirmed', handleConfirmed)
215
+ document.removeEventListener('confirmation-modal:cancelled', handleCancelled)
216
+ resolve(false)
217
+ }
218
+
219
+ document.addEventListener('confirmation-modal:confirmed', handleConfirmed)
220
+ document.addEventListener('confirmation-modal:cancelled', handleCancelled)
221
+ })
222
+ }
223
+ }
@@ -0,0 +1,216 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { Chart, registerables } from "chart.js"
3
+
4
+ Chart.register(...registerables)
5
+
6
+ // iOS-style color palette
7
+ const iOSColors = {
8
+ blue: "#007AFF",
9
+ green: "#34C759",
10
+ red: "#FF3B30",
11
+ orange: "#FF9500",
12
+ yellow: "#FFCC00",
13
+ purple: "#AF52DE",
14
+ pink: "#FF2D92",
15
+ teal: "#5AC8FA",
16
+ indigo: "#5856D6",
17
+ gray: "#8E8E93"
18
+ }
19
+
20
+ const chartColors = [
21
+ iOSColors.blue,
22
+ iOSColors.green,
23
+ iOSColors.orange,
24
+ iOSColors.purple,
25
+ iOSColors.teal,
26
+ iOSColors.pink,
27
+ iOSColors.indigo,
28
+ iOSColors.yellow
29
+ ]
30
+
31
+ export default class extends Controller {
32
+ static values = {
33
+ type: String,
34
+ data: Object,
35
+ options: Object
36
+ }
37
+
38
+ connect() {
39
+ this.initializeChart()
40
+ }
41
+
42
+ disconnect() {
43
+ if (this.chart) {
44
+ this.chart.destroy()
45
+ }
46
+ }
47
+
48
+ initializeChart() {
49
+ const ctx = this.element.querySelector('canvas')
50
+ if (!ctx) return
51
+
52
+ const chartData = this.prepareData()
53
+ const chartOptions = this.prepareOptions()
54
+
55
+ this.chart = new Chart(ctx, {
56
+ type: this.getChartJSType(),
57
+ data: chartData,
58
+ options: chartOptions
59
+ })
60
+ }
61
+
62
+ // Map dashboard chart types to Chart.js types
63
+ getChartJSType() {
64
+ const typeMapping = {
65
+ 'horizontal_bar': 'bar',
66
+ 'area': 'line',
67
+ 'donut': 'doughnut',
68
+ 'polar_area': 'polarArea'
69
+ }
70
+
71
+ return typeMapping[this.typeValue] || this.typeValue || 'line'
72
+ }
73
+
74
+ prepareData() {
75
+ const data = this.dataValue || {}
76
+ const chartJSType = this.getChartJSType()
77
+
78
+ // Apply iOS colors to datasets
79
+ if (data.datasets) {
80
+ data.datasets.forEach((dataset, index) => {
81
+ const color = chartColors[index % chartColors.length]
82
+
83
+ if (chartJSType === 'line') {
84
+ dataset.borderColor = color
85
+ dataset.backgroundColor = this.hexToRgba(color, this.typeValue === 'area' ? 0.3 : 0.1)
86
+ dataset.pointBackgroundColor = color
87
+ dataset.pointBorderColor = '#ffffff'
88
+ dataset.pointBorderWidth = 2
89
+ dataset.pointRadius = 4
90
+ dataset.pointHoverRadius = 6
91
+ dataset.fill = this.typeValue === 'area' // Fill area charts
92
+ dataset.tension = 0.4
93
+ } else if (chartJSType === 'bar') {
94
+ dataset.backgroundColor = color
95
+ dataset.borderColor = color
96
+ dataset.borderWidth = 0
97
+ dataset.borderRadius = 4
98
+ dataset.borderSkipped = false
99
+ } else if (chartJSType === 'doughnut' || chartJSType === 'pie') {
100
+ dataset.backgroundColor = chartColors.slice(0, data.labels?.length || 8)
101
+ dataset.borderWidth = 0
102
+ }
103
+ })
104
+ }
105
+
106
+ return data
107
+ }
108
+
109
+ prepareOptions() {
110
+ const chartJSType = this.getChartJSType()
111
+
112
+ const defaultOptions = {
113
+ responsive: true,
114
+ maintainAspectRatio: false,
115
+ interaction: {
116
+ intersect: false,
117
+ mode: 'index'
118
+ },
119
+ // Set indexAxis for horizontal bar charts
120
+ indexAxis: this.typeValue === 'horizontal_bar' ? 'y' : 'x',
121
+ plugins: {
122
+ legend: {
123
+ display: true,
124
+ position: 'bottom',
125
+ labels: {
126
+ usePointStyle: true,
127
+ pointStyle: 'circle',
128
+ padding: 20,
129
+ font: {
130
+ family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
131
+ size: 12,
132
+ weight: '500'
133
+ },
134
+ color: '#1D1D1F'
135
+ }
136
+ },
137
+ tooltip: {
138
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
139
+ titleColor: '#ffffff',
140
+ bodyColor: '#ffffff',
141
+ borderColor: 'rgba(255, 255, 255, 0.1)',
142
+ borderWidth: 1,
143
+ cornerRadius: 8,
144
+ displayColors: true,
145
+ titleFont: {
146
+ family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
147
+ size: 13,
148
+ weight: '600'
149
+ },
150
+ bodyFont: {
151
+ family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
152
+ size: 12,
153
+ weight: '500'
154
+ }
155
+ }
156
+ },
157
+ scales: {
158
+ x: {
159
+ grid: {
160
+ display: true,
161
+ color: 'rgba(0, 0, 0, 0.05)',
162
+ lineWidth: 1
163
+ },
164
+ ticks: {
165
+ font: {
166
+ family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
167
+ size: 11,
168
+ weight: '500'
169
+ },
170
+ color: 'rgba(60, 60, 67, 0.6)'
171
+ },
172
+ border: {
173
+ display: false
174
+ }
175
+ },
176
+ y: {
177
+ grid: {
178
+ display: true,
179
+ color: 'rgba(0, 0, 0, 0.05)',
180
+ lineWidth: 1
181
+ },
182
+ ticks: {
183
+ font: {
184
+ family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
185
+ size: 11,
186
+ weight: '500'
187
+ },
188
+ color: 'rgba(60, 60, 67, 0.6)'
189
+ },
190
+ border: {
191
+ display: false
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ // Remove scales for pie/doughnut charts
198
+ if (chartJSType === 'doughnut' || chartJSType === 'pie') {
199
+ delete defaultOptions.scales
200
+ }
201
+
202
+ // Merge with custom options
203
+ return this.mergeOptions(defaultOptions, this.optionsValue || {})
204
+ }
205
+
206
+ mergeOptions(defaultOptions, customOptions) {
207
+ return Object.assign({}, defaultOptions, customOptions)
208
+ }
209
+
210
+ hexToRgba(hex, alpha) {
211
+ const r = parseInt(hex.slice(1, 3), 16)
212
+ const g = parseInt(hex.slice(3, 5), 16)
213
+ const b = parseInt(hex.slice(5, 7), 16)
214
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`
215
+ }
216
+ }
@@ -0,0 +1,118 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="collapsible-filters"
4
+ export default class extends Controller {
5
+ static targets = ["content", "icon"]
6
+ static values = {
7
+ expanded: { type: Boolean, default: true }
8
+ }
9
+
10
+ connect() {
11
+ // Load saved state
12
+ this.loadState()
13
+
14
+ // Set initial state
15
+ if (this.expandedValue) {
16
+ this.expand(false) // Don't animate on initial load
17
+ } else {
18
+ this.collapse(false) // Don't animate on initial load
19
+ }
20
+
21
+ // Ensure icon has correct initial rotation
22
+ this.updateIconRotation()
23
+ }
24
+
25
+ toggle(event) {
26
+ event.preventDefault()
27
+
28
+ if (this.expandedValue) {
29
+ this.collapse(true)
30
+ } else {
31
+ this.expand(true)
32
+ }
33
+ }
34
+
35
+ expand(animate = true) {
36
+ this.expandedValue = true
37
+
38
+ if (animate) {
39
+ // Animate expansion
40
+ this.contentTarget.style.maxHeight = this.contentTarget.scrollHeight + "px"
41
+ this.contentTarget.classList.remove("opacity-0")
42
+ this.contentTarget.classList.add("opacity-100")
43
+ } else {
44
+ // Instant expansion
45
+ this.contentTarget.style.maxHeight = "none"
46
+ this.contentTarget.classList.remove("opacity-0")
47
+ this.contentTarget.classList.add("opacity-100")
48
+ }
49
+
50
+ // Update icon rotation
51
+ this.updateIconRotation()
52
+
53
+ // Save state
54
+ this.saveState(true)
55
+ }
56
+
57
+ collapse(animate = true) {
58
+ this.expandedValue = false
59
+
60
+ if (animate) {
61
+ // Prepare for animation
62
+ this.contentTarget.style.maxHeight = this.contentTarget.scrollHeight + "px"
63
+
64
+ // Force reflow
65
+ this.contentTarget.offsetHeight
66
+
67
+ // Animate collapse
68
+ this.contentTarget.style.maxHeight = "0px"
69
+ this.contentTarget.classList.remove("opacity-100")
70
+ this.contentTarget.classList.add("opacity-0")
71
+ } else {
72
+ // Instant collapse
73
+ this.contentTarget.style.maxHeight = "0px"
74
+ this.contentTarget.classList.remove("opacity-100")
75
+ this.contentTarget.classList.add("opacity-0")
76
+ }
77
+
78
+ // Update icon rotation
79
+ this.updateIconRotation()
80
+
81
+ // Save state
82
+ this.saveState(false)
83
+ }
84
+
85
+ saveState(expanded) {
86
+ // Save the state for this specific page
87
+ const key = `filters_expanded_${window.location.pathname}`
88
+ localStorage.setItem(key, expanded ? "true" : "false")
89
+ }
90
+
91
+ loadState() {
92
+ // Load saved state for this specific page
93
+ const key = `filters_expanded_${window.location.pathname}`
94
+ const savedState = localStorage.getItem(key)
95
+
96
+ if (savedState !== null) {
97
+ this.expandedValue = savedState === "true"
98
+ }
99
+ // If no saved state exists, use the default value (true - expanded)
100
+ }
101
+
102
+ updateIconRotation() {
103
+ if (this.hasIconTarget) {
104
+ const svg = this.iconTarget.querySelector("svg")
105
+ if (svg) {
106
+ if (this.expandedValue) {
107
+ // Expanded state: icon points down (no rotation)
108
+ svg.classList.remove("-rotate-90")
109
+ svg.style.transform = "rotate(0deg)"
110
+ } else {
111
+ // Collapsed state: icon points right (rotated -90 degrees)
112
+ svg.classList.add("-rotate-90")
113
+ svg.style.transform = "rotate(-90deg)"
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,64 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["modal"]
5
+
6
+ connect() {
7
+ // Show modal with animation
8
+ this.element.style.opacity = '0'
9
+ this.element.style.display = 'block'
10
+ requestAnimationFrame(() => {
11
+ this.element.style.transition = 'opacity 0.3s ease-out'
12
+ this.element.style.opacity = '1'
13
+ })
14
+
15
+ // Scale animation for modal
16
+ this.modalTarget.style.transform = 'scale(0.9)'
17
+ this.modalTarget.style.transition = 'transform 0.3s ease-out'
18
+ requestAnimationFrame(() => {
19
+ this.modalTarget.style.transform = 'scale(1)'
20
+ })
21
+
22
+ // Prevent body scroll
23
+ document.body.style.overflow = 'hidden'
24
+ }
25
+
26
+ disconnect() {
27
+ // Restore body scroll
28
+ document.body.style.overflow = ''
29
+ }
30
+
31
+ confirm() {
32
+ // Dispatch confirmed event
33
+ this.dispatch('confirmed')
34
+ this.close()
35
+ }
36
+
37
+ cancel() {
38
+ // Dispatch cancelled event
39
+ this.dispatch('cancelled')
40
+ this.close()
41
+ }
42
+
43
+ clickOutside(event) {
44
+ // Close if clicked outside modal content
45
+ if (event.target === this.element) {
46
+ this.cancel()
47
+ }
48
+ }
49
+
50
+ close() {
51
+ // Hide with animation
52
+ this.element.style.transition = 'opacity 0.3s ease-out'
53
+ this.modalTarget.style.transform = 'scale(0.9)'
54
+ this.element.style.opacity = '0'
55
+
56
+ setTimeout(() => {
57
+ // Clear the modal turbo frame
58
+ const modalFrame = document.getElementById("modal")
59
+ if (modalFrame) {
60
+ modalFrame.innerHTML = ""
61
+ }
62
+ }, 300)
63
+ }
64
+ }