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,132 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["milestoneCard", "pointsValue", "countValue"]
5
+ // Supplied by the view via milestones_path. The engine is mounted at a
6
+ // host-chosen prefix, so this cannot be a literal path.
7
+ static values = { milestonesPath: String }
8
+
9
+ connect() {
10
+ this.animateStats()
11
+ this.setupCardHovers()
12
+ this.checkForNewMilestones()
13
+ }
14
+
15
+ animateStats() {
16
+ // Animate the point counter
17
+ if (this.hasPointsValueTarget) {
18
+ const finalValue = parseInt(this.pointsValueTarget.textContent)
19
+ this.animateCounter(this.pointsValueTarget, finalValue, 1000)
20
+ }
21
+
22
+ // Animate the milestone counter
23
+ if (this.hasCountValueTarget) {
24
+ const finalValue = parseInt(this.countValueTarget.textContent)
25
+ this.animateCounter(this.countValueTarget, finalValue, 800)
26
+ }
27
+ }
28
+
29
+ animateCounter(element, finalValue, duration) {
30
+ const startValue = 0
31
+ const startTime = performance.now()
32
+
33
+ const updateCounter = (currentTime) => {
34
+ const elapsed = currentTime - startTime
35
+ const progress = Math.min(elapsed / duration, 1)
36
+
37
+ // Easing function for smooth animation
38
+ const easeOut = 1 - Math.pow(1 - progress, 3)
39
+ const currentValue = Math.floor(startValue + (finalValue - startValue) * easeOut)
40
+
41
+ element.textContent = currentValue
42
+
43
+ if (progress < 1) {
44
+ requestAnimationFrame(updateCounter)
45
+ } else {
46
+ element.textContent = finalValue
47
+ }
48
+ }
49
+
50
+ requestAnimationFrame(updateCounter)
51
+ }
52
+
53
+ setupCardHovers() {
54
+ this.milestoneCardTargets.forEach(card => {
55
+ card.addEventListener('mouseenter', this.handleCardHover.bind(this))
56
+ card.addEventListener('mouseleave', this.handleCardLeave.bind(this))
57
+ })
58
+ }
59
+
60
+ handleCardHover(event) {
61
+ const card = event.currentTarget
62
+ card.style.transform = 'translateY(-4px)'
63
+ card.style.transition = 'transform 0.2s ease-in-out'
64
+ }
65
+
66
+ handleCardLeave(event) {
67
+ const card = event.currentTarget
68
+ card.style.transform = 'translateY(0)'
69
+ }
70
+
71
+ checkForNewMilestones() {
72
+ // Check URL parameters for newly achieved milestones
73
+ const urlParams = new URLSearchParams(window.location.search)
74
+ const awardedMilestones = urlParams.get('awarded_milestones')
75
+
76
+ if (awardedMilestones) {
77
+ const milestoneKeys = awardedMilestones.split(',')
78
+
79
+ // Fetch milestone details and show celebrations
80
+ milestoneKeys.forEach(key => {
81
+ this.fetchAndShowMilestone(key.trim())
82
+ })
83
+
84
+ // Clean up URL
85
+ this.cleanUrl()
86
+ }
87
+ }
88
+
89
+ async fetchAndShowMilestone(milestoneKey) {
90
+ // Without the mounted path there is nothing safe to guess at - bail rather
91
+ // than fire a request at the host app's root.
92
+ if (!this.hasMilestonesPathValue || !this.milestonesPathValue) return
93
+
94
+ const base = this.milestonesPathValue.replace(/\/+$/, "")
95
+ try {
96
+ const response = await fetch(`${base}/${encodeURIComponent(milestoneKey)}`, {
97
+ headers: { 'Accept': 'application/json' }
98
+ })
99
+
100
+ if (response.ok) {
101
+ const milestone = await response.json()
102
+ this.showMilestoneCelebration(milestone)
103
+ }
104
+ } catch (error) {
105
+ console.error('Failed to fetch milestone:', error)
106
+ }
107
+ }
108
+
109
+ showMilestoneCelebration(milestone) {
110
+ // An empty shell on purpose: milestone_celebration_controller builds the
111
+ // same structure the server-rendered partial produces when it connects to
112
+ // an overlay with no .celebration-modal. Attributes are set as properties
113
+ // rather than interpolated into markup so milestone data cannot break out
114
+ // of the attribute.
115
+ const overlay = document.createElement("div")
116
+ overlay.className = "milestone-celebration-overlay"
117
+ overlay.dataset.controller = "milestone-celebration"
118
+ overlay.dataset.milestoneCelebrationMilestoneValue = JSON.stringify(milestone)
119
+ overlay.dataset.temporary = "true"
120
+ if (this.hasMilestonesPathValue && this.milestonesPathValue) {
121
+ overlay.dataset.milestoneCelebrationMilestonesPathValue = this.milestonesPathValue
122
+ }
123
+
124
+ document.body.appendChild(overlay)
125
+ }
126
+
127
+ cleanUrl() {
128
+ const url = new URL(window.location)
129
+ url.searchParams.delete('awarded_milestones')
130
+ window.history.replaceState({}, '', url)
131
+ }
132
+ }
@@ -0,0 +1,34 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ this.setupAnimations()
6
+ }
7
+
8
+ setupAnimations() {
9
+ const iconLarge = this.element.querySelector('.milestone-icon-large')
10
+ const hero = this.element.querySelector('.milestone-hero')
11
+
12
+ if (iconLarge) {
13
+ // Add bounce animation to the icon
14
+ iconLarge.style.animation = 'onboarding-milestone-bounce 1s ease-in-out'
15
+ }
16
+
17
+ if (hero && hero.classList.contains('onboarding-achieved')) {
18
+ // Add celebration effect for achieved milestones
19
+ this.addCelebrationGlow()
20
+ }
21
+ }
22
+
23
+ addCelebrationGlow() {
24
+ const hero = this.element.querySelector('.milestone-hero')
25
+ if (hero) {
26
+ hero.classList.add('celebration-glow')
27
+
28
+ // Remove the glow effect after animation
29
+ setTimeout(() => {
30
+ hero.classList.remove('celebration-glow')
31
+ }, 2000)
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,277 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["helpTooltip"]
5
+ static values = { currentStep: String }
6
+
7
+ connect() {
8
+ this.setupKeyboardShortcuts()
9
+ this.setupClickOutsideHandler()
10
+ }
11
+
12
+ // Show help tooltip
13
+ showHelp(event) {
14
+ event.preventDefault()
15
+
16
+ if (this.hasHelpTooltipTarget) {
17
+ this.helpTooltipTarget.hidden = false
18
+ this.positionTooltip()
19
+ this.focusTooltip()
20
+ }
21
+ }
22
+
23
+ // Hide help tooltip
24
+ hideHelp(event) {
25
+ if (event) event.preventDefault()
26
+
27
+ if (this.hasHelpTooltipTarget) {
28
+ this.helpTooltipTarget.hidden = true
29
+ }
30
+ }
31
+
32
+ // Handle skip confirmation
33
+ confirmSkip(event) {
34
+ const confirmed = confirm(event.target.dataset.confirm || "Are you sure you want to skip this step?")
35
+
36
+ if (!confirmed) {
37
+ event.preventDefault()
38
+ return false
39
+ }
40
+
41
+ // Track skip event for analytics
42
+ this.trackSkipEvent()
43
+ return true
44
+ }
45
+
46
+ // Position help tooltip relative to button
47
+ positionTooltip() {
48
+ if (!this.hasHelpTooltipTarget) return
49
+
50
+ const tooltip = this.helpTooltipTarget
51
+ const button = this.element.querySelector('.help-action')
52
+
53
+ if (button) {
54
+ const buttonRect = button.getBoundingClientRect()
55
+ const tooltipRect = tooltip.getBoundingClientRect()
56
+
57
+ // Position above the button by default
58
+ let top = buttonRect.top - tooltipRect.height - 8
59
+ let left = buttonRect.left + (buttonRect.width / 2) - (tooltipRect.width / 2)
60
+
61
+ // Adjust if tooltip would be off-screen
62
+ if (top < 8) {
63
+ // Position below if no room above
64
+ top = buttonRect.bottom + 8
65
+ }
66
+
67
+ if (left < 8) {
68
+ left = 8
69
+ } else if (left + tooltipRect.width > window.innerWidth - 8) {
70
+ left = window.innerWidth - tooltipRect.width - 8
71
+ }
72
+
73
+ tooltip.style.position = 'fixed'
74
+ tooltip.style.top = `${top}px`
75
+ tooltip.style.left = `${left}px`
76
+ }
77
+ }
78
+
79
+ // Focus the first focusable element in tooltip
80
+ focusTooltip() {
81
+ if (this.hasHelpTooltipTarget) {
82
+ const focusableElement = this.helpTooltipTarget.querySelector('button, a, input, [tabindex]')
83
+ if (focusableElement) {
84
+ focusableElement.focus()
85
+ }
86
+ }
87
+ }
88
+
89
+ // Setup keyboard shortcuts
90
+ setupKeyboardShortcuts() {
91
+ document.addEventListener('keydown', this.handleKeyboard.bind(this))
92
+ }
93
+
94
+ // Handle keyboard events
95
+ handleKeyboard(event) {
96
+ // Close help on Escape
97
+ if (event.key === 'Escape' && this.hasHelpTooltipTarget && !this.helpTooltipTarget.hidden) {
98
+ this.hideHelp()
99
+ return
100
+ }
101
+
102
+ // Show help on F1 or Ctrl+?
103
+ if (event.key === 'F1' || (event.ctrlKey && event.key === '?')) {
104
+ event.preventDefault()
105
+ this.showHelp(event)
106
+ return
107
+ }
108
+
109
+ // Skip with Ctrl+S (if skippable)
110
+ if (event.ctrlKey && event.key === 's') {
111
+ const skipButton = this.element.querySelector('.skip-action')
112
+ if (skipButton) {
113
+ event.preventDefault()
114
+ skipButton.click()
115
+ }
116
+ }
117
+ }
118
+
119
+ // Setup click outside handler to close tooltip
120
+ setupClickOutsideHandler() {
121
+ document.addEventListener('click', (event) => {
122
+ if (this.hasHelpTooltipTarget && !this.helpTooltipTarget.hidden) {
123
+ // Check if click is outside tooltip and help button
124
+ const tooltip = this.helpTooltipTarget
125
+ const helpButton = this.element.querySelector('.help-action')
126
+
127
+ if (!tooltip.contains(event.target) && !helpButton.contains(event.target)) {
128
+ this.hideHelp()
129
+ }
130
+ }
131
+ })
132
+ }
133
+
134
+ // Track skip event for analytics
135
+ trackSkipEvent() {
136
+ // Send analytics event if analytics system is available
137
+ if (typeof gtag !== 'undefined') {
138
+ gtag('event', 'onboarding_step_skipped', {
139
+ step: this.currentStepValue,
140
+ timestamp: new Date().toISOString()
141
+ })
142
+ }
143
+
144
+ // Send to Rails if needed
145
+ if (typeof Rails !== 'undefined' && Rails.ajax) {
146
+ Rails.ajax({
147
+ url: '/rails_onboarding/analytics/skip',
148
+ type: 'POST',
149
+ data: `step=${encodeURIComponent(this.currentStepValue)}`,
150
+ headers: {
151
+ 'Content-Type': 'application/x-www-form-urlencoded',
152
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content
153
+ }
154
+ })
155
+ }
156
+ }
157
+
158
+ // Handle navigation button clicks
159
+ navigateToStep(event) {
160
+ const direction = event.params.direction
161
+ const form = document.querySelector('.onboarding-form')
162
+
163
+ if (direction === 'next' && form) {
164
+ // Validate form before proceeding
165
+ const isValid = this.validateCurrentStep()
166
+ if (isValid) {
167
+ form.submit()
168
+ }
169
+ } else if (direction === 'previous') {
170
+ // Handle previous navigation (if implemented)
171
+ this.goToPreviousStep()
172
+ }
173
+ }
174
+
175
+ // Validate current step before proceeding
176
+ validateCurrentStep() {
177
+ const form = document.querySelector('.onboarding-form')
178
+ if (!form) return true
179
+
180
+ // Check required fields
181
+ const requiredFields = form.querySelectorAll('input[required], select[required]')
182
+ const radioGroups = this.getRadioGroups(form)
183
+
184
+ // Validate radio button groups
185
+ for (const groupName in radioGroups) {
186
+ const group = radioGroups[groupName]
187
+ const hasSelection = group.some(radio => radio.checked)
188
+
189
+ if (!hasSelection && group.length > 0) {
190
+ this.showValidationMessage('Please make a selection to continue.')
191
+ group[0].focus()
192
+ return false
193
+ }
194
+ }
195
+
196
+ // Validate other required fields
197
+ for (const field of requiredFields) {
198
+ if (field.value.trim() === '') {
199
+ this.showValidationMessage('Please fill in all required fields.')
200
+ field.focus()
201
+ return false
202
+ }
203
+ }
204
+
205
+ return true
206
+ }
207
+
208
+ // Get radio button groups
209
+ getRadioGroups(form) {
210
+ const radioGroups = {}
211
+
212
+ form.querySelectorAll('input[type="radio"]').forEach(radio => {
213
+ if (!radioGroups[radio.name]) {
214
+ radioGroups[radio.name] = []
215
+ }
216
+ radioGroups[radio.name].push(radio)
217
+ })
218
+
219
+ return radioGroups
220
+ }
221
+
222
+ // Show validation message
223
+ showValidationMessage(message) {
224
+ // Remove existing messages
225
+ document.querySelectorAll('.navigation-validation-error').forEach(error => {
226
+ error.remove()
227
+ })
228
+
229
+ // Create new message
230
+ const errorDiv = document.createElement('div')
231
+ errorDiv.className = 'navigation-validation-error'
232
+ errorDiv.style.cssText = `
233
+ background: #fef2f2;
234
+ border: 1px solid #fecaca;
235
+ color: #dc2626;
236
+ padding: 0.75rem;
237
+ border-radius: 0.5rem;
238
+ margin: 1rem 0;
239
+ text-align: center;
240
+ font-size: 0.875rem;
241
+ `
242
+ errorDiv.textContent = message
243
+
244
+ // Insert at top of navigation
245
+ this.element.insertBefore(errorDiv, this.element.firstChild)
246
+
247
+ // Auto-remove after 5 seconds
248
+ setTimeout(() => {
249
+ if (errorDiv.parentNode) {
250
+ errorDiv.remove()
251
+ }
252
+ }, 5000)
253
+ }
254
+
255
+ // Go to previous step (if supported)
256
+ goToPreviousStep() {
257
+ // This would need to be implemented based on your routing
258
+ console.log('Previous step navigation not yet implemented')
259
+ }
260
+
261
+ // Show progress summary
262
+ showProgressSummary() {
263
+ const progress = document.querySelector('[data-controller="progress"]')
264
+ if (progress) {
265
+ const currentStep = progress.dataset.progressCurrentStepValue || 1
266
+ const totalSteps = progress.dataset.progressTotalStepsValue || 4
267
+
268
+ alert(`Progress: Step ${currentStep} of ${totalSteps}`)
269
+ }
270
+ }
271
+
272
+ // Cleanup on disconnect
273
+ disconnect() {
274
+ document.removeEventListener('keydown', this.handleKeyboard)
275
+ document.removeEventListener('click', this.handleClickOutside)
276
+ }
277
+ }