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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1351 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
- data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
- data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
- data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
- data/app/assets/javascripts/rails_onboarding/application.js +231 -0
- data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
- data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
- data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
- data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
- data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
- data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
- data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
- data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
- data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
- data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
- data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
- data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
- data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
- data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
- data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
- data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
- data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
- data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
- data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
- data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
- data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
- data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
- data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
- data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
- data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
- data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
- data/app/controllers/rails_onboarding/application_controller.rb +9 -0
- data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
- data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
- data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
- data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
- data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
- data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
- data/app/helpers/rails_onboarding/application_helper.rb +4 -0
- data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
- data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
- data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
- data/app/jobs/rails_onboarding/application_job.rb +4 -0
- data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
- data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
- data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
- data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
- data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
- data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
- data/app/models/rails_onboarding/analytics.rb +255 -0
- data/app/models/rails_onboarding/analytics_event.rb +168 -0
- data/app/models/rails_onboarding/application_record.rb +5 -0
- data/app/models/rails_onboarding/flow.rb +41 -0
- data/app/services/rails_onboarding/milestone_service.rb +105 -0
- data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
- data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
- data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
- data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
- data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
- data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
- data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
- data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
- data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
- data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
- data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
- data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
- data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
- data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
- data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
- data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
- data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
- data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
- data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
- data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
- data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
- data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
- data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
- data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
- data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
- data/app/views/rails_onboarding/templates/index.html.erb +255 -0
- data/config/importmap.rb +33 -0
- data/config/locales/en.yml +62 -0
- data/config/locales/es.yml +62 -0
- data/config/locales/fr.yml +62 -0
- data/config/routes.rb +79 -0
- data/lib/generators/rails_onboarding/install_generator.rb +124 -0
- data/lib/generators/rails_onboarding/templates/README +120 -0
- data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
- data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
- data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
- data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
- data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
- data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
- data/lib/generators/rails_onboarding/update_generator.rb +73 -0
- data/lib/rails_onboarding/api_mode.rb +367 -0
- data/lib/rails_onboarding/backfill.rb +130 -0
- data/lib/rails_onboarding/background_jobs.rb +412 -0
- data/lib/rails_onboarding/caching.rb +180 -0
- data/lib/rails_onboarding/cdn_support.rb +202 -0
- data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
- data/lib/rails_onboarding/configuration/analytics.rb +29 -0
- data/lib/rails_onboarding/configuration/integrations.rb +31 -0
- data/lib/rails_onboarding/configuration/milestones.rb +92 -0
- data/lib/rails_onboarding/configuration/personalization.rb +25 -0
- data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
- data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
- data/lib/rails_onboarding/configuration/steps.rb +121 -0
- data/lib/rails_onboarding/configuration/templates.rb +85 -0
- data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
- data/lib/rails_onboarding/configuration.rb +133 -0
- data/lib/rails_onboarding/configuration_errors.rb +19 -0
- data/lib/rails_onboarding/configuration_validator.rb +477 -0
- data/lib/rails_onboarding/controller_helpers.rb +153 -0
- data/lib/rails_onboarding/current.rb +13 -0
- data/lib/rails_onboarding/deprecation.rb +64 -0
- data/lib/rails_onboarding/devise_integration.rb +122 -0
- data/lib/rails_onboarding/engine.rb +245 -0
- data/lib/rails_onboarding/error_recovery.rb +173 -0
- data/lib/rails_onboarding/lazy_loading.rb +155 -0
- data/lib/rails_onboarding/multi_tenant.rb +306 -0
- data/lib/rails_onboarding/requirements_validator.rb +315 -0
- data/lib/rails_onboarding/responsive_helper.rb +218 -0
- data/lib/rails_onboarding/session_manager.rb +187 -0
- data/lib/rails_onboarding/skip_logic.rb +159 -0
- data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
- data/lib/rails_onboarding/version.rb +3 -0
- data/lib/rails_onboarding.rb +44 -0
- data/lib/tasks/rails_onboarding_analytics.rake +136 -0
- data/lib/tasks/rails_onboarding_tasks.rake +162 -0
- 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
|