rails_onboarding 0.6.1

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 (148) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1351 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
  6. data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
  7. data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
  8. data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
  9. data/app/assets/javascripts/rails_onboarding/application.js +231 -0
  10. data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
  11. data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
  12. data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
  13. data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
  14. data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
  15. data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
  16. data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
  17. data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
  18. data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
  19. data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
  20. data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
  21. data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
  22. data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
  23. data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
  24. data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
  25. data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
  26. data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
  27. data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
  28. data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
  29. data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
  30. data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
  31. data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
  32. data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
  33. data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
  34. data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
  35. data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
  36. data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
  37. data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
  38. data/app/controllers/rails_onboarding/application_controller.rb +9 -0
  39. data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
  40. data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
  41. data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
  42. data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
  43. data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
  44. data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
  45. data/app/helpers/rails_onboarding/application_helper.rb +4 -0
  46. data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
  47. data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
  48. data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
  49. data/app/jobs/rails_onboarding/application_job.rb +4 -0
  50. data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
  51. data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
  52. data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
  53. data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
  54. data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
  55. data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
  56. data/app/models/rails_onboarding/analytics.rb +255 -0
  57. data/app/models/rails_onboarding/analytics_event.rb +168 -0
  58. data/app/models/rails_onboarding/application_record.rb +5 -0
  59. data/app/models/rails_onboarding/flow.rb +41 -0
  60. data/app/services/rails_onboarding/milestone_service.rb +105 -0
  61. data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
  62. data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
  63. data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
  64. data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
  65. data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
  66. data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
  67. data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
  68. data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
  69. data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
  70. data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
  71. data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
  72. data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
  73. data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
  74. data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
  75. data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
  76. data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
  77. data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
  78. data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
  79. data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
  80. data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
  81. data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
  82. data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
  83. data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
  84. data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
  85. data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
  86. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
  87. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
  88. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
  89. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
  90. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
  91. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
  92. data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
  93. data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
  94. data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
  95. data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
  96. data/app/views/rails_onboarding/templates/index.html.erb +255 -0
  97. data/config/importmap.rb +33 -0
  98. data/config/locales/en.yml +62 -0
  99. data/config/locales/es.yml +62 -0
  100. data/config/locales/fr.yml +62 -0
  101. data/config/routes.rb +79 -0
  102. data/lib/generators/rails_onboarding/install_generator.rb +124 -0
  103. data/lib/generators/rails_onboarding/templates/README +120 -0
  104. data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
  105. data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
  106. data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
  107. data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
  108. data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
  109. data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
  110. data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
  111. data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
  112. data/lib/generators/rails_onboarding/update_generator.rb +73 -0
  113. data/lib/rails_onboarding/api_mode.rb +367 -0
  114. data/lib/rails_onboarding/backfill.rb +130 -0
  115. data/lib/rails_onboarding/background_jobs.rb +412 -0
  116. data/lib/rails_onboarding/caching.rb +180 -0
  117. data/lib/rails_onboarding/cdn_support.rb +202 -0
  118. data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
  119. data/lib/rails_onboarding/configuration/analytics.rb +29 -0
  120. data/lib/rails_onboarding/configuration/integrations.rb +31 -0
  121. data/lib/rails_onboarding/configuration/milestones.rb +92 -0
  122. data/lib/rails_onboarding/configuration/personalization.rb +25 -0
  123. data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
  124. data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
  125. data/lib/rails_onboarding/configuration/steps.rb +121 -0
  126. data/lib/rails_onboarding/configuration/templates.rb +85 -0
  127. data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
  128. data/lib/rails_onboarding/configuration.rb +133 -0
  129. data/lib/rails_onboarding/configuration_errors.rb +19 -0
  130. data/lib/rails_onboarding/configuration_validator.rb +477 -0
  131. data/lib/rails_onboarding/controller_helpers.rb +153 -0
  132. data/lib/rails_onboarding/current.rb +13 -0
  133. data/lib/rails_onboarding/deprecation.rb +64 -0
  134. data/lib/rails_onboarding/devise_integration.rb +122 -0
  135. data/lib/rails_onboarding/engine.rb +245 -0
  136. data/lib/rails_onboarding/error_recovery.rb +173 -0
  137. data/lib/rails_onboarding/lazy_loading.rb +155 -0
  138. data/lib/rails_onboarding/multi_tenant.rb +306 -0
  139. data/lib/rails_onboarding/requirements_validator.rb +315 -0
  140. data/lib/rails_onboarding/responsive_helper.rb +218 -0
  141. data/lib/rails_onboarding/session_manager.rb +187 -0
  142. data/lib/rails_onboarding/skip_logic.rb +159 -0
  143. data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
  144. data/lib/rails_onboarding/version.rb +3 -0
  145. data/lib/rails_onboarding.rb +44 -0
  146. data/lib/tasks/rails_onboarding_analytics.rake +136 -0
  147. data/lib/tasks/rails_onboarding_tasks.rake +162 -0
  148. metadata +303 -0
