fluxbit_view_components 0.3.0 → 0.4.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/app/assets/javascripts/fluxbit_view_components/assigner_controller.js +49 -0
  4. data/app/assets/javascripts/fluxbit_view_components/auto_submit_controller.js +39 -0
  5. data/app/assets/javascripts/fluxbit_view_components/drawer_controller.js +135 -0
  6. data/app/assets/javascripts/fluxbit_view_components/index.js +56 -0
  7. data/app/assets/javascripts/fluxbit_view_components/method_link_controller.js +143 -0
  8. data/app/assets/javascripts/fluxbit_view_components/modal_controller.js +118 -0
  9. data/app/assets/javascripts/fluxbit_view_components/password_controller.js +170 -0
  10. data/app/assets/javascripts/fluxbit_view_components/progress_controller.js +374 -0
  11. data/app/assets/javascripts/fluxbit_view_components/row_click_controller.js +32 -0
  12. data/app/assets/javascripts/fluxbit_view_components/select_all_controller.js +122 -0
  13. data/app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js +174 -0
  14. data/app/assets/javascripts/fluxbit_view_components/theme_button_controller.js +90 -0
  15. data/app/assets/javascripts/fluxbit_view_components.js +1175 -0
  16. data/app/components/fluxbit/accordion_component.rb +125 -0
  17. data/app/components/fluxbit/alert_component.rb +8 -8
  18. data/app/components/fluxbit/avatar_component.rb +11 -12
  19. data/app/components/fluxbit/avatar_group_component.rb +1 -1
  20. data/app/components/fluxbit/badge_component.rb +8 -7
  21. data/app/components/fluxbit/banner_component.rb +139 -0
  22. data/app/components/fluxbit/bottom_navigation_component.rb +437 -0
  23. data/app/components/fluxbit/breadcrumb_component.rb +66 -0
  24. data/app/components/fluxbit/button_component.rb +39 -11
  25. data/app/components/fluxbit/button_group_component.rb +1 -1
  26. data/app/components/fluxbit/card_component.rb +26 -23
  27. data/app/components/fluxbit/carousel_component.rb +154 -0
  28. data/app/components/fluxbit/component.rb +24 -3
  29. data/app/components/fluxbit/drawer_component.html.erb +30 -0
  30. data/app/components/fluxbit/drawer_component.rb +125 -0
  31. data/app/components/fluxbit/dropdown_component.rb +41 -0
  32. data/app/components/fluxbit/dropdown_item_component.rb +68 -0
  33. data/app/components/fluxbit/flex_component.rb +1 -1
  34. data/app/components/fluxbit/form/component.rb +15 -8
  35. data/app/components/fluxbit/form/dropzone_component.rb +3 -3
  36. data/app/components/fluxbit/form/field_component.rb +4 -2
  37. data/app/components/fluxbit/form/help_text_component.rb +1 -1
  38. data/app/components/fluxbit/form/label_component.rb +10 -3
  39. data/app/components/fluxbit/form/password_component.rb +247 -0
  40. data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
  41. data/app/components/fluxbit/form/select_component.rb +108 -11
  42. data/app/components/fluxbit/form/text_field_component.rb +40 -23
  43. data/app/components/fluxbit/form/toggle_component.rb +2 -2
  44. data/app/components/fluxbit/form/upload_image_component.html.erb +3 -3
  45. data/app/components/fluxbit/form/upload_image_component.rb +12 -1
  46. data/app/components/fluxbit/gravatar_component.rb +7 -0
  47. data/app/components/fluxbit/icon_helpers.rb +167 -0
  48. data/app/components/fluxbit/link_component.rb +42 -0
  49. data/app/components/fluxbit/modal_component.rb +28 -31
  50. data/app/components/fluxbit/pagination_component.rb +206 -0
  51. data/app/components/fluxbit/popover_component.rb +14 -14
  52. data/app/components/fluxbit/progress_component.rb +196 -0
  53. data/app/components/fluxbit/skeleton_component.rb +237 -0
  54. data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
  55. data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
  56. data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
  57. data/app/components/fluxbit/speed_dial_component.rb +73 -0
  58. data/app/components/fluxbit/spinner_component.rb +71 -0
  59. data/app/components/fluxbit/spinner_percent_component.rb +174 -0
  60. data/app/components/fluxbit/stepper_component.rb +223 -0
  61. data/app/components/fluxbit/tab_component.rb +44 -25
  62. data/app/components/fluxbit/table_component.rb +186 -0
  63. data/app/components/fluxbit/table_group_component.rb +28 -0
  64. data/app/components/fluxbit/theme_button_component.rb +64 -0
  65. data/app/components/fluxbit/timeline_component.rb +63 -0
  66. data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
  67. data/app/components/fluxbit/timeline_item_component.rb +78 -0
  68. data/app/components/fluxbit/tooltip_component.rb +2 -2
  69. data/app/helpers/fluxbit/components_helper.rb +74 -4
  70. data/app/helpers/fluxbit/form_builder.rb +64 -15
  71. data/app/helpers/fluxbit/view_helper.rb +71 -0
  72. data/config/locales/en.yml +37 -4
  73. data/config/locales/pt-BR.yml +36 -0
  74. data/lib/fluxbit/config/accordion_component.rb +73 -0
  75. data/lib/fluxbit/config/avatar_component.rb +11 -11
  76. data/lib/fluxbit/config/badge_component.rb +14 -11
  77. data/lib/fluxbit/config/banner_component.rb +60 -0
  78. data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
  79. data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
  80. data/lib/fluxbit/config/button_component.rb +6 -4
  81. data/lib/fluxbit/config/card_component.rb +23 -12
  82. data/lib/fluxbit/config/carousel_component.rb +33 -0
  83. data/lib/fluxbit/config/drawer_component.rb +48 -0
  84. data/lib/fluxbit/config/dropdown_component.rb +29 -0
  85. data/lib/fluxbit/config/form/check_box_component.rb +1 -1
  86. data/lib/fluxbit/config/form/dropzone_component.rb +1 -1
  87. data/lib/fluxbit/config/form/help_text_component.rb +1 -1
  88. data/lib/fluxbit/config/form/label_component.rb +3 -2
  89. data/lib/fluxbit/config/form/password_component.rb +19 -0
  90. data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
  91. data/lib/fluxbit/config/form/text_field_component.rb +11 -11
  92. data/lib/fluxbit/config/form/toggle_component.rb +5 -5
  93. data/lib/fluxbit/config/link_component.rb +24 -0
  94. data/lib/fluxbit/config/modal_component.rb +1 -1
  95. data/lib/fluxbit/config/pagination_component.rb +31 -0
  96. data/lib/fluxbit/config/popover_component.rb +1 -1
  97. data/lib/fluxbit/config/progress_component.rb +63 -0
  98. data/lib/fluxbit/config/skeleton_component.rb +82 -0
  99. data/lib/fluxbit/config/speed_dial_component.rb +50 -0
  100. data/lib/fluxbit/config/spinner_component.rb +30 -0
  101. data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
  102. data/lib/fluxbit/config/stepper_component.rb +299 -0
  103. data/lib/fluxbit/config/tab_component.rb +6 -0
  104. data/lib/fluxbit/config/table_component.rb +75 -0
  105. data/lib/fluxbit/config/theme_button_component.rb +19 -0
  106. data/lib/fluxbit/config/timeline_component.rb +77 -0
  107. data/lib/fluxbit/view_components/engine.rb +11 -3
  108. data/lib/fluxbit/view_components/version.rb +1 -1
  109. data/lib/fluxbit/view_components.rb +20 -0
  110. data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
  111. data/lib/generators/fluxbit/pagy_generator.rb +39 -0
  112. data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
  113. data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
  114. data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
  115. data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
  116. data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
  117. data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
  118. data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
  119. data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
  120. data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
  121. data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
  122. data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
  123. data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
  124. data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
  125. data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
  126. data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
  127. data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
  128. data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
  129. data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
  130. data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
  131. data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
  132. data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
  133. data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
  134. data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
  135. data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
  136. data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
  137. data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
  138. data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
  139. data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
  140. data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
  141. data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
  142. data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
  143. data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
  144. data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
  145. data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
  146. data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
  147. data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
  148. data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
  149. data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
  150. data/lib/install/install.rb +58 -0
  151. metadata +107 -18
  152. data/app/helpers/fluxbit/classes_helper.rb +0 -9
