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,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOnboarding
4
+ # Validates that the host application meets all requirements for RailsOnboarding
5
+ # Usage: RailsOnboarding::RequirementsValidator.validate!
6
+ class RequirementsValidator
7
+ class ValidationError < StandardError; end
8
+
9
+ class << self
10
+ # Run all validations and raise if any fail
11
+ def validate!
12
+ results = run_validations
13
+
14
+ if results[:errors].any?
15
+ raise ValidationError, format_error_message(results)
16
+ end
17
+
18
+ log_results(results)
19
+ true
20
+ end
21
+
22
+ # Run all validations and return results without raising
23
+ def check
24
+ run_validations
25
+ end
26
+
27
+ private
28
+
29
+ def run_validations
30
+ results = {
31
+ errors: [],
32
+ warnings: [],
33
+ info: []
34
+ }
35
+
36
+ validate_rails_version(results)
37
+ validate_ruby_version(results)
38
+ validate_user_class(results)
39
+ validate_user_model(results)
40
+ validate_application_controller(results)
41
+ validate_current_user_method(results)
42
+ validate_database_columns(results)
43
+ validate_routes(results)
44
+ validate_optional_dependencies(results)
45
+ validate_configuration(results)
46
+
47
+ results
48
+ end
49
+
50
+ def validate_rails_version(results)
51
+ required_version = Gem::Version.new("7.0.0")
52
+ current_version = Gem::Version.new(Rails.version)
53
+
54
+ if current_version < required_version
55
+ results[:errors] << "Rails version #{required_version} or higher required (current: #{current_version})"
56
+ else
57
+ results[:info] << "Rails version: #{current_version} ✓"
58
+ end
59
+ rescue => e
60
+ results[:errors] << "Could not determine Rails version: #{e.message}"
61
+ end
62
+
63
+ def validate_ruby_version(results)
64
+ required_version = Gem::Version.new("3.0.0")
65
+ current_version = Gem::Version.new(RUBY_VERSION)
66
+
67
+ if current_version < required_version
68
+ results[:errors] << "Ruby version #{required_version} or higher required (current: #{current_version})"
69
+ else
70
+ results[:info] << "Ruby version: #{current_version} ✓"
71
+ end
72
+ rescue => e
73
+ results[:errors] << "Could not determine Ruby version: #{e.message}"
74
+ end
75
+
76
+ def validate_user_class(results)
77
+ begin
78
+ user_class_name = RailsOnboarding.configuration.user_class_name
79
+ user_class = user_class_name.constantize
80
+
81
+ results[:info] << "User class '#{user_class_name}' found ✓"
82
+
83
+ # Check if Onboardable concern is included
84
+ unless user_class.included_modules.include?(RailsOnboarding::Onboardable)
85
+ results[:errors] << "User class '#{user_class_name}' must include RailsOnboarding::Onboardable concern"
86
+ else
87
+ results[:info] << "Onboardable concern included ✓"
88
+ end
89
+ rescue NameError
90
+ results[:errors] << "User class '#{RailsOnboarding.configuration.user_class_name}' not found"
91
+ rescue => e
92
+ results[:errors] << "Error validating user class: #{e.message}"
93
+ end
94
+ end
95
+
96
+ def validate_user_model(results)
97
+ begin
98
+ user_class_name = RailsOnboarding.configuration.user_class_name
99
+ user_class = user_class_name.constantize
100
+
101
+ # Check for required behavioral methods (provided by the Onboardable concern).
102
+ # onboarding_completed? and onboarding_current_step are column-backed attribute
103
+ # methods instead - Rails generates those lazily on first instantiation, so
104
+ # instance_methods.include? can't reliably see them here; their underlying
105
+ # columns are already checked in validate_database_columns.
106
+ required_methods = [
107
+ :needs_onboarding?,
108
+ :complete_onboarding!,
109
+ :onboarding_progress_percentage
110
+ ]
111
+
112
+ required_methods.each do |method|
113
+ unless user_class.instance_methods.include?(method)
114
+ results[:errors] << "User model missing required method: #{method}"
115
+ end
116
+ end
117
+ rescue NameError
118
+ # Already reported in validate_user_class
119
+ rescue => e
120
+ results[:warnings] << "Could not validate user model methods: #{e.message}"
121
+ end
122
+ end
123
+
124
+ def validate_application_controller(results)
125
+ if defined?(ApplicationController)
126
+ results[:info] << "ApplicationController found ✓"
127
+
128
+ # Check if ControllerHelpers are included
129
+ if ApplicationController.included_modules.include?(RailsOnboarding::ControllerHelpers)
130
+ results[:info] << "ControllerHelpers included ✓"
131
+ else
132
+ results[:warnings] << "RailsOnboarding::ControllerHelpers not included in ApplicationController (this is automatic in engine mode)"
133
+ end
134
+ else
135
+ results[:warnings] << "ApplicationController not found (unusual but may be intentional)"
136
+ end
137
+ rescue => e
138
+ results[:warnings] << "Error checking ApplicationController: #{e.message}"
139
+ end
140
+
141
+ def validate_current_user_method(results)
142
+ if defined?(ApplicationController)
143
+ if ApplicationController.instance_methods.include?(:current_user)
144
+ results[:info] << "current_user method available ✓"
145
+ else
146
+ results[:errors] << "ApplicationController must have a current_user method (typically provided by authentication system)"
147
+ end
148
+ end
149
+ rescue => e
150
+ results[:warnings] << "Could not validate current_user method: #{e.message}"
151
+ end
152
+
153
+ def validate_database_columns(results)
154
+ begin
155
+ user_class = RailsOnboarding.configuration.user_class_name.constantize
156
+
157
+ return unless user_class.respond_to?(:column_names)
158
+
159
+ required_columns = {
160
+ "onboarding_completed" => :boolean,
161
+ "onboarding_current_step" => :string,
162
+ "onboarding_skipped" => :boolean
163
+ }
164
+
165
+ optional_columns = {
166
+ "onboarding_completed_at" => :datetime,
167
+ "feature_tooltips_shown" => [ :jsonb, :text ],
168
+ "milestones_achieved" => [ :text, :jsonb ],
169
+ "milestone_points" => :integer,
170
+ "last_milestone_at" => :datetime
171
+ }
172
+
173
+ # Check required columns
174
+ required_columns.each do |column_name, expected_type|
175
+ if user_class.column_names.include?(column_name)
176
+ actual_type = user_class.columns_hash[column_name]&.type
177
+ if actual_type.to_s != expected_type.to_s
178
+ results[:warnings] << "Column '#{column_name}' has type #{actual_type}, expected #{expected_type}"
179
+ else
180
+ results[:info] << "Required column '#{column_name}' (#{expected_type}) ✓"
181
+ end
182
+ else
183
+ results[:errors] << "Required database column missing: #{column_name} (#{expected_type})"
184
+ end
185
+ end
186
+
187
+ # Check optional columns
188
+ optional_columns.each do |column_name, expected_types|
189
+ expected_types = Array(expected_types)
190
+ if user_class.column_names.include?(column_name)
191
+ actual_type = user_class.columns_hash[column_name]&.type
192
+ if expected_types.map(&:to_s).include?(actual_type.to_s)
193
+ results[:info] << "Optional column '#{column_name}' (#{actual_type}) ✓"
194
+ else
195
+ results[:warnings] << "Column '#{column_name}' has type #{actual_type}, expected one of #{expected_types.join(', ')}"
196
+ end
197
+ else
198
+ results[:info] << "Optional column '#{column_name}' not present (some features may be unavailable)"
199
+ end
200
+ end
201
+ rescue NameError
202
+ # Already reported in validate_user_class
203
+ rescue => e
204
+ results[:warnings] << "Could not validate database columns: #{e.message}"
205
+ end
206
+ end
207
+
208
+ def validate_routes(results)
209
+ if defined?(Rails.application)
210
+ routes = Rails.application.routes.routes.map(&:name).compact
211
+
212
+ if routes.include?("rails_onboarding")
213
+ results[:info] << "Engine mounted in routes ✓"
214
+ else
215
+ results[:errors] << "RailsOnboarding engine not mounted in routes. Add: mount RailsOnboarding::Engine => '/onboarding'"
216
+ end
217
+ end
218
+ rescue => e
219
+ results[:warnings] << "Could not validate routes: #{e.message}"
220
+ end
221
+
222
+ def validate_optional_dependencies(results)
223
+ # Check for optional gems
224
+ optional_gems = {
225
+ "ActiveJob" => -> { defined?(::ActiveJob::Base) },
226
+ "ActionMailer" => -> { defined?(::ActionMailer::Base) },
227
+ "Devise" => -> { defined?(Devise) },
228
+ "Turbo" => -> { defined?(Turbo) },
229
+ "Stimulus" => -> { defined?(Stimulus) },
230
+ "Noticed" => -> { defined?(Noticed) },
231
+ "ActiveModel::Serializers" => -> { defined?(ActiveModel::Serializer) }
232
+ }
233
+
234
+ optional_gems.each do |gem_name, check|
235
+ if check.call
236
+ results[:info] << "Optional: #{gem_name} available ✓"
237
+ else
238
+ results[:info] << "Optional: #{gem_name} not available (some features may be limited)"
239
+ end
240
+ end
241
+ rescue => e
242
+ results[:warnings] << "Error checking optional dependencies: #{e.message}"
243
+ end
244
+
245
+ def validate_configuration(results)
246
+ config = RailsOnboarding.configuration
247
+
248
+ # Check critical configuration
249
+ unless config.steps.any?
250
+ results[:errors] << "No onboarding steps configured. Add steps in config/initializers/rails_onboarding.rb"
251
+ else
252
+ results[:info] << "#{config.steps.size} onboarding step(s) configured ✓"
253
+ end
254
+
255
+ # Check step names are unique
256
+ step_names = config.steps.map { |s| s[:name] }
257
+ duplicates = step_names.select { |name| step_names.count(name) > 1 }.uniq
258
+ if duplicates.any?
259
+ results[:errors] << "Duplicate step names found: #{duplicates.join(', ')}"
260
+ end
261
+
262
+ # Check redirect paths
263
+ if config.redirect_after_completion.nil?
264
+ results[:warnings] << "No redirect_after_completion configured (users will stay on onboarding page)"
265
+ end
266
+
267
+ # Validate mailer configuration if emails are enabled
268
+ if RailsOnboarding::BackgroundJobs.background_job_options[:enable_emails] && !RailsOnboarding.action_mailer_configured?
269
+ results[:warnings] << "Emails enabled but ActionMailer not properly configured"
270
+ end
271
+ rescue => e
272
+ results[:warnings] << "Error validating configuration: #{e.message}"
273
+ end
274
+
275
+ def format_error_message(results)
276
+ message = "\n" + "=" * 80 + "\n"
277
+ message += "RailsOnboarding Requirements Validation Failed\n"
278
+ message += "=" * 80 + "\n\n"
279
+
280
+ if results[:errors].any?
281
+ message += "ERRORS (must be fixed):\n"
282
+ results[:errors].each { |error| message += " ✗ #{error}\n" }
283
+ message += "\n"
284
+ end
285
+
286
+ if results[:warnings].any?
287
+ message += "WARNINGS (should be addressed):\n"
288
+ results[:warnings].each { |warning| message += " ⚠ #{warning}\n" }
289
+ message += "\n"
290
+ end
291
+
292
+ message += "Please fix the errors above before using RailsOnboarding.\n"
293
+ message += "See documentation: https://github.com/bunnahabhain/rails_onboarding\n"
294
+ message += "=" * 80 + "\n"
295
+
296
+ message
297
+ end
298
+
299
+ def log_results(results)
300
+ return unless defined?(Rails.logger)
301
+
302
+ Rails.logger.info "RailsOnboarding requirements validation passed ✓"
303
+
304
+ if results[:warnings].any?
305
+ Rails.logger.warn "RailsOnboarding has #{results[:warnings].size} warning(s):"
306
+ results[:warnings].each { |warning| Rails.logger.warn " ⚠ #{warning}" }
307
+ end
308
+
309
+ if results[:info].any? && Rails.env.development?
310
+ results[:info].each { |info| Rails.logger.debug " #{info}" }
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOnboarding
4
+ # Responsive layout helper module
5
+ # Provides helper methods for implementing responsive design features in the host application
6
+ module ResponsiveHelper
7
+ # Returns the viewport meta tag for responsive design
8
+ # This should be included in the host application's layout
9
+ #
10
+ # @param options [Hash] Optional configuration
11
+ # @option options [String] :width The viewport width (default: 'device-width')
12
+ # @option options [Float] :initial_scale The initial zoom level (default: 1.0)
13
+ # @option options [Float] :maximum_scale The maximum zoom level (default: 5.0)
14
+ # @option options [Float] :minimum_scale The minimum zoom level (default: 1.0)
15
+ # @option options [Boolean] :user_scalable Whether user can zoom (default: true)
16
+ # @option options [String] :viewport_fit How to handle notches/safe areas (default: 'cover')
17
+ #
18
+ # @return [String] The viewport meta tag HTML
19
+ #
20
+ # @example Basic usage in a layout
21
+ # <%= rails_onboarding_viewport_meta %>
22
+ #
23
+ # @example Custom configuration
24
+ # <%= rails_onboarding_viewport_meta(maximum_scale: 3.0, viewport_fit: 'contain') %>
25
+ def rails_onboarding_viewport_meta(options = {})
26
+ width = options[:width] || "device-width"
27
+ initial_scale = options[:initial_scale] || 1.0
28
+ maximum_scale = options[:maximum_scale] || 5.0
29
+ minimum_scale = options[:minimum_scale] || 1.0
30
+ user_scalable = options.fetch(:user_scalable, true) ? "yes" : "no"
31
+ viewport_fit = options[:viewport_fit] || "cover"
32
+
33
+ content = [
34
+ "width=#{width}",
35
+ "initial-scale=#{initial_scale}",
36
+ "maximum-scale=#{maximum_scale}",
37
+ "minimum-scale=#{minimum_scale}",
38
+ "user-scalable=#{user_scalable}",
39
+ "viewport-fit=#{viewport_fit}"
40
+ ].join(", ")
41
+
42
+ tag(:meta, name: "viewport", content: content)
43
+ end
44
+
45
+ # Returns theme color meta tag for mobile browsers
46
+ #
47
+ # @param color [String] The theme color (default: primary color from config)
48
+ # @return [String] The theme-color meta tag HTML
49
+ #
50
+ # @example
51
+ # <%= rails_onboarding_theme_color %>
52
+ # <%= rails_onboarding_theme_color('#6366f1') %>
53
+ def rails_onboarding_theme_color(color = nil)
54
+ color ||= "#6366f1" # Default primary color
55
+ tag(:meta, name: "theme-color", content: color)
56
+ end
57
+
58
+ # Checks if the current device is mobile based on user agent
59
+ #
60
+ # @return [Boolean] True if mobile device
61
+ def mobile_device?
62
+ return false unless request.present?
63
+
64
+ user_agent = request.user_agent.to_s.downcase
65
+ mobile_patterns = [
66
+ /mobile/,
67
+ /android/,
68
+ /iphone/,
69
+ /ipad/,
70
+ /ipod/,
71
+ /blackberry/,
72
+ /windows phone/,
73
+ /opera mini/,
74
+ /kindle/,
75
+ /silk/
76
+ ]
77
+
78
+ mobile_patterns.any? { |pattern| user_agent.match?(pattern) }
79
+ end
80
+
81
+ # Checks if the current device is a tablet
82
+ #
83
+ # @return [Boolean] True if tablet device
84
+ def tablet_device?
85
+ return false unless request.present?
86
+
87
+ user_agent = request.user_agent.to_s.downcase
88
+ tablet_patterns = [ /ipad/, /tablet/, /kindle/, /silk/, /playbook/ ]
89
+
90
+ tablet_patterns.any? { |pattern| user_agent.match?(pattern) }
91
+ end
92
+
93
+ # Checks if the current device is a phone
94
+ #
95
+ # @return [Boolean] True if phone device
96
+ def phone_device?
97
+ mobile_device? && !tablet_device?
98
+ end
99
+
100
+ # Returns CSS classes for responsive context
101
+ #
102
+ # @return [String] Space-separated CSS classes
103
+ #
104
+ # @example In a view
105
+ # <div class="<%= responsive_device_classes %>">
106
+ def responsive_device_classes
107
+ classes = []
108
+ classes << "is-mobile" if mobile_device?
109
+ classes << "is-tablet" if tablet_device?
110
+ classes << "is-phone" if phone_device?
111
+ classes << "is-desktop" unless mobile_device?
112
+ classes.join(" ")
113
+ end
114
+
115
+ # Returns data attributes for JavaScript device detection
116
+ #
117
+ # @return [Hash] Data attributes for HTML tags
118
+ #
119
+ # @example In a view
120
+ # <div <%= responsive_device_data %>>
121
+ def responsive_device_data
122
+ {
123
+ "data-mobile" => mobile_device?,
124
+ "data-tablet" => tablet_device?,
125
+ "data-phone" => phone_device?
126
+ }
127
+ end
128
+
129
+ # Apple-specific meta tags for iOS devices
130
+ #
131
+ # @return [String] Combined meta tags for iOS
132
+ #
133
+ # @example In a layout
134
+ # <%= rails_onboarding_ios_meta %>
135
+ def rails_onboarding_ios_meta
136
+ tags = []
137
+
138
+ # Enable web app capable mode
139
+ tags << tag(:meta, name: "apple-mobile-web-app-capable", content: "yes")
140
+
141
+ # Status bar style
142
+ tags << tag(:meta, name: "apple-mobile-web-app-status-bar-style", content: "default")
143
+
144
+ # Disable phone number detection
145
+ tags << tag(:meta, name: "format-detection", content: "telephone=no")
146
+
147
+ safe_join(tags, "\n")
148
+ end
149
+
150
+ # Returns manifest link for PWA support
151
+ #
152
+ # @param manifest_path [String] Path to manifest.json
153
+ # @return [String] Link tag for manifest
154
+ #
155
+ # @example
156
+ # <%= rails_onboarding_manifest_link %>
157
+ def rails_onboarding_manifest_link(manifest_path = "/manifest.json")
158
+ tag(:link, rel: "manifest", href: manifest_path)
159
+ end
160
+
161
+ # Helper to render responsive images
162
+ #
163
+ # @param src [String] Base image source
164
+ # @param options [Hash] Image options
165
+ # @option options [String] :alt Alt text
166
+ # @option options [Hash] :srcset Responsive image sources
167
+ # @option options [String] :sizes Media query sizes
168
+ #
169
+ # @example
170
+ # <%= responsive_image('welcome.jpg',
171
+ # alt: 'Welcome',
172
+ # srcset: { '1x' => 'welcome.jpg', '2x' => 'welcome@2x.jpg' },
173
+ # sizes: '(max-width: 768px) 100vw, 50vw') %>
174
+ def responsive_image(src, options = {})
175
+ alt = options[:alt] || ""
176
+ srcset = options[:srcset]
177
+ sizes = options[:sizes]
178
+ css_class = options[:class] || ""
179
+
180
+ img_options = { src: src, alt: alt, class: css_class }
181
+
182
+ if srcset.present?
183
+ srcset_value = srcset.map { |density, path| "#{path} #{density}" }.join(", ")
184
+ img_options[:srcset] = srcset_value
185
+ end
186
+
187
+ img_options[:sizes] = sizes if sizes.present?
188
+
189
+ tag(:img, img_options)
190
+ end
191
+
192
+ # Returns appropriate loading attribute for images
193
+ #
194
+ # @param eager [Boolean] Whether to load eagerly
195
+ # @return [String] Loading attribute value
196
+ def image_loading_strategy(eager: false)
197
+ eager ? "eager" : "lazy"
198
+ end
199
+
200
+ # Checks if the viewport supports hover (desktop/laptop)
201
+ #
202
+ # @return [Boolean] True if hover is supported
203
+ # Note: This is a server-side estimation; client-side detection is more accurate
204
+ def supports_hover?
205
+ !mobile_device?
206
+ end
207
+
208
+ # Returns touch-friendly attributes for interactive elements
209
+ #
210
+ # @return [Hash] Data attributes for touch optimization
211
+ def touch_friendly_attrs
212
+ {
213
+ "data-touch-enabled" => mobile_device?,
214
+ "style" => mobile_device? ? "touch-action: manipulation;" : nil
215
+ }.compact
216
+ end
217
+ end
218
+ end