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,511 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Tooltip Scheduler for Progressive Disclosure
4
+ // Manages sequences of tooltips, timing, and user flow
5
+ export default class extends Controller {
6
+ static values = {
7
+ sequence: String, // JSON array of tooltip configurations
8
+ autoStart: { type: Boolean, default: false },
9
+ delay: { type: Number, default: 1000 }, // Delay between tooltips
10
+ pauseOnHover: { type: Boolean, default: true },
11
+ pauseOnInteraction: { type: Boolean, default: true },
12
+ maxConcurrent: { type: Number, default: 1 }, // Max tooltips shown at once
13
+ priority: { type: String, default: "sequential" } // sequential, priority, adaptive
14
+ }
15
+
16
+ connect() {
17
+ this.currentIndex = 0
18
+ this.isRunning = false
19
+ this.isPaused = false
20
+ this.activeTooltips = []
21
+ this.userInteracting = false
22
+ this.lastInteractionTime = Date.now()
23
+ this.behaviorData = this.loadBehaviorData()
24
+
25
+ this.parseSequence()
26
+ this.setupInteractionTracking()
27
+
28
+ if (this.autoStartValue) {
29
+ this.start()
30
+ }
31
+ }
32
+
33
+ // Parse the sequence configuration
34
+ parseSequence() {
35
+ try {
36
+ this.tooltipSequence = JSON.parse(this.sequenceValue || '[]')
37
+ } catch (error) {
38
+ console.warn('Invalid tooltip sequence configuration:', error)
39
+ this.tooltipSequence = []
40
+ }
41
+
42
+ // Validate and enhance each tooltip config
43
+ this.tooltipSequence = this.tooltipSequence.map((config, index) => ({
44
+ id: config.id || `tooltip_${index}`,
45
+ selector: config.selector,
46
+ content: config.content,
47
+ position: config.position || 'top',
48
+ trigger: config.trigger || 'auto',
49
+ priority: config.priority || 5,
50
+ delay: config.delay || this.delayValue,
51
+ duration: config.duration || 5000,
52
+ conditions: config.conditions || {},
53
+ onShow: config.onShow,
54
+ onHide: config.onHide,
55
+ dismissible: config.dismissible !== false,
56
+ contextual: config.contextual || false,
57
+ animation: config.animation || 'fade',
58
+ maxDaily: config.maxDaily || 3,
59
+ ...config
60
+ }))
61
+ }
62
+
63
+ // Setup interaction tracking to pause tooltips during active use
64
+ setupInteractionTracking() {
65
+ // Track various user interactions
66
+ const interactionEvents = ['click', 'keydown', 'scroll', 'mousemove', 'touchstart']
67
+
68
+ interactionEvents.forEach(eventType => {
69
+ document.addEventListener(eventType, () => {
70
+ this.userInteracting = true
71
+ this.lastInteractionTime = Date.now()
72
+
73
+ if (this.pauseOnInteractionValue && this.isRunning) {
74
+ this.pause()
75
+
76
+ // Resume after interaction stops
77
+ clearTimeout(this.resumeTimeout)
78
+ this.resumeTimeout = setTimeout(() => {
79
+ this.userInteracting = false
80
+ if (this.isPaused) {
81
+ this.resume()
82
+ }
83
+ }, 2000) // Resume 2 seconds after interaction stops
84
+ }
85
+ })
86
+ })
87
+
88
+ // Track hovering over tooltips
89
+ if (this.pauseOnHoverValue) {
90
+ document.addEventListener('mouseenter', (event) => {
91
+ if (event.target.closest('.onboarding-tooltip')) {
92
+ this.pause()
93
+ }
94
+ }, true)
95
+
96
+ document.addEventListener('mouseleave', (event) => {
97
+ if (event.target.closest('.onboarding-tooltip')) {
98
+ if (this.isPaused && !this.userInteracting) {
99
+ this.resume()
100
+ }
101
+ }
102
+ }, true)
103
+ }
104
+ }
105
+
106
+ // Start the tooltip sequence
107
+ start() {
108
+ if (this.isRunning) return
109
+
110
+ this.isRunning = true
111
+ this.isPaused = false
112
+ this.currentIndex = 0
113
+
114
+ this.scheduleNext()
115
+ }
116
+
117
+ // Pause the sequence
118
+ pause() {
119
+ this.isPaused = true
120
+ if (this.nextTimeout) {
121
+ clearTimeout(this.nextTimeout)
122
+ }
123
+ }
124
+
125
+ // Resume the sequence
126
+ resume() {
127
+ if (!this.isRunning || !this.isPaused) return
128
+
129
+ this.isPaused = false
130
+ this.scheduleNext()
131
+ }
132
+
133
+ // Stop the sequence
134
+ stop() {
135
+ this.isRunning = false
136
+ this.isPaused = false
137
+ this.currentIndex = 0
138
+
139
+ if (this.nextTimeout) {
140
+ clearTimeout(this.nextTimeout)
141
+ }
142
+
143
+ // Hide all active tooltips
144
+ this.hideAllTooltips()
145
+ }
146
+
147
+ // Schedule the next tooltip in the sequence
148
+ scheduleNext() {
149
+ if (!this.isRunning || this.isPaused) return
150
+
151
+ // Clean up finished tooltips
152
+ this.cleanupActiveTooltips()
153
+
154
+ // Check if we have more tooltips to show
155
+ const nextTooltip = this.getNextTooltip()
156
+ if (!nextTooltip) {
157
+ this.complete()
158
+ return
159
+ }
160
+
161
+ // Check if we can show more tooltips (concurrent limit)
162
+ if (this.activeTooltips.length >= this.maxConcurrentValue) {
163
+ // Wait for some to finish
164
+ setTimeout(() => this.scheduleNext(), 500)
165
+ return
166
+ }
167
+
168
+ // Schedule the tooltip
169
+ this.nextTimeout = setTimeout(() => {
170
+ this.showTooltip(nextTooltip)
171
+ this.scheduleNext()
172
+ }, nextTooltip.delay)
173
+ }
174
+
175
+ // Get the next tooltip to show based on priority strategy
176
+ getNextTooltip() {
177
+ switch (this.priorityValue) {
178
+ case 'sequential':
179
+ return this.getNextSequential()
180
+ case 'priority':
181
+ return this.getNextByPriority()
182
+ case 'adaptive':
183
+ return this.getNextAdaptive()
184
+ default:
185
+ return this.getNextSequential()
186
+ }
187
+ }
188
+
189
+ // Get next tooltip sequentially
190
+ getNextSequential() {
191
+ while (this.currentIndex < this.tooltipSequence.length) {
192
+ const tooltip = this.tooltipSequence[this.currentIndex]
193
+ this.currentIndex++
194
+
195
+ if (this.shouldShowTooltip(tooltip)) {
196
+ return tooltip
197
+ }
198
+ }
199
+ return null
200
+ }
201
+
202
+ // Get next tooltip by priority (highest first)
203
+ getNextByPriority() {
204
+ const availableTooltips = this.tooltipSequence
205
+ .filter(tooltip => this.shouldShowTooltip(tooltip))
206
+ .sort((a, b) => b.priority - a.priority)
207
+
208
+ if (availableTooltips.length > 0) {
209
+ const tooltip = availableTooltips[0]
210
+ // Remove from sequence so it's not shown again
211
+ this.tooltipSequence = this.tooltipSequence.filter(t => t.id !== tooltip.id)
212
+ return tooltip
213
+ }
214
+
215
+ return null
216
+ }
217
+
218
+ // Get next tooltip adaptively based on user behavior
219
+ getNextAdaptive() {
220
+ const behaviorData = this.behaviorData[this.getTodayKey()]
221
+ if (!behaviorData) return this.getNextSequential()
222
+
223
+ // Adapt based on user behavior patterns
224
+ const availableTooltips = this.tooltipSequence.filter(tooltip => this.shouldShowTooltip(tooltip))
225
+
226
+ if (availableTooltips.length === 0) return null
227
+
228
+ // Score tooltips based on user context
229
+ const scoredTooltips = availableTooltips.map(tooltip => ({
230
+ ...tooltip,
231
+ score: this.scoreTooltipRelevance(tooltip, behaviorData)
232
+ }))
233
+
234
+ // Sort by score and return highest
235
+ scoredTooltips.sort((a, b) => b.score - a.score)
236
+ const tooltip = scoredTooltips[0]
237
+
238
+ // Remove from sequence
239
+ this.tooltipSequence = this.tooltipSequence.filter(t => t.id !== tooltip.id)
240
+ return tooltip
241
+ }
242
+
243
+ // Score tooltip relevance based on user behavior
244
+ scoreTooltipRelevance(tooltip, behaviorData) {
245
+ let score = tooltip.priority
246
+
247
+ // Boost score for struggling areas
248
+ if (behaviorData.strugglingAreas && behaviorData.strugglingAreas.includes(tooltip.selector)) {
249
+ score += 3
250
+ }
251
+
252
+ // Reduce score if shown recently
253
+ const recentlyShown = behaviorData.tooltipsShown && behaviorData.tooltipsShown[tooltip.id]
254
+ if (recentlyShown && recentlyShown > Date.now() - 86400000) { // 24 hours
255
+ score -= 2
256
+ }
257
+
258
+ // Boost score for high interaction areas
259
+ if (behaviorData.interactions > 10) {
260
+ score += 1
261
+ }
262
+
263
+ // Contextual adjustments
264
+ if (tooltip.contextual) {
265
+ // Check if user is on relevant page/section
266
+ const relevantElement = document.querySelector(tooltip.selector)
267
+ if (relevantElement && this.isElementVisible(relevantElement)) {
268
+ score += 2
269
+ }
270
+ }
271
+
272
+ return score
273
+ }
274
+
275
+ // Check if tooltip should be shown
276
+ shouldShowTooltip(tooltip) {
277
+ // Check if element exists
278
+ const element = document.querySelector(tooltip.selector)
279
+ if (!element) return false
280
+
281
+ // Check visibility
282
+ if (!this.isElementVisible(element)) return false
283
+
284
+ // Check conditions
285
+ if (tooltip.conditions && !this.checkConditions(tooltip.conditions)) {
286
+ return false
287
+ }
288
+
289
+ // Check daily limits
290
+ const behaviorData = this.behaviorData[this.getTodayKey()]
291
+ if (behaviorData && behaviorData.tooltipsShown) {
292
+ const todayCount = behaviorData.tooltipsShown[tooltip.id] || 0
293
+ if (todayCount >= tooltip.maxDaily) {
294
+ return false
295
+ }
296
+ }
297
+
298
+ // Check if already dismissed
299
+ const seenTooltips = JSON.parse(localStorage.getItem('rails_onboarding_seen_tooltips') || '{}')
300
+ if (seenTooltips[tooltip.id]) {
301
+ return false
302
+ }
303
+
304
+ return true
305
+ }
306
+
307
+ // Check custom conditions
308
+ checkConditions(conditions) {
309
+ for (const [condition, value] of Object.entries(conditions)) {
310
+ switch (condition) {
311
+ case 'url':
312
+ if (!window.location.href.includes(value)) return false
313
+ break
314
+ case 'element_visible':
315
+ if (!this.isElementVisible(document.querySelector(value))) return false
316
+ break
317
+ case 'time_on_page':
318
+ const timeOnPage = Date.now() - this.pageStartTime
319
+ if (timeOnPage < value) return false
320
+ break
321
+ case 'user_type':
322
+ // Could check user attributes here
323
+ break
324
+ case 'screen_size':
325
+ if (value === 'mobile' && window.innerWidth > 768) return false
326
+ if (value === 'desktop' && window.innerWidth <= 768) return false
327
+ break
328
+ }
329
+ }
330
+ return true
331
+ }
332
+
333
+ // Show a tooltip
334
+ showTooltip(tooltipConfig) {
335
+ const element = document.querySelector(tooltipConfig.selector)
336
+ if (!element) return
337
+
338
+ // Create tooltip controller instance
339
+ const tooltipElement = this.createTooltipElement(tooltipConfig)
340
+ element.appendChild(tooltipElement)
341
+
342
+ // Track as active
343
+ const tooltipInstance = {
344
+ config: tooltipConfig,
345
+ element: tooltipElement,
346
+ startTime: Date.now()
347
+ }
348
+
349
+ this.activeTooltips.push(tooltipInstance)
350
+ this.recordTooltipShow(tooltipConfig.id)
351
+
352
+ // Auto-hide after duration
353
+ setTimeout(() => {
354
+ this.hideTooltip(tooltipInstance)
355
+ }, tooltipConfig.duration)
356
+
357
+ // Execute onShow callback
358
+ if (tooltipConfig.onShow && typeof window[tooltipConfig.onShow] === 'function') {
359
+ window[tooltipConfig.onShow](tooltipConfig)
360
+ }
361
+ }
362
+
363
+ // Create tooltip DOM element
364
+ createTooltipElement(config) {
365
+ const wrapper = document.createElement('div')
366
+ wrapper.innerHTML = `
367
+ <div data-controller="tooltip"
368
+ data-tooltip-position-value="${config.position}"
369
+ data-tooltip-feature-value="${config.id}"
370
+ data-tooltip-animation-value="${config.animation}"
371
+ data-tooltip-trigger-value="auto"
372
+ data-tooltip-dismissible-value="${config.dismissible}">
373
+ <!-- hidden: the tooltip controller reads this element's innerHTML to
374
+ build the floating popup, so it must exist in the DOM, but it must
375
+ not render inline inside the target element (which bled the step's
376
+ copy into the page, e.g. the dashboard title). -->
377
+ <div data-tooltip-target="content" hidden>
378
+ ${config.content}
379
+ </div>
380
+ </div>
381
+ `
382
+ return wrapper.firstElementChild
383
+ }
384
+
385
+ // Hide a specific tooltip
386
+ hideTooltip(tooltipInstance) {
387
+ if (tooltipInstance.element && tooltipInstance.element.parentNode) {
388
+ // Trigger hide through tooltip controller
389
+ const tooltipController = this.application.getControllerForElementAndIdentifier(
390
+ tooltipInstance.element, 'tooltip'
391
+ )
392
+
393
+ if (tooltipController) {
394
+ tooltipController.forceHide()
395
+ } else {
396
+ tooltipInstance.element.remove()
397
+ }
398
+ }
399
+
400
+ // Execute onHide callback
401
+ if (tooltipInstance.config.onHide && typeof window[tooltipInstance.config.onHide] === 'function') {
402
+ window[tooltipInstance.config.onHide](tooltipInstance.config)
403
+ }
404
+
405
+ // Remove from active tooltips
406
+ this.activeTooltips = this.activeTooltips.filter(t => t !== tooltipInstance)
407
+ }
408
+
409
+ // Hide all active tooltips
410
+ hideAllTooltips() {
411
+ [...this.activeTooltips].forEach(tooltip => {
412
+ this.hideTooltip(tooltip)
413
+ })
414
+ }
415
+
416
+ // Clean up finished tooltips
417
+ cleanupActiveTooltips() {
418
+ this.activeTooltips = this.activeTooltips.filter(tooltip => {
419
+ return tooltip.element && tooltip.element.parentNode
420
+ })
421
+ }
422
+
423
+ // Check if element is visible
424
+ isElementVisible(element) {
425
+ if (!element) return false
426
+
427
+ const rect = element.getBoundingClientRect()
428
+ return rect.width > 0 && rect.height > 0 &&
429
+ rect.top >= 0 && rect.left >= 0 &&
430
+ rect.bottom <= window.innerHeight &&
431
+ rect.right <= window.innerWidth
432
+ }
433
+
434
+ // Complete the sequence
435
+ complete() {
436
+ this.isRunning = false
437
+ this.isPaused = false
438
+
439
+ // Dispatch completion event
440
+ this.dispatch('complete', {
441
+ detail: {
442
+ totalShown: this.activeTooltips.length,
443
+ sequence: this.tooltipSequence
444
+ }
445
+ })
446
+ }
447
+
448
+ // Load behavior data
449
+ loadBehaviorData() {
450
+ return JSON.parse(localStorage.getItem('rails_onboarding_behavior') || '{}')
451
+ }
452
+
453
+ // Record tooltip show
454
+ recordTooltipShow(tooltipId) {
455
+ const todayKey = this.getTodayKey()
456
+
457
+ if (!this.behaviorData[todayKey]) {
458
+ this.behaviorData[todayKey] = { tooltipsShown: {} }
459
+ }
460
+
461
+ if (!this.behaviorData[todayKey].tooltipsShown) {
462
+ this.behaviorData[todayKey].tooltipsShown = {}
463
+ }
464
+
465
+ // Store a running COUNT, not a timestamp: shouldShowTooltip compares this
466
+ // against tooltip.maxDaily as a count, so storing Date.now() here made every
467
+ // value astronomically larger than maxDaily and suppressed the tooltip after
468
+ // a single show (breaking the guided tour on any subsequent load).
469
+ const shownToday = this.behaviorData[todayKey].tooltipsShown
470
+ shownToday[tooltipId] = (shownToday[tooltipId] || 0) + 1
471
+
472
+ localStorage.setItem('rails_onboarding_behavior', JSON.stringify(this.behaviorData))
473
+ }
474
+
475
+ // Get today's key
476
+ getTodayKey() {
477
+ return new Date().toISOString().split('T')[0]
478
+ }
479
+
480
+ // Public API methods
481
+ skipCurrent() {
482
+ if (this.activeTooltips.length > 0) {
483
+ this.hideTooltip(this.activeTooltips[0])
484
+ }
485
+ this.scheduleNext()
486
+ }
487
+
488
+ skipAll() {
489
+ this.stop()
490
+ }
491
+
492
+ showTooltipById(id) {
493
+ const tooltip = this.tooltipSequence.find(t => t.id === id)
494
+ if (tooltip && this.shouldShowTooltip(tooltip)) {
495
+ this.showTooltip(tooltip)
496
+ }
497
+ }
498
+
499
+ // Cleanup on disconnect
500
+ disconnect() {
501
+ this.stop()
502
+
503
+ if (this.nextTimeout) {
504
+ clearTimeout(this.nextTimeout)
505
+ }
506
+
507
+ if (this.resumeTimeout) {
508
+ clearTimeout(this.resumeTimeout)
509
+ }
510
+ }
511
+ }