@@ -0,0 +1,326 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["submitButton", "radioButton"]
5
+ static values = { currentStep: String }
6
+
7
+ connect() {
8
+ this.updateSubmitButtonState()
9
+ this.setupKeyboardNavigation()
10
+ this.announceStepToScreenReader()
11
+ }
12
+
13
+ // Enable submit button when radio button is selected
14
+ enableSubmit(event) {
15
+ const submitButton = this.element.querySelector('.primary-action[disabled]')
16
+ if (submitButton && event.target.checked) {
17
+ submitButton.disabled = false
18
+ submitButton.textContent = submitButton.textContent.replace('disabled', '')
19
+ }
20
+ }
21
+
22
+ // Handle radio button changes
23
+ radioChanged(event) {
24
+ if (event.target.type === 'radio' && event.target.checked) {
25
+ this.enableSubmitButton()
26
+ this.updateSelectedChoice(event.target)
27
+ }
28
+ }
29
+
30
+ // Show feature tooltip
31
+ showFeatureTooltip(event) {
32
+ const feature = event.target.dataset.featureTarget
33
+ const tooltip = this.createTooltip(feature)
34
+ this.showTooltip(tooltip, event.target)
35
+ }
36
+
37
+ // Update submit button state based on form validity
38
+ updateSubmitButtonState() {
39
+ const submitButton = this.element.querySelector('.primary-action')
40
+ const radioButtons = this.element.querySelectorAll('input[type="radio"]')
41
+ const requiredFields = this.element.querySelectorAll('input[required], select[required]')
42
+
43
+ if (submitButton && (radioButtons.length > 0 || requiredFields.length > 0)) {
44
+ const hasSelection = Array.from(radioButtons).some(radio => radio.checked)
45
+ const allRequiredFilled = Array.from(requiredFields).every(field =>
46
+ field.value.trim() !== ''
47
+ )
48
+
49
+ if (radioButtons.length > 0) {
50
+ submitButton.disabled = !hasSelection
51
+ } else if (requiredFields.length > 0) {
52
+ submitButton.disabled = !allRequiredFilled
53
+ }
54
+ }
55
+ }
56
+
57
+ // Enable the submit button
58
+ enableSubmitButton() {
59
+ const submitButton = this.element.querySelector('.primary-action')
60
+ if (submitButton) {
61
+ submitButton.disabled = false
62
+ }
63
+ }
64
+
65
+ // Update visual state of selected choice
66
+ updateSelectedChoice(selectedRadio) {
67
+ // Remove selection from all choices
68
+ this.element.querySelectorAll('.action-choice').forEach(choice => {
69
+ choice.classList.remove('selected')
70
+ })
71
+
72
+ // Add selection to current choice
73
+ const selectedChoice = selectedRadio.closest('.action-choice')
74
+ if (selectedChoice) {
75
+ selectedChoice.classList.add('selected')
76
+ }
77
+ }
78
+
79
+ // Create tooltip for feature explanation
80
+ createTooltip(feature) {
81
+ const tooltips = {
82
+ organizing: {
83
+ title: "Smart Organization",
84
+ content: "Create hierarchical lists, set priorities, add due dates, and organize items exactly how you think. Use tags, categories, and custom sorting to keep everything in its place."
85
+ },
86
+ collaboration: {
87
+ title: "Team Collaboration",
88
+ content: "Invite team members, assign tasks, leave comments, and track progress together. Real-time updates keep everyone synchronized and productive."
89
+ },
90
+ automation: {
91
+ title: "Smart Automation",
92
+ content: "Set up recurring tasks, automated reminders, smart templates, and rule-based actions. Let the app handle routine work so you can focus on what matters."
93
+ },
94
+ insights: {
95
+ title: "Progress Insights",
96
+ content: "Track completion rates, identify patterns, see productivity trends, and get personalized suggestions to improve your workflow and achieve your goals."
97
+ }
98
+ }
99
+
100
+ return tooltips[feature] || { title: "Feature", content: "Learn more about this feature." }
101
+ }
102
+
103
+ // Show tooltip near target element
104
+ showTooltip(tooltipData, targetElement) {
105
+ // Remove any existing tooltips
106
+ this.hideTooltips()
107
+
108
+ const tooltip = document.createElement('div')
109
+ tooltip.className = 'feature-tooltip'
110
+ // The class names here are the ones tooltips.css already styles.
111
+ // Previously this emitted a bare <h4>/<p>, which picked up
112
+ // .tooltip-content h4/p from application.css - rules coloured for a light
113
+ // background (var(--onboarding-text)) - leaving dark text on the feature
114
+ // tooltip's saturated gradient. .tooltip-title/.tooltip-body carry the
115
+ // white-on-gradient treatment that was written for this component.
116
+ // "Got it" is a text button, so it uses .tooltip-action rather than
117
+ // .tooltip-close, which is sized 1.5rem square for an icon and clipped it.
118
+ tooltip.innerHTML = `
119
+ <div class="tooltip-content">
120
+ <h4 class="tooltip-title">${tooltipData.title}</h4>
121
+ <p class="tooltip-body">${tooltipData.content}</p>
122
+ <div class="tooltip-actions">
123
+ <button type="button" class="tooltip-action onboarding-secondary" data-action="click->onboarding#hideTooltips">
124
+ Got it
125
+ </button>
126
+ </div>
127
+ </div>
128
+ `
129
+
130
+ // Position tooltip
131
+ document.body.appendChild(tooltip)
132
+ this.positionTooltip(tooltip, targetElement)
133
+
134
+ // Auto-hide after 10 seconds
135
+ setTimeout(() => this.hideTooltips(), 10000)
136
+ }
137
+
138
+ // Position tooltip relative to target
139
+ positionTooltip(tooltip, target) {
140
+ const rect = target.getBoundingClientRect()
141
+ const tooltipRect = tooltip.getBoundingClientRect()
142
+
143
+ // Sits below the target by default, flipping above when it would run off
144
+ // the bottom of the viewport.
145
+ let top = rect.bottom + 10
146
+ let placedAbove = false
147
+ let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2)
148
+
149
+ // Adjust if tooltip would be off-screen
150
+ if (left < 10) left = 10
151
+ if (left + tooltipRect.width > window.innerWidth - 10) {
152
+ left = window.innerWidth - tooltipRect.width - 10
153
+ }
154
+ if (top + tooltipRect.height > window.innerHeight - 10) {
155
+ top = rect.top - tooltipRect.height - 10
156
+ placedAbove = true
157
+ }
158
+
159
+ // Tell the stylesheet which way the arrow should point. The naming
160
+ // matches tooltip_controller.js: onboarding-top means the tooltip sits
161
+ // above its target, so the arrow hangs off the bottom edge.
162
+ tooltip.classList.remove("onboarding-top", "onboarding-bottom")
163
+ tooltip.classList.add(placedAbove ? "onboarding-top" : "onboarding-bottom")
164
+
165
+ tooltip.style.position = 'fixed'
166
+ tooltip.style.top = `${top}px`
167
+ tooltip.style.left = `${left}px`
168
+ tooltip.style.zIndex = '1000'
169
+ }
170
+
171
+ // Hide all tooltips
172
+ hideTooltips() {
173
+ document.querySelectorAll('.feature-tooltip').forEach(tooltip => {
174
+ tooltip.remove()
175
+ })
176
+ }
177
+
178
+ // Setup keyboard navigation
179
+ setupKeyboardNavigation() {
180
+ this.element.addEventListener('keydown', (event) => {
181
+ if (event.key === 'Escape') {
182
+ this.hideTooltips()
183
+ }
184
+
185
+ // Arrow key navigation for radio buttons
186
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
187
+ const radioButtons = Array.from(this.element.querySelectorAll('input[type="radio"]'))
188
+ const currentIndex = radioButtons.findIndex(radio => radio.checked)
189
+
190
+ if (radioButtons.length > 0) {
191
+ event.preventDefault()
192
+ let nextIndex
193
+
194
+ if (event.key === 'ArrowDown') {
195
+ nextIndex = (currentIndex + 1) % radioButtons.length
196
+ } else {
197
+ nextIndex = currentIndex <= 0 ? radioButtons.length - 1 : currentIndex - 1
198
+ }
199
+
200
+ radioButtons[nextIndex].checked = true
201
+ radioButtons[nextIndex].focus()
202
+ this.updateSelectedChoice(radioButtons[nextIndex])
203
+ this.enableSubmitButton()
204
+ }
205
+ }
206
+ })
207
+ }
208
+
209
+ // Announce step change to screen readers
210
+ announceStepToScreenReader() {
211
+ const announcement = document.createElement('div')
212
+ announcement.setAttribute('aria-live', 'polite')
213
+ announcement.setAttribute('aria-atomic', 'true')
214
+ announcement.className = 'onboarding-sr-only'
215
+ announcement.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;'
216
+
217
+ const stepTitle = this.element.querySelector('.step-title')
218
+ if (stepTitle) {
219
+ announcement.textContent = `Onboarding step: ${stepTitle.textContent}`
220
+ document.body.appendChild(announcement)
221
+
222
+ // Remove after announcement
223
+ setTimeout(() => {
224
+ if (announcement.parentNode) {
225
+ announcement.parentNode.removeChild(announcement)
226
+ }
227
+ }, 1000)
228
+ }
229
+ }
230
+
231
+ // Handle form submission with validation
232
+ submitForm(event) {
233
+ const form = event.target.closest('form')
234
+ if (form) {
235
+ // Validate required fields
236
+ const requiredFields = form.querySelectorAll('input[required], select[required]')
237
+ const radioGroups = {}
238
+
239
+ // Check radio button groups
240
+ form.querySelectorAll('input[type="radio"]').forEach(radio => {
241
+ if (!radioGroups[radio.name]) {
242
+ radioGroups[radio.name] = []
243
+ }
244
+ radioGroups[radio.name].push(radio)
245
+ })
246
+
247
+ // Validate radio groups
248
+ for (const groupName in radioGroups) {
249
+ const group = radioGroups[groupName]
250
+ const hasSelection = group.some(radio => radio.checked)
251
+
252
+ if (!hasSelection && group.length > 0) {
253
+ event.preventDefault()
254
+ this.showValidationError('Please make a selection to continue.')
255
+ return false
256
+ }
257
+ }
258
+
259
+ // Validate other required fields
260
+ for (const field of requiredFields) {
261
+ if (field.value.trim() === '') {
262
+ event.preventDefault()
263
+ field.focus()
264
+ this.showValidationError('Please fill in all required fields.')
265
+ return false
266
+ }
267
+ }
268
+ }
269
+
270
+ return true
271
+ }
272
+
273
+ // Show validation error message
274
+ showValidationError(message) {
275
+ // Remove existing error messages
276
+ this.element.querySelectorAll('.validation-error').forEach(error => {
277
+ error.remove()
278
+ })
279
+
280
+ // Create error message
281
+ const errorDiv = document.createElement('div')
282
+ errorDiv.className = 'validation-error'
283
+ errorDiv.style.cssText = `
284
+ background: #fef2f2;
285
+ border: 1px solid #fecaca;
286
+ color: #dc2626;
287
+ padding: 0.75rem 1rem;
288
+ border-radius: 0.5rem;
289
+ margin-top: 1rem;
290
+ font-size: 0.875rem;
291
+ text-align: center;
292
+ `
293
+ errorDiv.textContent = message
294
+
295
+ // Insert before action buttons
296
+ const actionButtons = this.element.querySelector('.action-buttons')
297
+ if (actionButtons) {
298
+ actionButtons.parentNode.insertBefore(errorDiv, actionButtons)
299
+ } else {
300
+ this.element.appendChild(errorDiv)
301
+ }
302
+
303
+ // Auto-remove after 5 seconds
304
+ setTimeout(() => {
305
+ if (errorDiv.parentNode) {
306
+ errorDiv.remove()
307
+ }
308
+ }, 5000)
309
+ }
310
+
311
+ // Handle input changes for real-time validation
312
+ inputChanged(event) {
313
+ this.updateSubmitButtonState()
314
+
315
+ // Clear validation errors when user starts typing
316
+ const existingErrors = this.element.querySelectorAll('.validation-error')
317
+ if (existingErrors.length > 0) {
318
+ existingErrors.forEach(error => error.remove())
319
+ }
320
+ }
321
+
322
+ // Disconnect cleanup
323
+ disconnect() {
324
+ this.hideTooltips()
325
+ }
326
+ }
@@ -0,0 +1,203 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ progress: Number,
6
+ totalSteps: Number,
7
+ currentStep: Number
8
+ }
9
+
10
+ connect() {
11
+ this.updateProgressDisplay()
12
+ }
13
+
14
+ // Update progress when values change
15
+ progressValueChanged() {
16
+ this.updateProgressDisplay()
17
+ }
18
+
19
+ currentStepValueChanged() {
20
+ this.updateStepStates()
21
+ }
22
+
23
+ // Update the visual progress display. The fill's width comes from
24
+ // --onboarding-progress-width (consumed by .progress-fill in CSS), so
25
+ // changing it here still animates via the stylesheet's `transition:
26
+ // width` - no inline width/transition needed.
27
+ updateProgressDisplay() {
28
+ const progressFill = this.element.querySelector('.onboarding-progress-fill')
29
+ const progressText = this.element.querySelector('.current-progress')
30
+
31
+ if (progressFill) {
32
+ progressFill.style.setProperty('--onboarding-progress-width', `${this.progressValue}%`)
33
+ }
34
+
35
+ if (progressText) {
36
+ progressText.textContent = `Step ${this.currentStepValue || 1}`
37
+ }
38
+ }
39
+
40
+ // Update step marker states
41
+ updateStepStates() {
42
+ const steps = this.element.querySelectorAll('.progress-step')
43
+ const currentStep = this.currentStepValue || 1
44
+
45
+ steps.forEach((step, index) => {
46
+ const stepNumber = index + 1
47
+ const marker = step.querySelector('.step-marker')
48
+ const checkElement = step.querySelector('.step-check')
49
+ const numberElement = step.querySelector('.step-number')
50
+
51
+ // Remove all state classes
52
+ step.classList.remove('onboarding-completed', 'current', 'upcoming')
53
+
54
+ // Add appropriate class based on progress
55
+ if (stepNumber < currentStep) {
56
+ step.classList.add('onboarding-completed')
57
+
58
+ // Show checkmark for completed steps
59
+ if (numberElement && !checkElement) {
60
+ numberElement.textContent = '✓'
61
+ numberElement.className = 'step-check'
62
+ }
63
+ } else if (stepNumber === currentStep) {
64
+ step.classList.add('onboarding-current')
65
+
66
+ // Show number for current step
67
+ if (numberElement) {
68
+ numberElement.textContent = stepNumber
69
+ numberElement.className = 'step-number'
70
+ }
71
+ } else {
72
+ step.classList.add('onboarding-upcoming')
73
+
74
+ // Show number for upcoming steps
75
+ if (numberElement) {
76
+ numberElement.textContent = stepNumber
77
+ numberElement.className = 'step-number'
78
+ }
79
+ }
80
+ })
81
+ }
82
+
83
+ // Calculate progress percentage
84
+ calculateProgress(currentStep, totalSteps) {
85
+ if (!totalSteps || totalSteps <= 0) return 0
86
+ return Math.min(100, Math.max(0, (currentStep / totalSteps) * 100))
87
+ }
88
+
89
+ // Pulse animation for current step
90
+ pulseCurrentStep() {
91
+ const currentStepElement = this.element.querySelector('.progress-step.onboarding-current .step-marker')
92
+ if (currentStepElement) {
93
+ currentStepElement.classList.add('step-marker-pulse')
94
+
95
+ setTimeout(() => {
96
+ currentStepElement.classList.remove('step-marker-pulse')
97
+ }, 1000)
98
+ }
99
+ }
100
+
101
+ // Show step tooltip on hover
102
+ showStepTooltip(event) {
103
+ const step = event.currentTarget
104
+ const stepIndex = Array.from(step.parentNode.children).indexOf(step)
105
+ const stepData = this.getStepData(stepIndex)
106
+
107
+ if (stepData) {
108
+ this.createStepTooltip(stepData, step)
109
+ }
110
+ }
111
+
112
+ // Hide step tooltip
113
+ hideStepTooltip(event) {
114
+ const tooltip = document.querySelector('.step-tooltip')
115
+ if (tooltip) {
116
+ tooltip.remove()
117
+ }
118
+ }
119
+
120
+ // Get step data for tooltip
121
+ getStepData(stepIndex) {
122
+ const stepNames = ['welcome', 'profile', 'first_action', 'explore']
123
+ const stepTitles = ['Welcome', 'Setup Profile', 'First Action', 'Explore Features']
124
+ const stepDescriptions = [
125
+ 'Get introduced to the platform',
126
+ 'Personalize your experience',
127
+ 'Take your first meaningful action',
128
+ 'Discover key features'
129
+ ]
130
+
131
+ if (stepIndex < stepNames.length) {
132
+ return {
133
+ name: stepNames[stepIndex],
134
+ title: stepTitles[stepIndex],
135
+ description: stepDescriptions[stepIndex]
136
+ }
137
+ }
138
+ return null
139
+ }
140
+
141
+ // Create tooltip for step
142
+ createStepTooltip(stepData, targetElement) {
143
+ const tooltip = document.createElement('div')
144
+ tooltip.className = 'step-tooltip'
145
+ tooltip.innerHTML = `
146
+ <div class="tooltip-content">
147
+ <h4>${stepData.title}</h4>
148
+ <p>${stepData.description}</p>
149
+ </div>
150
+ `
151
+
152
+ // Position tooltip (top/left are viewport-relative and computed per
153
+ // call, so they stay inline; everything else lives in .step-tooltip)
154
+ document.body.appendChild(tooltip)
155
+ this.positionTooltip(tooltip, targetElement)
156
+
157
+ // Auto-remove after 3 seconds
158
+ setTimeout(() => {
159
+ if (tooltip.parentNode) {
160
+ tooltip.remove()
161
+ }
162
+ }, 3000)
163
+ }
164
+
165
+ // Position tooltip relative to target
166
+ positionTooltip(tooltip, target) {
167
+ const rect = target.getBoundingClientRect()
168
+ const tooltipRect = tooltip.getBoundingClientRect()
169
+
170
+ let top = rect.bottom + 8
171
+ let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2)
172
+
173
+ // Adjust if tooltip would be off-screen
174
+ if (left < 8) left = 8
175
+ if (left + tooltipRect.width > window.innerWidth - 8) {
176
+ left = window.innerWidth - tooltipRect.width - 8
177
+ }
178
+ if (top + tooltipRect.height > window.innerHeight - 8) {
179
+ top = rect.top - tooltipRect.height - 8
180
+ }
181
+
182
+ tooltip.style.top = `${top}px`
183
+ tooltip.style.left = `${left}px`
184
+ }
185
+
186
+ // Advance to next step (for testing/demo purposes)
187
+ advanceStep() {
188
+ if (this.currentStepValue < this.totalStepsValue) {
189
+ this.currentStepValue += 1
190
+ this.progressValue = this.calculateProgress(this.currentStepValue, this.totalStepsValue)
191
+ this.pulseCurrentStep()
192
+ }
193
+ }
194
+
195
+ // Go back to previous step
196
+ previousStep() {
197
+ if (this.currentStepValue > 1) {
198
+ this.currentStepValue -= 1
199
+ this.progressValue = this.calculateProgress(this.currentStepValue, this.totalStepsValue)
200
+ this.pulseCurrentStep()
201
+ }
202
+ }
203
+ }