@@ -0,0 +1,170 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = [
5
+ "eyeIcon",
6
+ "eyeSlashIcon",
7
+ "inputWrapper",
8
+ "strengthIndicator",
9
+ "strengthBar",
10
+ "checkLength",
11
+ "checkLengthPass",
12
+ "checkLengthFail",
13
+ "checkUppercase",
14
+ "checkUppercasePass",
15
+ "checkUppercaseFail",
16
+ "checkLowercase",
17
+ "checkLowercasePass",
18
+ "checkLowercaseFail",
19
+ "checkNumbers",
20
+ "checkNumbersPass",
21
+ "checkNumbersFail",
22
+ "checkSpecial",
23
+ "checkSpecialPass",
24
+ "checkSpecialFail"
25
+ ]
26
+
27
+ static values = {
28
+ minLength: { type: Number, default: 8 },
29
+ requireUppercase: { type: Boolean, default: true },
30
+ requireLowercase: { type: Boolean, default: true },
31
+ requireNumbers: { type: Boolean, default: true },
32
+ requireSpecial: { type: Boolean, default: true }
33
+ }
34
+
35
+ connect() {
36
+ this.passwordVisible = false
37
+ this.passwordInput = this.element.querySelector('input[type="password"]')
38
+
39
+ if (!this.passwordInput) {
40
+ this.passwordInput = this.element.querySelector('input[type="text"]')
41
+ }
42
+
43
+ // Store original attributes that might be lost when changing type
44
+ this.maxLength = this.passwordInput.getAttribute('maxlength')
45
+ }
46
+
47
+ toggleVisibility(event) {
48
+ event.preventDefault()
49
+ event.stopPropagation()
50
+
51
+ this.passwordVisible = !this.passwordVisible
52
+
53
+ if (this.passwordVisible) {
54
+ this.passwordInput.type = "text"
55
+ this.showEyeSlash()
56
+ } else {
57
+ this.passwordInput.type = "password"
58
+ this.showEye()
59
+ }
60
+
61
+ // Restore maxlength if it was set
62
+ if (this.maxLength) {
63
+ this.passwordInput.setAttribute('maxlength', this.maxLength)
64
+ }
65
+ }
66
+
67
+ showEye() {
68
+ if (!this.hasEyeIconTarget || !this.hasEyeSlashIconTarget) return
69
+
70
+ this.eyeIconTarget.classList.remove('hidden')
71
+ this.eyeSlashIconTarget.classList.add('hidden')
72
+ }
73
+
74
+ showEyeSlash() {
75
+ if (!this.hasEyeIconTarget || !this.hasEyeSlashIconTarget) return
76
+
77
+ this.eyeIconTarget.classList.add('hidden')
78
+ this.eyeSlashIconTarget.classList.remove('hidden')
79
+ }
80
+
81
+ validate() {
82
+ if (!this.hasStrengthIndicatorTarget) return
83
+
84
+ const password = this.passwordInput.value
85
+ const checks = {
86
+ length: password.length >= this.minLengthValue,
87
+ uppercase: this.requireUppercaseValue ? /[A-Z]/.test(password) : true,
88
+ lowercase: this.requireLowercaseValue ? /[a-z]/.test(password) : true,
89
+ numbers: this.requireNumbersValue ? /[0-9]/.test(password) : true,
90
+ special: this.requireSpecialValue ? /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]/.test(password) : true
91
+ }
92
+
93
+ // Update check indicators
94
+ this.updateCheck("length", checks.length)
95
+ if (this.requireUppercaseValue) this.updateCheck("uppercase", checks.uppercase)
96
+ if (this.requireLowercaseValue) this.updateCheck("lowercase", checks.lowercase)
97
+ if (this.requireNumbersValue) this.updateCheck("numbers", checks.numbers)
98
+ if (this.requireSpecialValue) this.updateCheck("special", checks.special)
99
+
100
+ // Calculate strength
101
+ const totalChecks = Object.values(checks).length
102
+ const passedChecks = Object.values(checks).filter(Boolean).length
103
+ const strengthPercentage = (passedChecks / totalChecks) * 100
104
+
105
+ this.updateStrengthBar(strengthPercentage)
106
+ }
107
+
108
+ updateCheck(type, passed) {
109
+ const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1)
110
+ const checkTarget = `check${capitalizedType}Target`
111
+ const passTarget = `check${capitalizedType}PassTarget`
112
+ const failTarget = `check${capitalizedType}FailTarget`
113
+
114
+ // Check if targets exist
115
+ if (!this[`has${checkTarget.charAt(0).toUpperCase() + checkTarget.slice(1)}`]) return
116
+ if (!this[`has${passTarget.charAt(0).toUpperCase() + passTarget.slice(1)}`]) return
117
+ if (!this[`has${failTarget.charAt(0).toUpperCase() + failTarget.slice(1)}`]) return
118
+
119
+ const checkElement = this[checkTarget]
120
+ const passIcon = this[passTarget]
121
+ const failIcon = this[failTarget]
122
+ const iconContainer = checkElement.querySelector('.flex-shrink-0')
123
+
124
+ if (passed) {
125
+ // Show check icon, hide X icon
126
+ passIcon.classList.remove('hidden')
127
+ failIcon.classList.add('hidden')
128
+
129
+ // Update colors
130
+ iconContainer.classList.remove('text-red-500', 'dark:text-red-400')
131
+ iconContainer.classList.add('text-green-500', 'dark:text-green-400')
132
+ checkElement.classList.remove('text-slate-600', 'dark:text-slate-400')
133
+ checkElement.classList.add('text-green-600', 'dark:text-green-400')
134
+ } else {
135
+ // Show X icon, hide check icon
136
+ passIcon.classList.add('hidden')
137
+ failIcon.classList.remove('hidden')
138
+
139
+ // Update colors
140
+ iconContainer.classList.remove('text-green-500', 'dark:text-green-400')
141
+ iconContainer.classList.add('text-red-500', 'dark:text-red-400')
142
+ checkElement.classList.remove('text-green-600', 'dark:text-green-400')
143
+ checkElement.classList.add('text-slate-600', 'dark:text-slate-400')
144
+ }
145
+ }
146
+
147
+ updateStrengthBar(percentage) {
148
+ if (!this.hasStrengthBarTarget) return
149
+
150
+ this.strengthBarTarget.style.width = `${percentage}%`
151
+
152
+ // Update color based on strength
153
+ this.strengthBarTarget.classList.remove(
154
+ 'bg-red-500', 'dark:bg-red-400',
155
+ 'bg-yellow-500', 'dark:bg-yellow-400',
156
+ 'bg-green-500', 'dark:bg-green-400',
157
+ 'bg-slate-300', 'dark:bg-slate-600'
158
+ )
159
+
160
+ if (percentage === 0) {
161
+ this.strengthBarTarget.classList.add('bg-slate-300', 'dark:bg-slate-600')
162
+ } else if (percentage < 50) {
163
+ this.strengthBarTarget.classList.add('bg-red-500', 'dark:bg-red-400')
164
+ } else if (percentage < 100) {
165
+ this.strengthBarTarget.classList.add('bg-yellow-500', 'dark:bg-yellow-400')
166
+ } else {
167
+ this.strengthBarTarget.classList.add('bg-green-500', 'dark:bg-green-400')
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,374 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["bar", "textLabel", "progressLabel"];
5
+ static values = {
6
+ progress: {
7
+ type: Number,
8
+ default: 0
9
+ },
10
+ animate: {
11
+ type: Boolean,
12
+ default: true
13
+ },
14
+ speed: {
15
+ type: String,
16
+ default: "normal"
17
+ }
18
+ };
19
+
20
+ connect() {
21
+ this.updateProgress();
22
+ this.updateBarTransition();
23
+ }
24
+
25
+ progressValueChanged() {
26
+ this.updateProgress();
27
+ }
28
+
29
+ animateValueChanged() {
30
+ this.updateBarTransition();
31
+ }
32
+
33
+ speedValueChanged() {
34
+ this.updateBarTransition();
35
+ }
36
+
37
+ updateProgress() {
38
+ const clampedProgress = Math.max(0, Math.min(100, this.progressValue));
39
+
40
+ if (this.hasBarTarget) {
41
+ this.barTarget.style.width = `${clampedProgress}%`;
42
+ this.barTarget.setAttribute('aria-valuenow', clampedProgress);
43
+ }
44
+
45
+ if (this.hasProgressLabelTarget) {
46
+ this.progressLabelTarget.textContent = `${clampedProgress}%`;
47
+ }
48
+
49
+ this.element.setAttribute('aria-valuenow', clampedProgress);
50
+ }
51
+
52
+ updateBarTransition() {
53
+ if (!this.hasBarTarget) return;
54
+
55
+ if (this.animateValue) {
56
+ let duration;
57
+ switch (this.speedValue) {
58
+ case 'slow':
59
+ duration = '2s';
60
+ break;
61
+ case 'fast':
62
+ duration = '0.3s';
63
+ break;
64
+ case 'very_fast':
65
+ duration = '0.1s';
66
+ break;
67
+ case 'normal':
68
+ default:
69
+ duration = '0.6s';
70
+ break;
71
+ }
72
+
73
+ this.barTarget.style.transition = `width ${duration} ease-out`;
74
+ } else {
75
+ this.barTarget.style.transition = 'none';
76
+ }
77
+ }
78
+
79
+ // Core progress methods
80
+ setProgress(progress) {
81
+ this.progressValue = progress;
82
+ }
83
+
84
+ incrementProgress(amount = 1) {
85
+ this.progressValue = Math.min(100, this.progressValue + amount);
86
+ }
87
+
88
+ decrementProgress(amount = 1) {
89
+ this.progressValue = Math.max(0, this.progressValue - amount);
90
+ }
91
+
92
+ reset() {
93
+ this.progressValue = 0;
94
+ }
95
+
96
+ complete() {
97
+ this.progressValue = 100;
98
+ }
99
+
100
+ setSpeed(speed) {
101
+ this.speedValue = speed;
102
+ }
103
+
104
+ setAnimate(animate) {
105
+ this.animateValue = animate;
106
+ }
107
+
108
+ // Animation method
109
+ animateToProgress(targetProgress, duration = 1000) {
110
+ const startProgress = this.progressValue;
111
+ const difference = targetProgress - startProgress;
112
+ const startTime = performance.now();
113
+
114
+ if (this.animationId) {
115
+ cancelAnimationFrame(this.animationId);
116
+ }
117
+
118
+ const animate = (currentTime) => {
119
+ const elapsed = currentTime - startTime;
120
+ const progress = Math.min(elapsed / duration, 1);
121
+ const easeOut = 1 - Math.pow(1 - progress, 3);
122
+ const currentProgress = startProgress + (difference * easeOut);
123
+
124
+ this.updateProgressDirect(Math.round(currentProgress));
125
+
126
+ if (progress < 1) {
127
+ this.animationId = requestAnimationFrame(animate);
128
+ } else {
129
+ this.progressValue = targetProgress;
130
+ this.animationId = null;
131
+ }
132
+ };
133
+
134
+ this.animationId = requestAnimationFrame(animate);
135
+ }
136
+
137
+ updateProgressDirect(progress) {
138
+ const clampedProgress = Math.max(0, Math.min(100, progress));
139
+
140
+ if (this.hasBarTarget) {
141
+ this.barTarget.style.width = `${clampedProgress}%`;
142
+ this.barTarget.setAttribute('aria-valuenow', clampedProgress);
143
+ }
144
+
145
+ if (this.hasProgressLabelTarget) {
146
+ this.progressLabelTarget.textContent = `${clampedProgress}%`;
147
+ }
148
+
149
+ this.element.setAttribute('aria-valuenow', clampedProgress);
150
+ }
151
+
152
+ // Action methods (for Stimulus data-action bindings)
153
+ increment(event) {
154
+ const amount = parseInt(event.params?.amount) || 1;
155
+ const progressId = event.params?.id;
156
+
157
+ if (progressId) {
158
+ this.updateProgressById(progressId, (controller) => controller.incrementProgress(amount));
159
+ } else {
160
+ this.incrementProgress(amount);
161
+ }
162
+ }
163
+
164
+ decrement(event) {
165
+ const amount = parseInt(event.params?.amount) || 1;
166
+ const progressId = event.params?.id;
167
+
168
+ if (progressId) {
169
+ this.updateProgressById(progressId, (controller) => controller.decrementProgress(amount));
170
+ } else {
171
+ this.decrementProgress(amount);
172
+ }
173
+ }
174
+
175
+ resetProgress(event) {
176
+ const progressId = event.params?.id;
177
+
178
+ if (progressId) {
179
+ this.updateProgressById(progressId, (controller) => controller.reset());
180
+ } else {
181
+ this.reset();
182
+ }
183
+ }
184
+
185
+ completeProgress(event) {
186
+ const progressId = event.params?.id;
187
+
188
+ if (progressId) {
189
+ this.updateProgressById(progressId, (controller) => controller.complete());
190
+ } else {
191
+ this.complete();
192
+ }
193
+ }
194
+
195
+ animateTo(event) {
196
+ const target = parseInt(event.params?.target) || 100;
197
+ const duration = parseInt(event.params?.duration) || 1000;
198
+ const progressId = event.params?.id;
199
+
200
+ if (progressId) {
201
+ this.updateProgressById(progressId, (controller) => controller.animateToProgress(target, duration));
202
+ } else {
203
+ this.animateToProgress(target, duration);
204
+ }
205
+ }
206
+
207
+ updateSpeed(event) {
208
+ const speed = event.params?.speed || 'normal';
209
+ const progressId = event.params?.id;
210
+
211
+ if (progressId) {
212
+ this.updateProgressById(progressId, (controller) => controller.setSpeed(speed));
213
+ } else {
214
+ this.setSpeed(speed);
215
+ }
216
+ }
217
+
218
+ // Helper method to target specific progress bars by ID
219
+ updateProgressById(progressId, callback) {
220
+ const progressContainer = this.element.querySelector(`[data-progress-id="${progressId}"]`);
221
+ if (!progressContainer) return;
222
+
223
+ // Get the progress bar and labels within the specific progress container
224
+ const progressBar = progressContainer.querySelector('div[style*="width"]');
225
+ const textLabel = progressContainer.parentElement.querySelector('span:first-child');
226
+ const progressLabel = progressContainer.parentElement.querySelector('span:last-child');
227
+
228
+ if (!progressBar) return;
229
+
230
+ // Create a temporary controller-like object to operate on this specific progress bar
231
+ const tempController = {
232
+ barElement: progressBar,
233
+ textLabelElement: textLabel,
234
+ progressLabelElement: progressLabel,
235
+ containerElement: progressContainer,
236
+
237
+ setProgress: (progress) => {
238
+ const clampedProgress = Math.max(0, Math.min(100, progress));
239
+ this.updateSpecificProgress(progressBar, progressLabel, clampedProgress);
240
+ },
241
+
242
+ incrementProgress: (amount = 1) => {
243
+ const currentProgress = this.getCurrentProgress(progressBar);
244
+ const newProgress = Math.min(100, currentProgress + amount);
245
+ this.updateSpecificProgress(progressBar, progressLabel, newProgress);
246
+ },
247
+
248
+ decrementProgress: (amount = 1) => {
249
+ const currentProgress = this.getCurrentProgress(progressBar);
250
+ const newProgress = Math.max(0, currentProgress - amount);
251
+ this.updateSpecificProgress(progressBar, progressLabel, newProgress);
252
+ },
253
+
254
+ reset: () => {
255
+ this.updateSpecificProgress(progressBar, progressLabel, 0);
256
+ },
257
+
258
+ complete: () => {
259
+ this.updateSpecificProgress(progressBar, progressLabel, 100);
260
+ },
261
+
262
+ animateToProgress: (targetProgress, duration = 1000) => {
263
+ const currentProgress = this.getCurrentProgress(progressBar);
264
+ this.animateSpecificProgress(progressBar, progressLabel, currentProgress, targetProgress, duration);
265
+ },
266
+
267
+ setSpeed: (speed) => {
268
+ // Update transition speed for this specific progress bar
269
+ let duration;
270
+ switch (speed) {
271
+ case 'slow': duration = '2s'; break;
272
+ case 'fast': duration = '0.3s'; break;
273
+ case 'very_fast': duration = '0.1s'; break;
274
+ default: duration = '0.6s'; break;
275
+ }
276
+ progressBar.style.transition = `width ${duration} ease-out`;
277
+ }
278
+ };
279
+
280
+ if (callback) {
281
+ callback(tempController);
282
+ }
283
+ }
284
+
285
+ // Helper methods for specific progress bar operations
286
+ getCurrentProgress(progressBar) {
287
+ const widthStyle = progressBar.style.width;
288
+ return parseInt(widthStyle.replace('%', '')) || 0;
289
+ }
290
+
291
+ updateSpecificProgress(progressBar, progressLabel, progress) {
292
+ const clampedProgress = Math.max(0, Math.min(100, progress));
293
+
294
+ progressBar.style.width = `${clampedProgress}%`;
295
+ progressBar.setAttribute('aria-valuenow', clampedProgress);
296
+
297
+ if (progressLabel) {
298
+ progressLabel.textContent = `${clampedProgress}%`;
299
+ }
300
+ }
301
+
302
+ animateSpecificProgress(progressBar, progressLabel, startProgress, targetProgress, duration) {
303
+ const difference = targetProgress - startProgress;
304
+ const startTime = performance.now();
305
+
306
+ const animate = (currentTime) => {
307
+ const elapsed = currentTime - startTime;
308
+ const progress = Math.min(elapsed / duration, 1);
309
+ const easeOut = 1 - Math.pow(1 - progress, 3);
310
+ const currentProgress = startProgress + (difference * easeOut);
311
+
312
+ this.updateSpecificProgress(progressBar, progressLabel, Math.round(currentProgress));
313
+
314
+ if (progress < 1) {
315
+ requestAnimationFrame(animate);
316
+ } else {
317
+ this.updateSpecificProgress(progressBar, progressLabel, targetProgress);
318
+ }
319
+ };
320
+
321
+ requestAnimationFrame(animate);
322
+ }
323
+
324
+ disconnect() {
325
+ if (this.animationId) {
326
+ cancelAnimationFrame(this.animationId);
327
+ this.animationId = null;
328
+ }
329
+ }
330
+
331
+ // Global access methods for vanilla JavaScript
332
+ static getController(element) {
333
+ if (typeof element === 'string') {
334
+ element = document.querySelector(element);
335
+ }
336
+ if (!element) return null;
337
+
338
+ const controllerElement = element.closest('[data-controller*="fx-progress"]');
339
+ if (!controllerElement) return null;
340
+
341
+ return window.Stimulus?.getControllerForElementAndIdentifier(controllerElement, 'fx-progress') ||
342
+ application?.getControllerForElementAndIdentifier(controllerElement, 'fx-progress');
343
+ }
344
+
345
+ static updateProgress(selector, progress) {
346
+ const controller = this.getController(selector);
347
+ if (controller) controller.setProgress(progress);
348
+ }
349
+
350
+ static incrementProgress(selector, amount = 10) {
351
+ const controller = this.getController(selector);
352
+ if (controller) controller.incrementProgress(amount);
353
+ }
354
+
355
+ static animateProgress(selector, target, duration = 1000) {
356
+ const controller = this.getController(selector);
357
+ if (controller) controller.animateToProgress(target, duration);
358
+ }
359
+
360
+ static resetProgress(selector) {
361
+ const controller = this.getController(selector);
362
+ if (controller) controller.reset();
363
+ }
364
+
365
+ static completeProgress(selector) {
366
+ const controller = this.getController(selector);
367
+ if (controller) controller.complete();
368
+ }
369
+
370
+ static updateProgressById(selector, progressId, callback) {
371
+ const controller = this.getController(selector);
372
+ if (controller) controller.updateProgressById(progressId, callback);
373
+ }
374
+ }
@@ -0,0 +1,32 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { url: String, frame: String }
5
+
6
+ connect() {
7
+ // Ensure we're dealing with a table row
8
+ if (this.element.tagName !== 'TR') {
9
+ console.warn('row-click controller should only be used on <tr> tags')
10
+ return
11
+ }
12
+ this.element.addEventListener("click", this.navigate.bind(this));
13
+ }
14
+
15
+ disconnect() {
16
+ this.element.removeEventListener("click", this.navigate.bind(this));
17
+ }
18
+
19
+ navigate(event) {
20
+ if (event.target === event.currentTarget ||
21
+ !event.target.closest('button, a, input, select, textarea')) {
22
+ if (this.frameValue) {
23
+ // Navigate within a turbo frame
24
+ window.Turbo.visit(this.urlValue, { frame: this.frameValue });
25
+
26
+ } else {
27
+ // Regular navigation
28
+ window.Turbo.visit(this.urlValue);
29
+ }
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,122 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // This controller manages the behavior of a select all checkbox and its associated checkboxes
4
+ // It allows users to select or deselect all items in a list and provides visual feedback based on the selection state.
5
+ // It also updates the visibility and enabled/disabled state of other elements based on the selection state
6
+ // The code is based on Stimulus Component Checkbox Select All
7
+ // (https://github.com/stimulus-components/stimulus-components/tree/master/components/checkbox-select-all)
8
+ //
9
+ // Copyright (c) 2024 Guillaume Briday <guillaumebriday@gmail.com>
10
+ // Licensed under the MIT License
11
+ // Modified by Arthur Molina <arthurmolina@gmail.com>, 2025
12
+ export default class extends Controller {
13
+ static targets = [
14
+ "selectAll",
15
+ "select",
16
+ "disableOnEmptySelect",
17
+ "enableOnEmptySelect",
18
+ "hideOnEmptySelect",
19
+ "showOnEmptySelect",
20
+ "count"
21
+ ];
22
+
23
+ static values = {
24
+ disableIndeterminate: {
25
+ type: Boolean,
26
+ default: false,
27
+ },
28
+ };
29
+
30
+ initialize() {
31
+ this.toggle = this.toggle.bind(this);
32
+ this.refresh = this.refresh.bind(this);
33
+ }
34
+
35
+ selectAllTargetConnected(checkbox) {
36
+ checkbox.addEventListener("change", this.toggle);
37
+ this.refresh();
38
+ }
39
+
40
+ selectTargetConnected(checkbox) {
41
+ checkbox.addEventListener("change", this.refresh);
42
+ this.refresh();
43
+ }
44
+
45
+ selectAllTargetDisconnected(checkbox) {
46
+ checkbox.removeEventListener("change", this.toggle);
47
+ this.refresh();
48
+ }
49
+
50
+ selectTargetDisconnected(checkbox) {
51
+ checkbox.removeEventListener("change", this.refresh);
52
+ this.refresh();
53
+ }
54
+
55
+ toggle(e) {
56
+ e.preventDefault();
57
+
58
+ this.selectTargets.forEach((checkbox) => {
59
+ checkbox.checked = e.target.checked;
60
+ this.triggerInputEvent(checkbox);
61
+ });
62
+ this.refresh()
63
+ }
64
+
65
+ refresh() {
66
+ const checkboxesCount = this.selectTargets.length;
67
+ const checkboxesCheckedCount = this.checked.length;
68
+
69
+ if (this.disableIndeterminateValue) {
70
+ this.selectAllTarget.checked = checkboxesCheckedCount === checkboxesCount;
71
+ } else {
72
+ this.selectAllTarget.checked = checkboxesCheckedCount > 0;
73
+ this.selectAllTarget.indeterminate = checkboxesCheckedCount > 0 && checkboxesCheckedCount < checkboxesCount;
74
+ }
75
+ this.updateVisibility();
76
+ this.updateDisabledState();
77
+ this.updateCount();
78
+ }
79
+
80
+ updateVisibility() {
81
+ const hasChecked = this.checkedCount() > 0;
82
+ this.hideOnEmptySelectTargets.forEach((el) => {
83
+ el.classList.toggle("hidden", hasChecked);
84
+ });
85
+ this.showOnEmptySelectTargets.forEach((el) => {
86
+ el.classList.toggle("hidden", !hasChecked);
87
+ });
88
+ }
89
+
90
+ updateDisabledState() {
91
+ const hasChecked = this.checkedCount() > 0;
92
+ this.disableOnEmptySelectTargets.forEach((el) => {
93
+ el.disabled = !hasChecked;
94
+ });
95
+ this.enableOnEmptySelectTargets.forEach((el) => {
96
+ el.disabled = hasChecked;
97
+ });
98
+ }
99
+
100
+ updateCount() {
101
+ this.countTargets.forEach((el) => {
102
+ el.textContent = this.checkedCount().toString();
103
+ });
104
+ }
105
+
106
+ triggerInputEvent(checkbox) {
107
+ const event = new Event("input", { bubbles: false, cancelable: true });
108
+ checkbox.dispatchEvent(event);
109
+ }
110
+
111
+ checkedCount() {
112
+ return this.checked.length;
113
+ }
114
+
115
+ get checked() {
116
+ return this.selectTargets.filter((checkbox) => checkbox.checked);
117
+ }
118
+
119
+ get unchecked() {
120
+ return this.selectTargets.filter((checkbox) => !checkbox.checked);
121
+ }
122
+ }