@umituz/web-dashboard 5.0.1 → 6.1.0

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 (120) hide show
  1. package/package.json +19 -17
  2. package/src/domains/onboarding/calc/calc.test.ts +181 -0
  3. package/src/domains/onboarding/calc/card-stack-motion-curve.ts +33 -0
  4. package/src/domains/onboarding/calc/count-with-locale.ts +16 -0
  5. package/src/domains/onboarding/calc/duration-equal-split.ts +24 -0
  6. package/src/domains/onboarding/calc/elapsed-millis.ts +12 -0
  7. package/src/domains/onboarding/calc/index.ts +36 -0
  8. package/src/domains/onboarding/calc/progress-percentage.ts +37 -0
  9. package/src/domains/onboarding/calc/slider-value-bounds.ts +44 -0
  10. package/src/domains/onboarding/calc/step-index-clamp.ts +28 -0
  11. package/src/domains/onboarding/calc/swipe-gesture-curve.ts +67 -0
  12. package/src/domains/onboarding/components/OnboardingFlow.test.tsx +481 -0
  13. package/src/domains/onboarding/components/OnboardingFlow.tsx +361 -0
  14. package/src/domains/onboarding/components/OnboardingIntl.tsx +108 -0
  15. package/src/domains/onboarding/components/hooks/index.ts +7 -0
  16. package/src/domains/onboarding/components/hooks/useOnboardingAnalytics.test.tsx +74 -0
  17. package/src/domains/onboarding/components/hooks/useOnboardingAnalytics.ts +62 -0
  18. package/src/domains/onboarding/components/hooks/useOnboardingProgress.test.tsx +150 -0
  19. package/src/domains/onboarding/components/hooks/useOnboardingProgress.ts +260 -0
  20. package/src/domains/onboarding/components/hooks/usePermissionPriming.ts +52 -0
  21. package/src/domains/onboarding/components/hooks/useReducedMotion.ts +24 -0
  22. package/src/domains/onboarding/components/screens/AccountGateScreen.tsx +89 -0
  23. package/src/domains/onboarding/components/screens/AppDemoScreen.tsx +39 -0
  24. package/src/domains/onboarding/components/screens/ComparisonScreen.tsx +59 -0
  25. package/src/domains/onboarding/components/screens/GoalQuestionScreen.tsx +56 -0
  26. package/src/domains/onboarding/components/screens/PainPointsScreen.tsx +65 -0
  27. package/src/domains/onboarding/components/screens/PaywallScreen.tsx +138 -0
  28. package/src/domains/onboarding/components/screens/PermissionPrimingScreen.tsx +113 -0
  29. package/src/domains/onboarding/components/screens/PersonalizedSolutionScreen.tsx +53 -0
  30. package/src/domains/onboarding/components/screens/PreferencesScreen.tsx +194 -0
  31. package/src/domains/onboarding/components/screens/ProcessingScreen.tsx +79 -0
  32. package/src/domains/onboarding/components/screens/SocialProofScreen.tsx +116 -0
  33. package/src/domains/onboarding/components/screens/TinderCardsScreen.tsx +108 -0
  34. package/src/domains/onboarding/components/screens/ValueDeliveryScreen.tsx +85 -0
  35. package/src/domains/onboarding/components/screens/WelcomeScreen.tsx +52 -0
  36. package/src/domains/onboarding/components/screens/index.ts +28 -0
  37. package/src/domains/onboarding/components/shared/AnimationWrapper.tsx +73 -0
  38. package/src/domains/onboarding/components/shared/OnboardingErrorBoundary.tsx +81 -0
  39. package/src/domains/onboarding/components/shared/OptionCard.tsx +73 -0
  40. package/src/domains/onboarding/components/shared/PrimaryButton.tsx +58 -0
  41. package/src/domains/onboarding/components/shared/ProgressBar.tsx +51 -0
  42. package/src/domains/onboarding/components/shared/QuestionWrapper.tsx +36 -0
  43. package/src/domains/onboarding/components/shared/ScreenContainer.tsx +29 -0
  44. package/src/domains/onboarding/components/shared/SwipeCard.test.tsx +51 -0
  45. package/src/domains/onboarding/components/shared/SwipeCard.tsx +106 -0
  46. package/src/domains/onboarding/components/shared/index.ts +16 -0
  47. package/src/domains/onboarding/components/shared/renderBundledIcon.tsx +20 -0
  48. package/src/domains/onboarding/index.ts +182 -0
  49. package/src/domains/onboarding/onboarding.css +1 -0
  50. package/src/domains/onboarding/predicates/has-handler.ts +25 -0
  51. package/src/domains/onboarding/predicates/icon-name-resolves.ts +113 -0
  52. package/src/domains/onboarding/predicates/index.ts +26 -0
  53. package/src/domains/onboarding/predicates/permission-granted.ts +26 -0
  54. package/src/domains/onboarding/predicates/permission-supports.ts +66 -0
  55. package/src/domains/onboarding/predicates/predicates.test.ts +202 -0
  56. package/src/domains/onboarding/predicates/screen-emits-data.ts +17 -0
  57. package/src/domains/onboarding/predicates/storage-available.ts +22 -0
  58. package/src/domains/onboarding/templates/TemplateDemoFallback.tsx +112 -0
  59. package/src/domains/onboarding/templates/art-onboarding.ts +81 -0
  60. package/src/domains/onboarding/templates/health-onboarding.ts +97 -0
  61. package/src/domains/onboarding/templates/index.ts +6 -0
  62. package/src/domains/onboarding/templates/platform-onboarding.ts +210 -0
  63. package/src/domains/onboarding/templates/video-onboarding.ts +100 -0
  64. package/src/domains/onboarding/test/setup.ts +30 -0
  65. package/src/domains/onboarding/theme/cn.ts +10 -0
  66. package/src/domains/onboarding/theme/index.css +131 -0
  67. package/src/domains/onboarding/theme/index.ts +12 -0
  68. package/src/domains/onboarding/theme/tokens.ts +122 -0
  69. package/src/domains/onboarding/types/config.ts +68 -0
  70. package/src/domains/onboarding/types/index.ts +10 -0
  71. package/src/domains/onboarding/types/labels.ts +83 -0
  72. package/src/domains/onboarding/types/screens.ts +193 -0
  73. package/src/domains/onboarding/utils/analytics-dispatch.test.ts +93 -0
  74. package/src/domains/onboarding/utils/analytics-dispatch.ts +92 -0
  75. package/src/domains/onboarding/utils/default-storage-key.ts +2 -0
  76. package/src/domains/onboarding/utils/index.ts +13 -0
  77. package/src/domains/onboarding/utils/local-storage-bridge.test.ts +93 -0
  78. package/src/domains/onboarding/utils/local-storage-bridge.ts +104 -0
  79. package/src/domains/onboarding/utils/permission-bridge.test.ts +64 -0
  80. package/src/domains/onboarding/utils/permission-bridge.ts +95 -0
  81. package/src/domains/onboarding/validation/flow-structure-check.ts +28 -0
  82. package/src/domains/onboarding/validation/index.ts +9 -0
  83. package/src/domains/onboarding/validation/permission-status-check.ts +23 -0
  84. package/src/domains/onboarding/validation/screen-config-check.ts +105 -0
  85. package/src/domains/onboarding/validation/stored-progress-check.ts +68 -0
  86. package/src/domains/onboarding/validation/validation.test.ts +175 -0
  87. package/src/index.ts +0 -10
  88. package/src/styles/dashboard.contract.test.ts +1 -0
  89. package/src/styles/dashboard.css +0 -460
  90. package/src/domains/auth/components/AuthLayout.tsx +0 -99
  91. package/src/domains/auth/components/ForgotPasswordForm.tsx +0 -167
  92. package/src/domains/auth/components/LoginForm.tsx +0 -222
  93. package/src/domains/auth/components/RegisterForm.tsx +0 -281
  94. package/src/domains/auth/components/ResetPasswordForm.tsx +0 -173
  95. package/src/domains/auth/components/index.ts +0 -11
  96. package/src/domains/auth/components/shared/AuthErrorBanner.tsx +0 -25
  97. package/src/domains/auth/components/shared/EmailInput.tsx +0 -73
  98. package/src/domains/auth/components/shared/PasswordInput.tsx +0 -100
  99. package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +0 -63
  100. package/src/domains/auth/components/shared/SocialLoginButtons.tsx +0 -44
  101. package/src/domains/auth/components/shared/SocialProviderIcons.tsx +0 -48
  102. package/src/domains/auth/components/shared/index.ts +0 -20
  103. package/src/domains/auth/hooks/index.ts +0 -8
  104. package/src/domains/auth/hooks/useAuth.ts +0 -230
  105. package/src/domains/auth/index.ts +0 -62
  106. package/src/domains/auth/types/auth.ts +0 -302
  107. package/src/domains/auth/types/index.ts +0 -25
  108. package/src/domains/auth/utils/auth.test.ts +0 -188
  109. package/src/domains/auth/utils/auth.ts +0 -221
  110. package/src/domains/auth/utils/i18nKeys.ts +0 -109
  111. package/src/domains/auth/utils/index.ts +0 -29
  112. package/src/domains/auth/utils/secureToken.ts +0 -33
  113. package/src/domains/auth/utils/validationKeys.ts +0 -23
  114. package/src/domains/session/DemoLogin.tsx +0 -42
  115. package/src/domains/session/OrgSwitcher.tsx +0 -102
  116. package/src/domains/session/SessionContext.tsx +0 -150
  117. package/src/domains/session/inMemoryAuthAdapter.ts +0 -53
  118. package/src/domains/session/index.ts +0 -18
  119. package/src/domains/session/types.ts +0 -47
  120. package/src/domains/session/utils/i18nKeys.ts +0 -12
