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,393 @@
1
+ /* ======================
2
+ Utility Classes
3
+ ====================== */
4
+
5
+ /* Spacing utilities */
6
+ .onboarding-mt-xs { margin-top: var(--onboarding-space-xs); }
7
+ .onboarding-mt-sm { margin-top: var(--onboarding-space-sm); }
8
+ .onboarding-mt-md { margin-top: var(--onboarding-space-md); }
9
+ .onboarding-mt-lg { margin-top: var(--onboarding-space-lg); }
10
+ .onboarding-mt-xl { margin-top: var(--onboarding-space-xl); }
11
+ .onboarding-mt-2xl { margin-top: var(--onboarding-space-2xl); }
12
+
13
+ .onboarding-mb-xs { margin-bottom: var(--onboarding-space-xs); }
14
+ .onboarding-mb-sm { margin-bottom: var(--onboarding-space-sm); }
15
+ .onboarding-mb-md { margin-bottom: var(--onboarding-space-md); }
16
+ .onboarding-mb-lg { margin-bottom: var(--onboarding-space-lg); }
17
+ .onboarding-mb-xl { margin-bottom: var(--onboarding-space-xl); }
18
+ .onboarding-mb-2xl { margin-bottom: var(--onboarding-space-2xl); }
19
+
20
+ .onboarding-ml-xs { margin-left: var(--onboarding-space-xs); }
21
+ .onboarding-ml-sm { margin-left: var(--onboarding-space-sm); }
22
+ .onboarding-ml-md { margin-left: var(--onboarding-space-md); }
23
+ .onboarding-ml-lg { margin-left: var(--onboarding-space-lg); }
24
+ .onboarding-ml-xl { margin-left: var(--onboarding-space-xl); }
25
+ .onboarding-ml-2xl { margin-left: var(--onboarding-space-2xl); }
26
+
27
+ .onboarding-mr-xs { margin-right: var(--onboarding-space-xs); }
28
+ .onboarding-mr-sm { margin-right: var(--onboarding-space-sm); }
29
+ .onboarding-mr-md { margin-right: var(--onboarding-space-md); }
30
+ .onboarding-mr-lg { margin-right: var(--onboarding-space-lg); }
31
+ .onboarding-mr-xl { margin-right: var(--onboarding-space-xl); }
32
+ .onboarding-mr-2xl { margin-right: var(--onboarding-space-2xl); }
33
+
34
+ .onboarding-pt-xs { padding-top: var(--onboarding-space-xs); }
35
+ .onboarding-pt-sm { padding-top: var(--onboarding-space-sm); }
36
+ .onboarding-pt-md { padding-top: var(--onboarding-space-md); }
37
+ .onboarding-pt-lg { padding-top: var(--onboarding-space-lg); }
38
+ .onboarding-pt-xl { padding-top: var(--onboarding-space-xl); }
39
+ .onboarding-pt-2xl { padding-top: var(--onboarding-space-2xl); }
40
+
41
+ .onboarding-pb-xs { padding-bottom: var(--onboarding-space-xs); }
42
+ .onboarding-pb-sm { padding-bottom: var(--onboarding-space-sm); }
43
+ .onboarding-pb-md { padding-bottom: var(--onboarding-space-md); }
44
+ .onboarding-pb-lg { padding-bottom: var(--onboarding-space-lg); }
45
+ .onboarding-pb-xl { padding-bottom: var(--onboarding-space-xl); }
46
+ .onboarding-pb-2xl { padding-bottom: var(--onboarding-space-2xl); }
47
+
48
+ .onboarding-pl-xs { padding-left: var(--onboarding-space-xs); }
49
+ .onboarding-pl-sm { padding-left: var(--onboarding-space-sm); }
50
+ .onboarding-pl-md { padding-left: var(--onboarding-space-md); }
51
+ .onboarding-pl-lg { padding-left: var(--onboarding-space-lg); }
52
+ .onboarding-pl-xl { padding-left: var(--onboarding-space-xl); }
53
+ .onboarding-pl-2xl { padding-left: var(--onboarding-space-2xl); }
54
+
55
+ .onboarding-pr-xs { padding-right: var(--onboarding-space-xs); }
56
+ .onboarding-pr-sm { padding-right: var(--onboarding-space-sm); }
57
+ .onboarding-pr-md { padding-right: var(--onboarding-space-md); }
58
+ .onboarding-pr-lg { padding-right: var(--onboarding-space-lg); }
59
+ .onboarding-pr-xl { padding-right: var(--onboarding-space-xl); }
60
+ .onboarding-pr-2xl { padding-right: var(--onboarding-space-2xl); }
61
+
62
+ /* Text utilities */
63
+ .onboarding-text-xs { font-size: var(--onboarding-text-xs); }
64
+ .onboarding-text-sm { font-size: var(--onboarding-text-sm); }
65
+ .onboarding-text-base { font-size: var(--onboarding-text-base); }
66
+ .onboarding-text-lg { font-size: var(--onboarding-text-lg); }
67
+ .onboarding-text-xl { font-size: var(--onboarding-text-xl); }
68
+ .onboarding-text-2xl { font-size: var(--onboarding-text-2xl); }
69
+ .onboarding-text-3xl { font-size: var(--onboarding-text-3xl); }
70
+
71
+ .onboarding-text-left { text-align: left; }
72
+ .onboarding-text-center { text-align: center; }
73
+ .onboarding-text-right { text-align: right; }
74
+
75
+ .onboarding-text-normal { font-weight: 400; }
76
+ .onboarding-text-medium { font-weight: 500; }
77
+ .onboarding-text-semibold { font-weight: 600; }
78
+ .onboarding-text-bold { font-weight: 700; }
79
+
80
+ .onboarding-text-primary { color: var(--onboarding-text); }
81
+ .onboarding-text-muted { color: var(--onboarding-text-muted); }
82
+ .onboarding-text-light { color: var(--onboarding-text-light); }
83
+ .onboarding-text-brand { color: var(--onboarding-primary); }
84
+ .onboarding-text-success { color: var(--onboarding-success); }
85
+ .onboarding-text-warning { color: var(--onboarding-warning); }
86
+ .onboarding-text-danger { color: var(--onboarding-danger); }
87
+
88
+ /* Display utilities */
89
+ .onboarding-hidden { display: none; }
90
+ .onboarding-block { display: block; }
91
+ .onboarding-inline { display: inline; }
92
+ .onboarding-inline-block { display: inline-block; }
93
+ .onboarding-flex { display: flex; }
94
+ .onboarding-grid { display: grid; }
95
+
96
+ /* Flex utilities */
97
+ .onboarding-flex-row { flex-direction: row; }
98
+ .onboarding-flex-col { flex-direction: column; }
99
+ .onboarding-flex-wrap { flex-wrap: wrap; }
100
+ .onboarding-flex-nowrap { flex-wrap: nowrap; }
101
+
102
+ .onboarding-justify-start { justify-content: flex-start; }
103
+ .onboarding-justify-center { justify-content: center; }
104
+ .onboarding-justify-end { justify-content: flex-end; }
105
+ .onboarding-justify-between { justify-content: space-between; }
106
+ .onboarding-justify-around { justify-content: space-around; }
107
+
108
+ .onboarding-items-start { align-items: flex-start; }
109
+ .onboarding-items-center { align-items: center; }
110
+ .onboarding-items-end { align-items: flex-end; }
111
+ .onboarding-items-stretch { align-items: stretch; }
112
+
113
+ .onboarding-flex-1 { flex: 1; }
114
+ .onboarding-flex-auto { flex: auto; }
115
+ .onboarding-flex-none { flex: none; }
116
+
117
+ /* Grid utilities */
118
+ .onboarding-grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
119
+ .onboarding-grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
120
+ .onboarding-grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
121
+ .onboarding-grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
122
+
123
+ .onboarding-gap-xs { gap: var(--onboarding-space-xs); }
124
+ .onboarding-gap-sm { gap: var(--onboarding-space-sm); }
125
+ .onboarding-gap-md { gap: var(--onboarding-space-md); }
126
+ .onboarding-gap-lg { gap: var(--onboarding-space-lg); }
127
+ .onboarding-gap-xl { gap: var(--onboarding-space-xl); }
128
+ .onboarding-gap-2xl { gap: var(--onboarding-space-2xl); }
129
+
130
+ /* Position utilities */
131
+ .onboarding-relative { position: relative; }
132
+ .onboarding-absolute { position: absolute; }
133
+ .onboarding-fixed { position: fixed; }
134
+ .onboarding-sticky { position: sticky; }
135
+
136
+ /* Border utilities */
137
+ .onboarding-border { border: 1px solid var(--onboarding-border); }
138
+ .onboarding-border-t { border-top: 1px solid var(--onboarding-border); }
139
+ .onboarding-border-b { border-bottom: 1px solid var(--onboarding-border); }
140
+ .onboarding-border-l { border-left: 1px solid var(--onboarding-border); }
141
+ .onboarding-border-r { border-right: 1px solid var(--onboarding-border); }
142
+
143
+ .onboarding-rounded-none { border-radius: 0; }
144
+ .onboarding-rounded-sm { border-radius: 0.25rem; }
145
+ .onboarding-rounded { border-radius: 0.5rem; }
146
+ .onboarding-rounded-lg { border-radius: 0.75rem; }
147
+ .onboarding-rounded-xl { border-radius: 1rem; }
148
+ .onboarding-rounded-full { border-radius: 9999px; }
149
+
150
+ /* Background utilities */
151
+ .onboarding-bg-transparent { background-color: transparent; }
152
+ .onboarding-bg-primary { background-color: var(--onboarding-background); }
153
+ .onboarding-bg-surface { background-color: var(--onboarding-surface); }
154
+ .onboarding-bg-elevated { background-color: var(--onboarding-surface-elevated); }
155
+ .onboarding-bg-brand { background-color: var(--onboarding-primary); }
156
+ .onboarding-bg-success { background-color: var(--onboarding-success); }
157
+ .onboarding-bg-warning { background-color: var(--onboarding-warning); }
158
+ .onboarding-bg-danger { background-color: var(--onboarding-danger); }
159
+
160
+ /* Shadow utilities */
161
+ .onboarding-shadow-none { box-shadow: none; }
162
+ .onboarding-shadow-sm { box-shadow: var(--onboarding-shadow-sm); }
163
+ .onboarding-shadow { box-shadow: var(--onboarding-shadow-md); }
164
+ .onboarding-shadow-lg { box-shadow: var(--onboarding-shadow-lg); }
165
+
166
+ /* Width and height utilities */
167
+ .onboarding-w-full { width: 100%; }
168
+ .onboarding-w-auto { width: auto; }
169
+ .onboarding-h-full { height: 100%; }
170
+ .onboarding-h-auto { height: auto; }
171
+
172
+ .onboarding-max-w-xs { max-width: 20rem; }
173
+ .onboarding-max-w-sm { max-width: 24rem; }
174
+ .onboarding-max-w-md { max-width: 28rem; }
175
+ .onboarding-max-w-lg { max-width: 32rem; }
176
+ .onboarding-max-w-xl { max-width: 36rem; }
177
+ .onboarding-max-w-2xl { max-width: 42rem; }
178
+ .onboarding-max-w-full { max-width: 100%; }
179
+
180
+ /* Interaction utilities */
181
+ .onboarding-cursor-pointer { cursor: pointer; }
182
+ .onboarding-cursor-help { cursor: help; }
183
+ .onboarding-cursor-not-allowed { cursor: not-allowed; }
184
+
185
+ .onboarding-select-none { user-select: none; }
186
+ .onboarding-select-text { user-select: text; }
187
+ .onboarding-select-all { user-select: all; }
188
+
189
+ /* Transition utilities */
190
+ .onboarding-transition { transition: var(--onboarding-transition); }
191
+ .onboarding-transition-none { transition: none; }
192
+
193
+ /* Opacity utilities */
194
+ .onboarding-opacity-0 { opacity: 0; }
195
+ .onboarding-opacity-25 { opacity: 0.25; }
196
+ .onboarding-opacity-50 { opacity: 0.5; }
197
+ .onboarding-opacity-75 { opacity: 0.75; }
198
+ .onboarding-opacity-100 { opacity: 1; }
199
+
200
+ /* Visibility utilities */
201
+ .onboarding-visible { visibility: visible; }
202
+ .onboarding-invisible { visibility: hidden; }
203
+
204
+ /* Z-index utilities */
205
+ .onboarding-z-0 { z-index: 0; }
206
+ .onboarding-z-10 { z-index: 10; }
207
+ .onboarding-z-20 { z-index: 20; }
208
+ .onboarding-z-30 { z-index: 30; }
209
+ .onboarding-z-40 { z-index: 40; }
210
+ .onboarding-z-50 { z-index: 50; }
211
+
212
+ /* Special utility classes */
213
+ .onboarding-sr-only {
214
+ position: absolute;
215
+ width: 1px;
216
+ height: 1px;
217
+ padding: 0;
218
+ margin: -1px;
219
+ overflow: hidden;
220
+ clip: rect(0, 0, 0, 0);
221
+ white-space: nowrap;
222
+ border: 0;
223
+ }
224
+
225
+ .onboarding-not-sr-only {
226
+ position: static;
227
+ width: auto;
228
+ height: auto;
229
+ padding: 0;
230
+ margin: 0;
231
+ overflow: visible;
232
+ clip: auto;
233
+ white-space: normal;
234
+ }
235
+
236
+ /* Focus utilities */
237
+ .onboarding-focus-ring:focus {
238
+ outline: none;
239
+ box-shadow: var(--onboarding-focus-ring);
240
+ }
241
+
242
+ .onboarding-focus-visible:focus-visible {
243
+ outline: 2px solid var(--onboarding-primary);
244
+ outline-offset: 2px;
245
+ }
246
+
247
+ /* Animation utilities */
248
+ .onboarding-animate-fade-in {
249
+ animation: onboarding-fadeInUp 0.3s ease-out;
250
+ }
251
+
252
+ .onboarding-animate-bounce {
253
+ animation: onboarding-bounce 1s infinite;
254
+ }
255
+
256
+ .onboarding-animate-pulse {
257
+ animation: onboarding-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
258
+ }
259
+
260
+ @keyframes onboarding-bounce {
261
+ 0%, 100% {
262
+ transform: translateY(-25%);
263
+ animation-timing-function: cubic-bezier(0.8,0,1,1);
264
+ }
265
+ 50% {
266
+ transform: none;
267
+ animation-timing-function: cubic-bezier(0,0,0.2,1);
268
+ }
269
+ }
270
+
271
+ @keyframes onboarding-pulse {
272
+ 0%, 100% {
273
+ opacity: 1;
274
+ }
275
+ 50% {
276
+ opacity: .5;
277
+ }
278
+ }
279
+
280
+ /* Responsive utilities */
281
+ @media screen and (max-width: 48em) {
282
+ .onboarding-sm\:hidden { display: none; }
283
+ .onboarding-sm\:block { display: block; }
284
+ .onboarding-sm\:flex { display: flex; }
285
+ .onboarding-sm\:grid { display: grid; }
286
+
287
+ .onboarding-sm\:flex-col { flex-direction: column; }
288
+ .onboarding-sm\:text-center { text-align: center; }
289
+ .onboarding-sm\:text-left { text-align: left; }
290
+
291
+ .onboarding-sm\:w-full { width: 100%; }
292
+ .onboarding-sm\:max-w-full { max-width: 100%; }
293
+
294
+ .onboarding-sm\:grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
295
+ .onboarding-sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
296
+ }
297
+
298
+ @media screen and (max-width: 30em) {
299
+ .onboarding-xs\:hidden { display: none; }
300
+ .onboarding-xs\:block { display: block; }
301
+ .onboarding-xs\:flex { display: flex; }
302
+
303
+ .onboarding-xs\:text-sm { font-size: var(--onboarding-text-sm); }
304
+ .onboarding-xs\:text-center { text-align: center; }
305
+
306
+ .onboarding-xs\:p-sm { padding: var(--onboarding-space-sm); }
307
+ .onboarding-xs\:p-md { padding: var(--onboarding-space-md); }
308
+
309
+ .onboarding-xs\:grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
310
+ }
311
+
312
+ /* Loading state utilities */
313
+ .onboarding-loading {
314
+ position: relative;
315
+ pointer-events: none;
316
+ }
317
+
318
+ .onboarding-loading::before {
319
+ content: '';
320
+ position: absolute;
321
+ top: 0;
322
+ left: 0;
323
+ right: 0;
324
+ bottom: 0;
325
+ background: rgba(255, 255, 255, 0.8);
326
+ border-radius: inherit;
327
+ z-index: 1;
328
+ }
329
+
330
+ .onboarding-loading::after {
331
+ content: '';
332
+ position: absolute;
333
+ top: 50%;
334
+ left: 50%;
335
+ width: 1.5rem;
336
+ height: 1.5rem;
337
+ margin: -0.75rem 0 0 -0.75rem;
338
+ border: 2px solid var(--onboarding-border);
339
+ border-top-color: var(--onboarding-primary);
340
+ border-radius: 50%;
341
+ animation: onboarding-spin 1s linear infinite;
342
+ z-index: 2;
343
+ }
344
+
345
+ @keyframes onboarding-spin {
346
+ to {
347
+ transform: rotate(360deg);
348
+ }
349
+ }
350
+
351
+ /* Error state utilities */
352
+ .onboarding-error {
353
+ border-color: var(--onboarding-danger);
354
+ background-color: #fef2f2;
355
+ }
356
+
357
+ .onboarding-error:focus {
358
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
359
+ }
360
+
361
+ .onboarding-success {
362
+ border-color: var(--onboarding-success);
363
+ background-color: #f0fdf4;
364
+ }
365
+
366
+ .onboarding-success:focus {
367
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
368
+ }
369
+
370
+ /* High contrast mode utilities */
371
+ @media (prefers-contrast: high) {
372
+ .onboarding-hc-border {
373
+ border: 2px solid currentColor;
374
+ }
375
+
376
+ .onboarding-hc-text {
377
+ color: currentColor;
378
+ }
379
+ }
380
+
381
+ /* Reduced motion utilities */
382
+ @media (prefers-reduced-motion: reduce) {
383
+ .onboarding-reduced-motion {
384
+ animation: none;
385
+ transition: none;
386
+ }
387
+
388
+ .onboarding-animate-fade-in,
389
+ .onboarding-animate-bounce,
390
+ .onboarding-animate-pulse {
391
+ animation: none;
392
+ }
393
+ }
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOnboarding
4
+ # Admin gate shared by the Admin dashboard controllers and by the standalone
5
+ # AbTests/Templates controllers, which are admin-only but live outside the
6
+ # Admin namespace. Provides the authentication/authorization before_action
7
+ # targets plus the handler methods used to turn a denial into a friendly
8
+ # redirect or error response.
9
+ #
10
+ # This concern intentionally does NOT declare any `rescue_from` handlers of
11
+ # its own. Handler *methods* live here, but each controller declares its own
12
+ # `rescue_from ..., with:` so the registration order stays under that
13
+ # controller's control - `rescue_from` resolves most-recently-registered
14
+ # first, so a generic StandardError handler must be registered before the
15
+ # specific ones or it swallows them.
16
+ module AdminAuthorization
17
+ extend ActiveSupport::Concern
18
+
19
+ # Raised when a logged-in user without admin privileges reaches an
20
+ # admin-only action.
21
+ class UnauthorizedError < StandardError; end
22
+
23
+ included do
24
+ helper_method :admin_user?
25
+ end
26
+
27
+ private
28
+
29
+ # Override in the host application to customize admin authentication, e.g.
30
+ # def authenticate_rails_onboarding_admin!
31
+ # redirect_to root_path unless current_user&.admin?
32
+ # end
33
+ def authenticate_admin!
34
+ if defined?(authenticate_rails_onboarding_admin!)
35
+ authenticate_rails_onboarding_admin!
36
+ elsif respond_to?(:current_user, true)
37
+ unless current_user.present?
38
+ flash[:alert] = "You must be logged in to access this page"
39
+ redirect_to main_app.root_path
40
+ return
41
+ end
42
+
43
+ unless current_user.respond_to?(:admin?) && current_user.admin?
44
+ raise UnauthorizedError, "Unauthorized access to admin area"
45
+ end
46
+ else
47
+ raise NotImplementedError,
48
+ "Please define authenticate_rails_onboarding_admin! method in your ApplicationController " \
49
+ "or ensure your User model has an admin? method"
50
+ end
51
+ end
52
+
53
+ def verify_admin_authorization!
54
+ return if admin_user?
55
+
56
+ logger.warn "Unauthorized admin access attempt by #{current_user&.id || 'anonymous'}"
57
+ raise UnauthorizedError, "You do not have permission to perform this action"
58
+ end
59
+
60
+ def admin_user?
61
+ return false unless respond_to?(:current_user, true)
62
+
63
+ current_user&.respond_to?(:admin?) && current_user.admin?
64
+ end
65
+
66
+ # A logged-in non-admin reached an admin-only action.
67
+ def handle_admin_unauthorized(exception)
68
+ logger.warn "Unauthorized admin access: #{exception.message}"
69
+
70
+ respond_to do |format|
71
+ format.html do
72
+ flash[:alert] = "Access denied: #{exception.message}"
73
+ redirect_to main_app.root_path
74
+ end
75
+ format.json do
76
+ render json: { error: "Unauthorized access" }, status: :forbidden
77
+ end
78
+ end
79
+ end
80
+
81
+ # The host app hasn't wired up admin authentication yet - surface the
82
+ # guidance from authenticate_admin! instead of crashing. NotImplementedError
83
+ # is a ScriptError, not a StandardError, so it needs its own handler.
84
+ def handle_admin_not_implemented(exception)
85
+ logger.error "Admin setup error: #{exception.message}"
86
+
87
+ respond_to do |format|
88
+ format.html do
89
+ flash[:alert] = exception.message
90
+ redirect_to main_app.root_path
91
+ end
92
+ format.json do
93
+ render json: { error: exception.message }, status: :internal_server_error
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOnboarding
4
+ # Rate limiting concern for API endpoints
5
+ # Prevents abuse by limiting the number of requests per time period
6
+ module RateLimitable
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ before_action :check_rate_limit, if: :rate_limiting_enabled?
11
+ end
12
+
13
+ private
14
+
15
+ # Check if request exceeds rate limit
16
+ def check_rate_limit
17
+ return unless rate_limiting_enabled?
18
+
19
+ identifier = rate_limit_identifier
20
+ key = rate_limit_cache_key(identifier)
21
+
22
+ # Get current request count
23
+ count = Rails.cache.read(key) || 0
24
+ limit = rate_limit_per_period
25
+
26
+ if count >= limit
27
+ rate_limit_exceeded_response
28
+ return
29
+ end
30
+
31
+ # Increment counter
32
+ Rails.cache.write(
33
+ key,
34
+ count + 1,
35
+ expires_in: rate_limit_period,
36
+ unless_exist: false
37
+ )
38
+
39
+ # Set rate limit headers
40
+ response.set_header("X-RateLimit-Limit", limit.to_s)
41
+ response.set_header("X-RateLimit-Remaining", (limit - count - 1).to_s)
42
+ response.set_header("X-RateLimit-Reset", rate_limit_reset_time.to_i.to_s)
43
+ end
44
+
45
+ # Generate cache key for rate limiting
46
+ def rate_limit_cache_key(identifier)
47
+ period = (Time.current.to_i / rate_limit_period).to_i
48
+ "rails_onboarding:rate_limit:#{identifier}:#{period}"
49
+ end
50
+
51
+ # Identifier for rate limiting (IP address or user ID)
52
+ def rate_limit_identifier
53
+ if respond_to?(:current_user, true) && current_user
54
+ "user:#{current_user.id}"
55
+ else
56
+ "ip:#{request.remote_ip}"
57
+ end
58
+ end
59
+
60
+ # Rate limit configuration
61
+ def rate_limit_per_period
62
+ RailsOnboarding.configuration.rate_limit_per_period || 60
63
+ end
64
+
65
+ def rate_limit_period
66
+ RailsOnboarding.configuration.rate_limit_period || 1.minute
67
+ end
68
+
69
+ def rate_limit_reset_time
70
+ period_seconds = rate_limit_period.to_i
71
+ current_period = (Time.current.to_i / period_seconds).to_i
72
+ Time.at((current_period + 1) * period_seconds)
73
+ end
74
+
75
+ def rate_limiting_enabled?
76
+ RailsOnboarding.configuration.rate_limiting_enabled != false
77
+ end
78
+
79
+ # Response when rate limit is exceeded
80
+ def rate_limit_exceeded_response
81
+ retry_after = (rate_limit_reset_time - Time.current).to_i
82
+ message = "Too many requests. Please retry after #{retry_after} seconds."
83
+
84
+ response.set_header("Retry-After", retry_after.to_s)
85
+ response.set_header("X-RateLimit-Limit", rate_limit_per_period.to_s)
86
+ response.set_header("X-RateLimit-Remaining", "0")
87
+ response.set_header("X-RateLimit-Reset", rate_limit_reset_time.to_i.to_s)
88
+
89
+ if respond_to?(:render_api_error)
90
+ render_api_error(message, status: :too_many_requests)
91
+ return
92
+ end
93
+
94
+ # These controllers are HTML/Turbo-driven, not JSON APIs, so a plain
95
+ # `render json:` here would hand a browser a raw JSON body instead of
96
+ # a page. Respond appropriately to what was actually requested.
97
+ respond_to do |format|
98
+ format.html { redirect_back fallback_location: main_app.root_path, alert: message }
99
+ format.json { render json: { error: "Rate limit exceeded", retry_after: retry_after, message: message }, status: :too_many_requests }
100
+ format.any { head :too_many_requests }
101
+ end
102
+ end
103
+ end
104
+ end