@@ -0,0 +1,97 @@
1
+ import type { OnboardingConfig } from '../types/config';
2
+ import type { OnboardingScreen } from '../types/screens';
3
+
4
+ /**
5
+ * Health category — privacy-first, 5–7 screens.
6
+ * Trust is the #1 conversion lever: lead with privacy reassurance.
7
+ */
8
+
9
+ const screens: OnboardingScreen[] = [
10
+ {
11
+ id: 'welcome',
12
+ type: 'welcome',
13
+ title: 'Your health, your data, your call',
14
+ subtitle: 'Private by design. Always encrypted on-device.',
15
+ cta: 'Get started',
16
+ heroIconName: 'ShieldCheck',
17
+ highlight: 'HIPAA-ready',
18
+ },
19
+ {
20
+ id: 'goal',
21
+ type: 'goal-question',
22
+ question: 'What brings you here today?',
23
+ multiSelect: false,
24
+ options: [
25
+ { id: 'sleep', label: 'Better sleep', iconName: 'Moon' },
26
+ { id: 'fitness', label: 'Get fitter', iconName: 'Dumbbell' },
27
+ { id: 'nutrition', label: 'Eat healthier', iconName: 'Apple' },
28
+ { id: 'stress', label: 'Manage stress', iconName: 'HeartPulse' },
29
+ ],
30
+ onChange: () => undefined,
31
+ cta: 'Continue',
32
+ },
33
+ {
34
+ id: 'privacy',
35
+ type: 'pain-points',
36
+ title: 'What worries you most about health apps?',
37
+ painPoints: [
38
+ { id: 'data-shared', label: 'Data shared with insurers' },
39
+ { id: 'tracking', label: 'Being tracked across apps' },
40
+ { id: 'ads', label: 'Seeing health ads' },
41
+ { id: 'leaks', label: 'Data breaches' },
42
+ ],
43
+ multiSelect: true,
44
+ onChange: () => undefined,
45
+ cta: 'Continue',
46
+ },
47
+ {
48
+ id: 'permission-priming-notifications',
49
+ type: 'permission-priming',
50
+ permission: 'notifications',
51
+ title: 'Gentle daily reminders',
52
+ description: 'We only ping you when it matters — never spammy.',
53
+ benefits: [
54
+ 'Reminders you can mute any time',
55
+ 'On-device scheduling — nothing leaves your phone',
56
+ 'One-tap unsubscribe from any category',
57
+ ],
58
+ iconName: 'Bell',
59
+ onRequest: async () => 'prompt',
60
+ cta: 'Allow reminders',
61
+ },
62
+ {
63
+ id: 'processing',
64
+ type: 'processing',
65
+ title: 'Building your wellness plan…',
66
+ steps: ['Reading your goals', 'Filtering privacy settings', 'Personalizing the plan'],
67
+ durationMs: 3000,
68
+ onComplete: () => undefined,
69
+ },
70
+ {
71
+ id: 'paywall',
72
+ type: 'paywall',
73
+ title: 'Unlock your full plan',
74
+ subtitle: 'Most users see results in their first week.',
75
+ price: '$6.99 / month',
76
+ trialDays: 7,
77
+ trialPrice: 'Free',
78
+ cta: 'Start 7-day trial',
79
+ secondaryCta: 'Maybe later',
80
+ features: [
81
+ { iconName: 'Lock', text: 'End-to-end encrypted data' },
82
+ { iconName: 'Activity', text: 'Personalized daily plan' },
83
+ { iconName: 'MessageCircle', text: '24/7 human coaching' },
84
+ { iconName: 'Award', text: 'Progress you can share (or not)' },
85
+ ],
86
+ socialProof: { text: 'Trusted by', count: 250000 },
87
+ onSecondaryCta: () => undefined,
88
+ onAccept: async () => ({ accepted: true, trialStarted: true }),
89
+ },
90
+ ];
91
+
92
+ export const healthOnboardingTemplate: OnboardingConfig = {
93
+ category: 'health',
94
+ screens,
95
+ onSkip: () => undefined,
96
+ onComplete: () => undefined,
97
+ };
@@ -0,0 +1,6 @@
1
+ export { healthOnboardingTemplate } from './health-onboarding';
2
+ export { artOnboardingTemplate } from './art-onboarding';
3
+ export { videoOnboardingTemplate } from './video-onboarding';
4
+ export { platformOnboardingTemplate } from './platform-onboarding';
5
+ export { withDefaultDemo, defaultDemoResult, TemplateDemoFallback } from './TemplateDemoFallback';
6
+ export type { TemplateDemoResult } from './TemplateDemoFallback';
@@ -0,0 +1,210 @@
1
+ import type { OnboardingConfig } from '../types/config';
2
+ import type { OnboardingScreen } from '../types/screens';
3
+ import { withDefaultDemo, defaultDemoResult } from './TemplateDemoFallback';
4
+
5
+ /**
6
+ * Platform category — full 14-screen flow.
7
+ * Use for multi-feature platforms where personalization drives retention.
8
+ *
9
+ * The app-demo screen ships with a working palette-preview fallback so the
10
+ * template is runnable end-to-end out of the box.
11
+ */
12
+
13
+ const screens: OnboardingScreen[] = [
14
+ {
15
+ id: 'welcome',
16
+ type: 'welcome',
17
+ title: 'Welcome aboard',
18
+ subtitle: 'A few questions and we’ll tailor the entire experience.',
19
+ cta: 'Start',
20
+ heroIconName: 'Rocket',
21
+ highlight: '~2 minutes',
22
+ },
23
+ {
24
+ id: 'goals',
25
+ type: 'goal-question',
26
+ question: 'What do you want to get out of this?',
27
+ multiSelect: true,
28
+ options: [
29
+ { id: 'productivity', label: 'Be more productive', iconName: 'Zap' },
30
+ { id: 'learn', label: 'Learn new skills', iconName: 'GraduationCap' },
31
+ { id: 'connect', label: 'Connect with people', iconName: 'Users' },
32
+ { id: 'create', label: 'Create content', iconName: 'PenTool' },
33
+ ],
34
+ onChange: () => undefined,
35
+ cta: 'Continue',
36
+ },
37
+ {
38
+ id: 'pains',
39
+ type: 'pain-points',
40
+ title: 'What frustrates you about tools like this?',
41
+ painPoints: [
42
+ { id: 'complex', label: 'Too complex' },
43
+ { id: 'slow', label: 'Too slow' },
44
+ { id: 'pricey', label: 'Too expensive' },
45
+ { id: 'privacy', label: 'Privacy concerns' },
46
+ ],
47
+ multiSelect: true,
48
+ maxSelect: 3,
49
+ onChange: () => undefined,
50
+ cta: 'Continue',
51
+ },
52
+ {
53
+ id: 'proof',
54
+ type: 'social-proof',
55
+ title: 'Why people stay',
56
+ testimonials: [
57
+ {
58
+ name: 'Maya K.',
59
+ role: 'Designer',
60
+ content: 'The best $10/mo I spend. Period.',
61
+ rating: 5,
62
+ },
63
+ {
64
+ name: 'Andre P.',
65
+ role: 'PM',
66
+ content: 'I onboarded my whole team in a day.',
67
+ rating: 5,
68
+ },
69
+ {
70
+ name: 'Sara L.',
71
+ role: 'Founder',
72
+ content: 'Replaced three other tools for me.',
73
+ rating: 4,
74
+ },
75
+ ],
76
+ },
77
+ {
78
+ id: 'tinder',
79
+ type: 'tinder-cards',
80
+ title: 'A bit about you',
81
+ statements: [
82
+ { id: 'a', text: 'I prefer learning by doing.', category: 'Style' },
83
+ { id: 'b', text: 'I read every doc end-to-end.', category: 'Style' },
84
+ { id: 'c', text: 'I collaborate with others daily.', category: 'Style' },
85
+ { id: 'd', text: 'I work alone most of the time.', category: 'Style' },
86
+ ],
87
+ onChange: () => undefined,
88
+ cta: 'Continue',
89
+ },
90
+ {
91
+ id: 'personalized',
92
+ type: 'personalized-solution',
93
+ title: 'Built for the way you work',
94
+ subtitle: 'Here is what we’ll set up for you.',
95
+ solutions: [
96
+ { painPointId: 'complex', solution: 'A guided first-run experience with smart defaults.' },
97
+ { painPointId: 'slow', solution: 'Sub-200ms interactions, no spinner purgatory.' },
98
+ { painPointId: 'pricey', solution: 'A free tier that actually does the job.' },
99
+ { painPointId: 'privacy', solution: 'You own your data — export and delete anytime.' },
100
+ ],
101
+ selectedPainPointIds: [],
102
+ },
103
+ {
104
+ id: 'comparison',
105
+ type: 'comparison',
106
+ title: 'Before vs. after',
107
+ before: ['Tool overload', 'Generic templates', 'Steep learning curve'],
108
+ after: ['One workspace', 'Personalized setup', '2-minute onboarding'],
109
+ },
110
+ {
111
+ id: 'prefs',
112
+ type: 'preferences',
113
+ title: 'A few preferences',
114
+ preferences: [
115
+ {
116
+ id: 'theme',
117
+ label: 'Theme',
118
+ type: 'select',
119
+ defaultValue: 'system',
120
+ options: [
121
+ { id: 'system', label: 'Match system' },
122
+ { id: 'light', label: 'Light' },
123
+ { id: 'dark', label: 'Dark' },
124
+ ],
125
+ },
126
+ {
127
+ id: 'density',
128
+ label: 'Density',
129
+ type: 'select',
130
+ defaultValue: 'comfortable',
131
+ options: [
132
+ { id: 'compact', label: 'Compact' },
133
+ { id: 'comfortable', label: 'Comfortable' },
134
+ ],
135
+ },
136
+ ],
137
+ onChange: () => undefined,
138
+ cta: 'Continue',
139
+ },
140
+ {
141
+ id: 'permission-notifications',
142
+ type: 'permission-priming',
143
+ permission: 'notifications',
144
+ title: 'Stay in the loop',
145
+ description: 'Only the alerts you actually want.',
146
+ benefits: ['Mentions & replies', 'Weekly summary', 'Customizable quiet hours'],
147
+ iconName: 'Bell',
148
+ onRequest: async () => 'prompt',
149
+ cta: 'Enable notifications',
150
+ },
151
+ {
152
+ id: 'processing',
153
+ type: 'processing',
154
+ title: 'Setting up your workspace…',
155
+ steps: ['Tuning recommendations', 'Importing defaults', 'Warming caches'],
156
+ durationMs: 3500,
157
+ onComplete: () => undefined,
158
+ },
159
+ {
160
+ id: 'demo',
161
+ type: 'app-demo',
162
+ title: 'Quick tour — try a quick action',
163
+ demo: { component: () => null, onSave: () => undefined },
164
+ },
165
+ {
166
+ id: 'value',
167
+ type: 'value-delivery',
168
+ title: 'You’re all set',
169
+ result: defaultDemoResult(),
170
+ },
171
+ {
172
+ id: 'account',
173
+ type: 'account-gate',
174
+ title: 'Save your progress',
175
+ subtitle: 'Sign in to sync across devices.',
176
+ optional: true,
177
+ signInMethod: 'email',
178
+ skipText: 'Continue without account',
179
+ onSignIn: async () => ({ signedIn: true, method: 'email' }),
180
+ },
181
+ {
182
+ id: 'paywall',
183
+ type: 'paywall',
184
+ title: 'Unlock the full platform',
185
+ subtitle: 'Pricing that scales with you.',
186
+ price: '$14.99 / month',
187
+ trialDays: 14,
188
+ cta: 'Start 14-day trial',
189
+ secondaryCta: 'Stay on free',
190
+ features: [
191
+ { iconName: 'Infinity', text: 'Unlimited projects' },
192
+ { iconName: 'Users', text: 'Team workspaces' },
193
+ { iconName: 'Shield', text: 'Enterprise SSO' },
194
+ { iconName: 'Headphones', text: 'Priority support' },
195
+ ],
196
+ socialProof: { text: 'Joined by', count: 850000 },
197
+ onSecondaryCta: () => undefined,
198
+ onAccept: async () => ({ accepted: true, trialStarted: true }),
199
+ },
200
+ ];
201
+
202
+ // Replace the no-op demo component with the built-in working fallback.
203
+ const resolvedScreens = screens.map((s) => withDefaultDemo(s));
204
+
205
+ export const platformOnboardingTemplate: OnboardingConfig = {
206
+ category: 'platform',
207
+ screens: resolvedScreens,
208
+ onSkip: () => undefined,
209
+ onComplete: () => undefined,
210
+ };
@@ -0,0 +1,100 @@
1
+ import type { OnboardingConfig } from '../types/config';
2
+ import type { OnboardingScreen } from '../types/screens';
3
+
4
+ /**
5
+ * Video category — personalization-led, 6–8 screens.
6
+ * Goal: tune the recommendation engine early.
7
+ */
8
+
9
+ const screens: OnboardingScreen[] = [
10
+ {
11
+ id: 'welcome',
12
+ type: 'welcome',
13
+ title: 'Watch what you actually love',
14
+ subtitle: 'A few quick questions and we’ll never recommend junk again.',
15
+ cta: 'Let’s go',
16
+ heroIconName: 'Play',
17
+ },
18
+ {
19
+ id: 'genres',
20
+ type: 'goal-question',
21
+ question: 'Which genres do you enjoy?',
22
+ multiSelect: true,
23
+ options: [
24
+ { id: 'comedy', label: 'Comedy', iconName: 'Smile' },
25
+ { id: 'drama', label: 'Drama', iconName: 'Theater' },
26
+ { id: 'documentary', label: 'Documentary', iconName: 'BookOpen' },
27
+ { id: 'thriller', label: 'Thriller', iconName: 'AlertTriangle' },
28
+ { id: 'sci-fi', label: 'Sci-fi', iconName: 'Rocket' },
29
+ ],
30
+ onChange: () => undefined,
31
+ cta: 'Continue',
32
+ },
33
+ {
34
+ id: 'mood',
35
+ type: 'tinder-cards',
36
+ title: 'Swipe on what feels right',
37
+ statements: [
38
+ { id: 's1', text: 'I want to learn something new today.', category: 'Curious' },
39
+ { id: 's2', text: 'I want pure entertainment.', category: 'Fun' },
40
+ { id: 's3', text: 'I want something to cry to.', category: 'Emotional' },
41
+ { id: 's4', text: 'I want background noise while I work.', category: 'Casual' },
42
+ ],
43
+ onChange: () => undefined,
44
+ cta: 'Continue',
45
+ },
46
+ {
47
+ id: 'length',
48
+ type: 'preferences',
49
+ title: 'How long should a video be?',
50
+ preferences: [
51
+ {
52
+ id: 'length',
53
+ label: 'Preferred length',
54
+ type: 'slider',
55
+ min: 1,
56
+ max: 120,
57
+ defaultValue: 20,
58
+ },
59
+ ],
60
+ onChange: () => undefined,
61
+ cta: 'Continue',
62
+ },
63
+ {
64
+ id: 'permission-priming-notifications',
65
+ type: 'permission-priming',
66
+ permission: 'notifications',
67
+ title: 'Never miss a live premiere',
68
+ description: 'Get a heads-up when channels you love go live.',
69
+ benefits: ['Only from channels you follow', 'Snooze any time', 'Silent by default'],
70
+ iconName: 'Bell',
71
+ onRequest: async () => 'prompt',
72
+ cta: 'Turn on alerts',
73
+ },
74
+ {
75
+ id: 'paywall',
76
+ type: 'paywall',
77
+ title: 'Watch without limits',
78
+ subtitle: 'Ad-free, offline downloads, 4K.',
79
+ price: '$11.99 / month',
80
+ trialDays: 14,
81
+ cta: 'Start 14-day trial',
82
+ secondaryCta: 'Continue with ads',
83
+ features: [
84
+ { iconName: 'Tv', text: '4K Ultra HD' },
85
+ { iconName: 'Download', text: 'Offline downloads' },
86
+ { iconName: 'Users', text: '4 screens at once' },
87
+ { iconName: 'Ban', text: 'Zero ads' },
88
+ ],
89
+ socialProof: { text: 'Loved by', count: 1200000 },
90
+ onSecondaryCta: () => undefined,
91
+ onAccept: async () => ({ accepted: true, trialStarted: true }),
92
+ },
93
+ ];
94
+
95
+ export const videoOnboardingTemplate: OnboardingConfig = {
96
+ category: 'video',
97
+ screens,
98
+ onSkip: () => undefined,
99
+ onComplete: () => undefined,
100
+ };
@@ -0,0 +1,30 @@
1
+ import { afterEach, vi } from 'vitest';
2
+ import { cleanup } from '@testing-library/react';
3
+
4
+ // React 18 testing-library contract: let `act` warnings be raised correctly.
5
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
6
+
7
+ // jsdom has no matchMedia. Provide a mock that reports reduced motion:
8
+ // jsdom also lacks animation frames, so framer-motion's AnimatePresence can
9
+ // never complete an exit animation here — the instant-transition path is the
10
+ // only one that can be exercised in CI (and it is the a11y-correct path,
11
+ // TEST_SCENARIOS.md §1.10).
12
+ if (typeof window !== 'undefined')
13
+ Object.defineProperty(window, 'matchMedia', {
14
+ writable: true,
15
+ value: (query: string): MediaQueryList =>
16
+ ({
17
+ matches: query.includes('prefers-reduced-motion'),
18
+ media: query,
19
+ onchange: null,
20
+ addListener: vi.fn(), // deprecated API — kept for older libraries
21
+ removeListener: vi.fn(),
22
+ addEventListener: vi.fn(),
23
+ removeEventListener: vi.fn(),
24
+ dispatchEvent: vi.fn(),
25
+ }) as unknown as MediaQueryList,
26
+ });
27
+
28
+ afterEach(() => {
29
+ cleanup();
30
+ });
@@ -0,0 +1,10 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ /**
5
+ * Class name composer — merges Tailwind utility classes safely.
6
+ * Single responsibility: build a className string without conflicts.
7
+ */
8
+ export function cn(...inputs: ClassValue[]): string {
9
+ return twMerge(clsx(inputs));
10
+ }
@@ -0,0 +1,131 @@
1
+ /*
2
+ * web-onboarding styles.
3
+ *
4
+ * Isolation contract (see README "Styling"):
5
+ * - NO Tailwind preflight. Host applications keep their own global reset;
6
+ * this stylesheet must never change styles outside `.uob-flow`.
7
+ * - A scoped reset restores the preflight invariants the library's utility
8
+ * classes rely on (box-sizing, border-style, form font inheritance)
9
+ * inside `.uob-flow` only.
10
+ * - All design tokens are namespaced `--uob-*`. Rebrand by overriding them:
11
+ * .uob-flow { --uob-primary-500: 34 197 94; }
12
+ * RGB triplets (e.g. "99 102 241"), not hex — they compose with
13
+ * `rgb(var(--uob-primary-500) / <alpha>)` and Tailwind `<alpha-value>`.
14
+ * - OnboardingFlow renders the `.uob-flow` wrapper automatically.
15
+ */
16
+
17
+ /* ------------------------------------------------------------------ */
18
+ /* Design tokens (light defaults) */
19
+ /* ------------------------------------------------------------------ */
20
+
21
+ .uob-flow {
22
+ --uob-font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
23
+ 'Segoe UI', Roboto, sans-serif;
24
+
25
+ --uob-primary-50: 238 242 255;
26
+ --uob-primary-100: 224 231 255;
27
+ --uob-primary-500: 99 102 241;
28
+ --uob-primary-600: 79 70 229;
29
+ --uob-primary-700: 67 56 202;
30
+
31
+ --uob-surface: 255 255 255;
32
+ --uob-surface-muted: 249 250 251;
33
+ --uob-text-primary: 17 24 39;
34
+ --uob-text-muted: 107 114 128;
35
+ --uob-border: 229 231 235;
36
+
37
+ --uob-success: 16 185 129;
38
+ --uob-warning: 245 158 11;
39
+ --uob-error: 239 68 68;
40
+ }
41
+
42
+ @media (prefers-color-scheme: dark) {
43
+ .uob-flow {
44
+ --uob-surface: 17 24 39;
45
+ --uob-surface-muted: 31 41 55;
46
+ --uob-text-primary: 243 244 246;
47
+ --uob-text-muted: 156 163 175;
48
+ --uob-border: 55 65 81;
49
+ }
50
+ }
51
+
52
+ /* ------------------------------------------------------------------ */
53
+ /* Scoped reset — restores preflight invariants inside `.uob-flow` */
54
+ /* only. Deliberately emitted BEFORE the utilities layer so utility */
55
+ /* classes always win on equal specificity. */
56
+ /* ------------------------------------------------------------------ */
57
+
58
+ .uob-flow,
59
+ .uob-flow *,
60
+ .uob-flow *::before,
61
+ .uob-flow *::after {
62
+ box-sizing: border-box;
63
+ border-width: 0;
64
+ border-style: solid;
65
+ border-color: rgb(var(--uob-border));
66
+ }
67
+
68
+ .uob-flow h1,
69
+ .uob-flow h2,
70
+ .uob-flow h3,
71
+ .uob-flow h4,
72
+ .uob-flow h5,
73
+ .uob-flow h6,
74
+ .uob-flow p,
75
+ .uob-flow figure,
76
+ .uob-flow blockquote,
77
+ .uob-flow pre {
78
+ margin: 0;
79
+ }
80
+
81
+ .uob-flow ol,
82
+ .uob-flow ul {
83
+ list-style: none;
84
+ margin: 0;
85
+ padding: 0;
86
+ }
87
+
88
+ .uob-flow button,
89
+ .uob-flow input,
90
+ .uob-flow optgroup,
91
+ .uob-flow select,
92
+ .uob-flow textarea {
93
+ font-family: inherit;
94
+ font-size: 100%;
95
+ font-weight: inherit;
96
+ line-height: inherit;
97
+ color: inherit;
98
+ margin: 0;
99
+ padding: 0;
100
+ }
101
+
102
+ .uob-flow button,
103
+ .uob-flow [type='button'],
104
+ .uob-flow [type='reset'],
105
+ .uob-flow [type='submit'] {
106
+ -webkit-appearance: button;
107
+ background-color: transparent;
108
+ background-image: none;
109
+ }
110
+
111
+ .uob-flow progress {
112
+ -webkit-appearance: none;
113
+ appearance: none;
114
+ }
115
+
116
+ .uob-flow img,
117
+ .uob-flow svg,
118
+ .uob-flow video,
119
+ .uob-flow canvas {
120
+ display: block;
121
+ vertical-align: middle;
122
+ }
123
+
124
+ .uob-flow img,
125
+ .uob-flow video {
126
+ max-width: 100%;
127
+ height: auto;
128
+ }
129
+
130
+ /* Utilities LAST — emitted in place, after the scoped reset. */
131
+ @tailwind utilities;
@@ -0,0 +1,12 @@
1
+ export {
2
+ spacing,
3
+ radius,
4
+ typography,
5
+ zIndex,
6
+ motion,
7
+ breakpoint,
8
+ colorCssVar,
9
+ } from './tokens';
10
+ export { cn } from './cn';
11
+
12
+ export type { SpacingToken, RadiusToken, FontSizeToken } from './tokens';