@umituz/web-dashboard 6.0.0 → 6.2.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 (133) hide show
  1. package/README.md +47 -73
  2. package/package.json +15 -7
  3. package/src/config/DashboardConfig.ts +19 -68
  4. package/src/domains/analytics/components/index.ts +0 -6
  5. package/src/domains/analytics/hooks/index.ts +0 -6
  6. package/src/domains/analytics/hooks/useAnalytics.ts +2 -20
  7. package/src/domains/analytics/index.ts +0 -10
  8. package/src/domains/analytics/services/AnalyticsEngineService.ts +7 -74
  9. package/src/domains/analytics/services/PerformanceService.ts +27 -138
  10. package/src/domains/analytics/services/index.ts +0 -4
  11. package/src/domains/analytics/types/analytics.ts +100 -270
  12. package/src/domains/analytics/types/index.ts +0 -6
  13. package/src/domains/analytics/utils/analytics.test.ts +2 -9
  14. package/src/domains/analytics/utils/analytics.ts +1 -22
  15. package/src/domains/analytics/utils/index.ts +0 -6
  16. package/src/domains/calendar/config.ts +0 -7
  17. package/src/domains/calendar/hooks/index.ts +0 -4
  18. package/src/domains/calendar/hooks/useCalendar.ts +7 -27
  19. package/src/domains/calendar/index.ts +0 -4
  20. package/src/domains/calendar/services/CalendarService.ts +0 -21
  21. package/src/domains/calendar/services/index.ts +0 -4
  22. package/src/domains/calendar/types/calendar.types.ts +9 -53
  23. package/src/domains/calendar/utils/contentItemMapper.test.ts +0 -4
  24. package/src/domains/calendar/utils/contentItemMapper.ts +0 -31
  25. package/src/domains/calendar/utils/index.ts +0 -26
  26. package/src/domains/layouts/components/index.ts +0 -4
  27. package/src/domains/layouts/hooks/dashboard.ts +0 -21
  28. package/src/domains/layouts/hooks/index.ts +0 -4
  29. package/src/domains/layouts/index.ts +0 -6
  30. package/src/domains/layouts/theme/default.ts +0 -12
  31. package/src/domains/layouts/theme/index.ts +0 -4
  32. package/src/domains/layouts/theme/presets.ts +0 -9
  33. package/src/domains/layouts/theme/utils.ts +0 -15
  34. package/src/domains/layouts/types/index.ts +0 -4
  35. package/src/domains/layouts/types/layout.ts +18 -56
  36. package/src/domains/layouts/types/notification.ts +4 -17
  37. package/src/domains/layouts/types/sidebar.ts +8 -28
  38. package/src/domains/layouts/types/theme.ts +23 -59
  39. package/src/domains/layouts/types/user.ts +9 -30
  40. package/src/domains/layouts/utils/dashboard.ts +4 -48
  41. package/src/domains/layouts/utils/index.ts +0 -4
  42. package/src/domains/modules/build.tsx +2 -16
  43. package/src/domains/modules/types.ts +8 -23
  44. package/src/domains/onboarding/calc/calc.test.ts +173 -0
  45. package/src/domains/onboarding/calc/card-stack-motion-curve.ts +27 -0
  46. package/src/domains/onboarding/calc/count-with-locale.ts +8 -0
  47. package/src/domains/onboarding/calc/duration-equal-split.ts +12 -0
  48. package/src/domains/onboarding/calc/elapsed-millis.ts +6 -0
  49. package/src/domains/onboarding/calc/index.ts +36 -0
  50. package/src/domains/onboarding/calc/progress-percentage.ts +27 -0
  51. package/src/domains/onboarding/calc/slider-value-bounds.ts +34 -0
  52. package/src/domains/onboarding/calc/step-index-clamp.ts +19 -0
  53. package/src/domains/onboarding/calc/swipe-gesture-curve.ts +54 -0
  54. package/src/domains/onboarding/components/AnimationWrapper.tsx +69 -0
  55. package/src/domains/onboarding/components/OnboardingErrorBoundary.tsx +61 -0
  56. package/src/domains/onboarding/components/OnboardingFlow.test.tsx +459 -0
  57. package/src/domains/onboarding/components/OnboardingFlow.tsx +313 -0
  58. package/src/domains/onboarding/components/OnboardingIntl.tsx +98 -0
  59. package/src/domains/onboarding/components/PrimaryButton.tsx +40 -0
  60. package/src/domains/onboarding/components/ProgressBar.tsx +32 -0
  61. package/src/domains/onboarding/components/QuestionWrapper.tsx +28 -0
  62. package/src/domains/onboarding/components/ScreenContainer.tsx +19 -0
  63. package/src/domains/onboarding/components/SwipeCard.test.tsx +49 -0
  64. package/src/domains/onboarding/components/SwipeCard.tsx +88 -0
  65. package/src/domains/onboarding/components/renderBundledIcon.tsx +7 -0
  66. package/src/domains/onboarding/components/screens/AccountGateScreen.tsx +79 -0
  67. package/src/domains/onboarding/components/screens/AppDemoScreen.tsx +33 -0
  68. package/src/domains/onboarding/components/screens/ComparisonScreen.tsx +49 -0
  69. package/src/domains/onboarding/components/screens/GoalQuestionScreen.tsx +59 -0
  70. package/src/domains/onboarding/components/screens/PainPointsScreen.tsx +65 -0
  71. package/src/domains/onboarding/components/screens/PaywallScreen.tsx +122 -0
  72. package/src/domains/onboarding/components/screens/PermissionPrimingScreen.tsx +98 -0
  73. package/src/domains/onboarding/components/screens/PersonalizedSolutionScreen.tsx +40 -0
  74. package/src/domains/onboarding/components/screens/PreferencesScreen.tsx +180 -0
  75. package/src/domains/onboarding/components/screens/ProcessingScreen.tsx +63 -0
  76. package/src/domains/onboarding/components/screens/SocialProofScreen.tsx +87 -0
  77. package/src/domains/onboarding/components/screens/TinderCardsScreen.tsx +91 -0
  78. package/src/domains/onboarding/components/screens/ValueDeliveryScreen.tsx +69 -0
  79. package/src/domains/onboarding/components/screens/WelcomeScreen.tsx +32 -0
  80. package/src/domains/onboarding/components/screens/index.ts +28 -0
  81. package/src/domains/onboarding/hooks/index.ts +7 -0
  82. package/src/domains/onboarding/hooks/useOnboardingAnalytics.test.tsx +74 -0
  83. package/src/domains/onboarding/hooks/useOnboardingAnalytics.ts +49 -0
  84. package/src/domains/onboarding/hooks/useOnboardingProgress.test.tsx +150 -0
  85. package/src/domains/onboarding/hooks/useOnboardingProgress.ts +249 -0
  86. package/src/domains/onboarding/hooks/usePermissionPriming.ts +45 -0
  87. package/src/domains/onboarding/hooks/useReducedMotion.ts +19 -0
  88. package/src/domains/onboarding/index.ts +153 -0
  89. package/src/domains/onboarding/predicates/has-handler.ts +21 -0
  90. package/src/domains/onboarding/predicates/icon-name-resolves.ts +97 -0
  91. package/src/domains/onboarding/predicates/index.ts +26 -0
  92. package/src/domains/onboarding/predicates/permission-granted.ts +21 -0
  93. package/src/domains/onboarding/predicates/permission-supports.ts +56 -0
  94. package/src/domains/onboarding/predicates/predicates.test.ts +188 -0
  95. package/src/domains/onboarding/predicates/screen-emits-data.ts +13 -0
  96. package/src/domains/onboarding/predicates/storage-available.ts +14 -0
  97. package/src/domains/onboarding/templates/TemplateDemoFallback.tsx +92 -0
  98. package/src/domains/onboarding/templates/art-onboarding.ts +71 -0
  99. package/src/domains/onboarding/templates/health-onboarding.ts +92 -0
  100. package/src/domains/onboarding/templates/index.ts +6 -0
  101. package/src/domains/onboarding/templates/platform-onboarding.ts +201 -0
  102. package/src/domains/onboarding/templates/video-onboarding.ts +95 -0
  103. package/src/domains/onboarding/test/setup.ts +23 -0
  104. package/src/domains/onboarding/types/config.ts +57 -0
  105. package/src/domains/onboarding/types/index.ts +10 -0
  106. package/src/domains/onboarding/types/labels.ts +49 -0
  107. package/src/domains/onboarding/types/screens.ts +185 -0
  108. package/src/domains/onboarding/utils/analytics-dispatch.test.ts +93 -0
  109. package/src/domains/onboarding/utils/analytics-dispatch.ts +73 -0
  110. package/src/domains/onboarding/utils/default-storage-key.ts +1 -0
  111. package/src/domains/onboarding/utils/index.ts +12 -0
  112. package/src/domains/onboarding/utils/local-storage-bridge.test.ts +93 -0
  113. package/src/domains/onboarding/utils/local-storage-bridge.ts +84 -0
  114. package/src/domains/onboarding/utils/permission-bridge.test.ts +64 -0
  115. package/src/domains/onboarding/utils/permission-bridge.ts +87 -0
  116. package/src/domains/onboarding/validation/flow-structure-check.ts +22 -0
  117. package/src/domains/onboarding/validation/index.ts +9 -0
  118. package/src/domains/onboarding/validation/permission-status-check.ts +18 -0
  119. package/src/domains/onboarding/validation/screen-config-check.ts +100 -0
  120. package/src/domains/onboarding/validation/stored-progress-check.ts +59 -0
  121. package/src/domains/onboarding/validation/validation.test.ts +170 -0
  122. package/src/domains/permissions/hasPermission.ts +0 -8
  123. package/src/domains/permissions/types.ts +0 -20
  124. package/src/domains/settings/components/index.ts +0 -4
  125. package/src/domains/settings/hooks/index.ts +0 -4
  126. package/src/domains/settings/hooks/useSettings.ts +1 -26
  127. package/src/domains/settings/index.ts +0 -6
  128. package/src/domains/settings/types/index.ts +0 -4
  129. package/src/domains/settings/types/settings.ts +23 -67
  130. package/src/domains/settings/utils/index.ts +0 -4
  131. package/src/domains/settings/utils/settings.ts +0 -45
  132. package/src/styles/dashboard.contract.test.ts +1 -0
  133. package/src/styles/dashboard.css +818 -1
@@ -0,0 +1,97 @@
1
+ import type { LucideIcon } from 'lucide-react';
2
+ import {
3
+ Activity,
4
+ AlertTriangle,
5
+ Apple,
6
+ Award,
7
+ Ban,
8
+ Bell,
9
+ BookOpen,
10
+ Camera,
11
+ Check,
12
+ CheckCircle2,
13
+ Download,
14
+ Dumbbell,
15
+ GraduationCap,
16
+ Headphones,
17
+ HeartPulse,
18
+ Image,
19
+ Infinity as InfinityIcon,
20
+ Lock,
21
+ MapPin,
22
+ MessageCircle,
23
+ Mic,
24
+ Moon,
25
+ Palette,
26
+ PenTool,
27
+ Play,
28
+ Rocket,
29
+ Rewind,
30
+ Share2,
31
+ Shield,
32
+ ShieldCheck,
33
+ Smile,
34
+ Sparkles,
35
+ Square,
36
+ Star,
37
+ Theater,
38
+ Tv,
39
+ Users,
40
+ X,
41
+ Zap,
42
+ } from 'lucide-react';
43
+
44
+ const ICON_REGISTRY: Readonly<Record<string, LucideIcon>> = {
45
+ Activity,
46
+ AlertTriangle,
47
+ Apple,
48
+ Award,
49
+ Ban,
50
+ Bell,
51
+ BookOpen,
52
+ Camera,
53
+ Check,
54
+ CheckCircle2,
55
+ Download,
56
+ Dumbbell,
57
+ GraduationCap,
58
+ Headphones,
59
+ HeartPulse,
60
+ Image,
61
+ Infinity: InfinityIcon,
62
+ Lock,
63
+ MapPin,
64
+ MessageCircle,
65
+ Mic,
66
+ Moon,
67
+ Palette,
68
+ PenTool,
69
+ Play,
70
+ Rocket,
71
+ Rewind,
72
+ Share2,
73
+ Shield,
74
+ ShieldCheck,
75
+ Smile,
76
+ Sparkles,
77
+ Square,
78
+ Star,
79
+ Theater,
80
+ Tv,
81
+ Users,
82
+ X,
83
+ Zap,
84
+ };
85
+
86
+ export function getRegisteredIconNames(): ReadonlyArray<string> {
87
+ return Object.keys(ICON_REGISTRY);
88
+ }
89
+
90
+ export function doesIconNameResolve(name: string | undefined): boolean {
91
+ return typeof name === 'string' && name in ICON_REGISTRY;
92
+ }
93
+
94
+ export function resolveIconByName(name: string | undefined): LucideIcon | null {
95
+ if (!doesIconNameResolve(name)) return null;
96
+ return ICON_REGISTRY[name as string];
97
+ }
@@ -0,0 +1,26 @@
1
+ export {
2
+ isPermissionGranted,
3
+ isPermissionDenied,
4
+ isPermissionPrompt,
5
+ isPermissionUnsupported,
6
+ isPermissionDecided,
7
+ } from './permission-granted';
8
+ export {
9
+ doesBrowserSupportNotifications,
10
+ doesBrowserSupportMediaCapture,
11
+ doesBrowserSupportGeolocation,
12
+ isRuntimeSupportMissing,
13
+ } from './permission-supports';
14
+ export { isLocalStorageAvailable } from './storage-available';
15
+ export {
16
+ doesIconNameResolve,
17
+ resolveIconByName,
18
+ getRegisteredIconNames,
19
+ } from './icon-name-resolves';
20
+ export {
21
+ hasOnRequest,
22
+ hasOnAccept,
23
+ hasOnSignIn,
24
+ accountGateAllowsSkip,
25
+ } from './has-handler';
26
+ export { screenEmitsData } from './screen-emits-data';
@@ -0,0 +1,21 @@
1
+ import type { PermissionStatus } from '../types/screens';
2
+
3
+ export function isPermissionGranted(status: PermissionStatus): boolean {
4
+ return status === 'granted';
5
+ }
6
+
7
+ export function isPermissionDenied(status: PermissionStatus): boolean {
8
+ return status === 'denied';
9
+ }
10
+
11
+ export function isPermissionPrompt(status: PermissionStatus): boolean {
12
+ return status === 'prompt';
13
+ }
14
+
15
+ export function isPermissionUnsupported(status: PermissionStatus): boolean {
16
+ return status === 'unsupported';
17
+ }
18
+
19
+ export function isPermissionDecided(status: PermissionStatus): boolean {
20
+ return status === 'granted' || status === 'denied';
21
+ }
@@ -0,0 +1,56 @@
1
+ import type { PermissionType } from '../types/screens';
2
+
3
+ declare const navigator: undefined | {
4
+ mediaDevices?: { getUserMedia?: (constraints: MediaStreamConstraints) => Promise<MediaStream> };
5
+ geolocation?: {
6
+ getCurrentPosition: (
7
+ success: (pos: GeolocationPosition) => void,
8
+ error?: (err: GeolocationPositionError) => void,
9
+ options?: PositionOptions,
10
+ ) => void;
11
+ };
12
+ permissions?: { query: (descriptor: { name: string }) => Promise<{ state: string }> };
13
+ };
14
+
15
+ function safeWindow(): (Window & typeof globalThis) | null {
16
+ return typeof window === 'undefined' ? null : (window as Window & typeof globalThis);
17
+ }
18
+
19
+ function safeNavigator(): typeof navigator | null {
20
+ if (typeof navigator === 'undefined') return null;
21
+ return navigator;
22
+ }
23
+
24
+ export function doesBrowserSupportNotifications(): boolean {
25
+ const win = safeWindow();
26
+ if (!win) return false;
27
+ return typeof win.Notification !== 'undefined';
28
+ }
29
+
30
+ export function doesBrowserSupportMediaCapture(): boolean {
31
+ const nav = safeNavigator();
32
+ if (!nav) return false;
33
+ return Boolean(nav.mediaDevices?.getUserMedia);
34
+ }
35
+
36
+ export function doesBrowserSupportGeolocation(): boolean {
37
+ const nav = safeNavigator();
38
+ if (!nav) return false;
39
+ return Boolean(nav.geolocation);
40
+ }
41
+
42
+ export function isRuntimeSupportMissing(permission: PermissionType): boolean {
43
+ switch (permission) {
44
+ case 'notifications':
45
+ return !doesBrowserSupportNotifications();
46
+ case 'camera':
47
+ case 'microphone':
48
+ return !doesBrowserSupportMediaCapture();
49
+ case 'location':
50
+ return !doesBrowserSupportGeolocation();
51
+ case 'photos':
52
+ case 'contacts':
53
+ return true; default:
54
+ return true;
55
+ }
56
+ }
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ doesIconNameResolve,
4
+ getRegisteredIconNames,
5
+ resolveIconByName,
6
+ } from './icon-name-resolves';
7
+ import {
8
+ isPermissionDecided,
9
+ isPermissionDenied,
10
+ isPermissionGranted,
11
+ isPermissionPrompt,
12
+ isPermissionUnsupported,
13
+ } from './permission-granted';
14
+ import {
15
+ doesBrowserSupportGeolocation,
16
+ doesBrowserSupportMediaCapture,
17
+ doesBrowserSupportNotifications,
18
+ isRuntimeSupportMissing,
19
+ } from './permission-supports';
20
+ import { isLocalStorageAvailable } from './storage-available';
21
+ import {
22
+ accountGateAllowsSkip,
23
+ hasOnAccept,
24
+ hasOnRequest,
25
+ hasOnSignIn,
26
+ } from './has-handler';
27
+ import { screenEmitsData } from './screen-emits-data';
28
+ import type { OnboardingScreen } from '../types/screens';
29
+
30
+ describe('icon-name-resolves (§24.4)', () => {
31
+ it('resolves registered names', () => {
32
+ expect(doesIconNameResolve('HeartPulse')).toBe(true);
33
+ expect(doesIconNameResolve('Sparkles')).toBe(true);
34
+ expect(resolveIconByName('Sparkles')).toBeTruthy();
35
+ });
36
+
37
+ it('does not resolve unknown, missing, or case-mismatched names', () => {
38
+ expect(doesIconNameResolve('NonExistentIcon')).toBe(false);
39
+ expect(doesIconNameResolve(undefined)).toBe(false);
40
+ expect(doesIconNameResolve('sparkles')).toBe(false); expect(resolveIconByName('NonExistentIcon')).toBeNull();
41
+ });
42
+
43
+ it('resolves the reserved-word alias "Infinity"', () => {
44
+ expect(doesIconNameResolve('Infinity')).toBe(true);
45
+ expect(resolveIconByName('Infinity')).toBeTruthy();
46
+ });
47
+
48
+ it('resolves every icon used by the bundled templates — including the video set', () => {
49
+ const templateIcons = [
50
+ 'Activity', 'Apple', 'Award', 'Ban', 'Bell', 'BookOpen', 'Camera', 'Check',
51
+ 'CheckCircle2', 'Download', 'Dumbbell', 'GraduationCap', 'Headphones',
52
+ 'HeartPulse', 'Image', 'Infinity', 'Lock', 'MapPin', 'MessageCircle',
53
+ 'Mic', 'Moon', 'Palette', 'PenTool', 'Play', 'Rocket', 'Rewind', 'Share2',
54
+ 'Shield', 'ShieldCheck', 'Smile', 'Sparkles', 'Square', 'Star', 'Theater',
55
+ 'Tv', 'Users', 'X', 'Zap',
56
+ ];
57
+ for (const name of templateIcons) {
58
+ expect(doesIconNameResolve(name), `expected "${name}" to resolve`).toBe(true);
59
+ }
60
+ });
61
+
62
+ it('keeps the registry curated (each name maps to a component)', () => {
63
+ const names = getRegisteredIconNames();
64
+ expect(names.length).toBeGreaterThan(30);
65
+ expect(names.length).toBeLessThan(60); for (const name of names) {
66
+ expect(resolveIconByName(name), `icon "${name}"`).toBeTruthy();
67
+ }
68
+ });
69
+ });
70
+
71
+ describe('permission-granted (§24.1)', () => {
72
+ it('classifies statuses', () => {
73
+ expect(isPermissionGranted('granted')).toBe(true);
74
+ expect(isPermissionGranted('prompt')).toBe(false);
75
+ expect(isPermissionDenied('denied')).toBe(true);
76
+ expect(isPermissionPrompt('prompt')).toBe(true);
77
+ expect(isPermissionDecided('granted')).toBe(true);
78
+ expect(isPermissionDecided('denied')).toBe(true);
79
+ expect(isPermissionDecided('prompt')).toBe(false);
80
+ expect(isPermissionUnsupported('unsupported')).toBe(true);
81
+ expect(isPermissionUnsupported('granted')).toBe(false);
82
+ });
83
+ });
84
+
85
+ describe('permission-supports (§24.2)', () => {
86
+ it('reports support from the live navigator/window', () => {
87
+ expect(doesBrowserSupportMediaCapture()).toBe(
88
+ Boolean((globalThis as { navigator?: { mediaDevices?: { getUserMedia?: unknown } } })
89
+ .navigator?.mediaDevices?.getUserMedia),
90
+ );
91
+ expect(doesBrowserSupportGeolocation()).toBe(
92
+ Boolean(
93
+ (globalThis as { navigator?: { geolocation?: unknown } }).navigator?.geolocation,
94
+ ),
95
+ );
96
+ });
97
+
98
+ it('reports notification support from the window object', () => {
99
+ expect(doesBrowserSupportNotifications()).toBe(
100
+ typeof (globalThis as { Notification?: unknown }).Notification !== 'undefined',
101
+ );
102
+ });
103
+
104
+ it('treats photos/contacts as unsupported (no web-standard API)', () => {
105
+ expect(isRuntimeSupportMissing('photos')).toBe(true);
106
+ expect(isRuntimeSupportMissing('contacts')).toBe(true);
107
+ });
108
+ });
109
+
110
+ describe('storage-available (§24.3)', () => {
111
+ it('works in the jsdom browser environment', () => {
112
+ expect(isLocalStorageAvailable()).toBe(true);
113
+ });
114
+
115
+ it('returns false when storage access throws (e.g. blocked cookies)', () => {
116
+ const original = window.localStorage;
117
+ Object.defineProperty(window, 'localStorage', {
118
+ configurable: true,
119
+ get() {
120
+ throw new DOMException('Access is denied for this document', 'SecurityError');
121
+ },
122
+ });
123
+ try {
124
+ expect(isLocalStorageAvailable()).toBe(false);
125
+ } finally {
126
+ Object.defineProperty(window, 'localStorage', {
127
+ configurable: true,
128
+ value: original,
129
+ writable: true,
130
+ });
131
+ }
132
+ });
133
+ });
134
+
135
+ describe('has-handler (§24.5)', () => {
136
+ const paywall = {
137
+ id: 'w',
138
+ type: 'paywall',
139
+ price: '$1',
140
+ trialDays: 7,
141
+ } as Parameters<typeof hasOnAccept>[0];
142
+
143
+ it('detects usable callbacks', () => {
144
+ expect(hasOnAccept(paywall)).toBe(false);
145
+ expect(hasOnAccept({ ...paywall, onAccept: async () => ({ accepted: true, trialStarted: true }) })).toBe(true);
146
+
147
+ expect(
148
+ hasOnRequest({
149
+ permission: 'camera',
150
+ description: 'd',
151
+ benefits: [],
152
+ } as unknown as Parameters<typeof hasOnRequest>[0]),
153
+ ).toBe(false);
154
+
155
+ expect(
156
+ hasOnSignIn({
157
+ optional: false,
158
+ onSignIn: () => Promise.resolve({ signedIn: true }),
159
+ } as unknown as Parameters<typeof hasOnSignIn>[0]),
160
+ ).toBe(true);
161
+ });
162
+
163
+ it('allows skip only on optional account gates', () => {
164
+ expect(
165
+ accountGateAllowsSkip({ optional: true } as unknown as Parameters<
166
+ typeof accountGateAllowsSkip
167
+ >[0]),
168
+ ).toBe(true);
169
+ expect(
170
+ accountGateAllowsSkip({ optional: false } as unknown as Parameters<
171
+ typeof accountGateAllowsSkip
172
+ >[0]),
173
+ ).toBe(false);
174
+ });
175
+ });
176
+
177
+ describe('screen-emits-data (§24.6)', () => {
178
+ it('marks view-only screens as non-emitting', () => {
179
+ expect(screenEmitsData({ id: 'a', type: 'welcome' } as OnboardingScreen)).toBe(false);
180
+ expect(screenEmitsData({ id: 'v', type: 'value-delivery' } as OnboardingScreen)).toBe(false);
181
+ expect(screenEmitsData({ id: 's', type: 'social-proof' } as OnboardingScreen)).toBe(false);
182
+ });
183
+
184
+ it('marks data-collecting screens as emitting', () => {
185
+ expect(screenEmitsData({ id: 'g', type: 'goal-question' } as OnboardingScreen)).toBe(true);
186
+ expect(screenEmitsData({ id: 'p', type: 'preferences' } as OnboardingScreen)).toBe(true);
187
+ });
188
+ });
@@ -0,0 +1,13 @@
1
+ import type { OnboardingScreen } from '../types/screens';
2
+
3
+ const VIEW_ONLY_SCREEN_TYPES: ReadonlySet<OnboardingScreen['type']> = new Set([
4
+ 'welcome',
5
+ 'social-proof',
6
+ 'comparison',
7
+ 'value-delivery',
8
+ 'personalized-solution',
9
+ ]);
10
+
11
+ export function screenEmitsData(screen: OnboardingScreen): boolean {
12
+ return !VIEW_ONLY_SCREEN_TYPES.has(screen.type);
13
+ }
@@ -0,0 +1,14 @@
1
+ const PROBE_KEY = '@umituz/web-onboarding:probe';
2
+ const PROBE_VALUE = '1';
3
+
4
+ export function isLocalStorageAvailable(): boolean {
5
+ if (typeof window === 'undefined') return false;
6
+ try {
7
+ const probe = window.localStorage;
8
+ probe.setItem(PROBE_KEY, PROBE_VALUE);
9
+ probe.removeItem(PROBE_KEY);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
@@ -0,0 +1,92 @@
1
+ import { useState } from 'react';
2
+ import { Button } from '@umituz/web-design-system/atoms';
3
+ import { Sparkles } from 'lucide-react';
4
+ import { cn } from '@umituz/web-design-system/utils/cn';
5
+ import type { DemoConfig, OnboardingScreen } from '../types/screens';
6
+
7
+ export interface TemplateDemoResult {
8
+ variant: string;
9
+ palette: string[];
10
+ generatedAt: number;
11
+ }
12
+
13
+ function TemplateDemoFallback({ onSave }: { onSave: (result: TemplateDemoResult) => void }) {
14
+ const palettes: ReadonlyArray<{ variant: string; palette: string[] }> = [
15
+ { variant: 'Minimal', palette: ['#0F172A', '#475569', '#94A3B8', '#F1F5F9', '#FFFFFF'] },
16
+ { variant: 'Bold', palette: ['#7C3AED', '#EC4899', '#F59E0B', '#10B981', '#0EA5E9'] },
17
+ { variant: 'Retro', palette: ['#92400E', '#B45309', '#D97706', '#FCD34D', '#FEF3C7'] },
18
+ { variant: 'Playful', palette: ['#F472B6', '#A78BFA', '#60A9FA', '#34D399', '#FBBF24'] },
19
+ ];
20
+
21
+ const [activeIdx, setActiveIdx] = useState(0);
22
+ const active = palettes[activeIdx];
23
+
24
+ return (
25
+ <div className="udb-onb-question__body">
26
+ <div className="udb-onb-palettes">
27
+ {palettes.map((p, i) => (
28
+ <button
29
+ key={p.variant}
30
+ type="button"
31
+ onClick={() => setActiveIdx(i)}
32
+ className={cn('udb-onb-palette', i === activeIdx && 'udb-onb-palette--active')}
33
+ >
34
+ <div className="udb-onb-palette__strip">
35
+ {p.palette.map((c) => (
36
+ <span key={c} className="udb-onb-palette__chip" style={{ background: c }} />
37
+ ))}
38
+ </div>
39
+ <span className="udb-onb-palette__name">{p.variant}</span>
40
+ </button>
41
+ ))}
42
+ </div>
43
+ <div className="udb-onb-palette-preview">
44
+ <div className="udb-onb-palette__strip udb-onb-palette__strip--lg">
45
+ {active.palette.map((c) => (
46
+ <span key={c} className="udb-onb-palette__chip" style={{ background: c }} />
47
+ ))}
48
+ </div>
49
+ <p className="udb-onb-lead">
50
+ Preview: <strong>{active.variant}</strong> palette
51
+ </p>
52
+ </div>
53
+ <Button
54
+ variant="primary"
55
+ size="sm"
56
+ icon={<Sparkles className="udb-onb-icon--sm" />}
57
+ onClick={() =>
58
+ onSave({
59
+ variant: active.variant,
60
+ palette: [...active.palette],
61
+ generatedAt: Date.now(),
62
+ })
63
+ }
64
+ >
65
+ Save this style
66
+ </Button>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ const fallbackDemo: DemoConfig = {
72
+ component: TemplateDemoFallback as React.ComponentType<Record<string, unknown>>,
73
+ onSave: () => undefined,
74
+ };
75
+
76
+ const fallbackResult: TemplateDemoResult = {
77
+ variant: 'Minimal',
78
+ palette: ['#0F172A', '#475569', '#94A3B8', '#F1F5F9', '#FFFFFF'],
79
+ generatedAt: 0,
80
+ };
81
+
82
+ export function withDefaultDemo(screen: OnboardingScreen, fallback: DemoConfig = fallbackDemo): OnboardingScreen {
83
+ if (screen.type !== 'app-demo') return screen;
84
+ return { ...screen, demo: { ...fallback, ...screen.demo } };
85
+ }
86
+
87
+ export function defaultDemoResult(): TemplateDemoResult {
88
+ return { ...fallbackResult };
89
+ }
90
+
91
+ export { TemplateDemoFallback };
92
+ export type { DemoConfig };
@@ -0,0 +1,71 @@
1
+ import type { OnboardingConfig } from '../types/config';
2
+ import type { OnboardingScreen } from '../types/screens';
3
+ import { withDefaultDemo, defaultDemoResult } from './TemplateDemoFallback';
4
+
5
+ const baseScreens: OnboardingScreen[] = [
6
+ {
7
+ id: 'welcome',
8
+ type: 'welcome',
9
+ title: 'Make something beautiful in 60 seconds',
10
+ subtitle: 'No design skills. No learning curve. Just create.',
11
+ cta: 'Try the demo',
12
+ heroIconName: 'Sparkles',
13
+ highlight: 'Live demo',
14
+ },
15
+ {
16
+ id: 'style',
17
+ type: 'goal-question',
18
+ question: 'Pick a style that feels like you',
19
+ multiSelect: true,
20
+ options: [
21
+ { id: 'minimal', label: 'Minimal', iconName: 'Square' },
22
+ { id: 'bold', label: 'Bold colors', iconName: 'Zap' },
23
+ { id: 'retro', label: 'Retro', iconName: 'Rewind' },
24
+ { id: 'playful', label: 'Playful', iconName: 'Smile' },
25
+ ],
26
+ onChange: () => undefined,
27
+ cta: 'Continue',
28
+ },
29
+ {
30
+ id: 'demo',
31
+ type: 'app-demo',
32
+ title: 'Your turn — pick a palette',
33
+ demo: {
34
+ component: () => null,
35
+ onSave: () => undefined,
36
+ },
37
+ },
38
+ {
39
+ id: 'value',
40
+ type: 'value-delivery',
41
+ title: 'You made this!',
42
+ result: defaultDemoResult(),
43
+ },
44
+ {
45
+ id: 'paywall',
46
+ type: 'paywall',
47
+ title: 'Keep creating',
48
+ subtitle: 'Unlimited generations, all styles, no watermarks.',
49
+ price: '$4.99 / week',
50
+ trialDays: 3,
51
+ cta: 'Start free trial',
52
+ secondaryCta: 'Not now',
53
+ features: [
54
+ { iconName: 'Infinity', text: 'Unlimited creations', highlighted: true },
55
+ { iconName: 'Palette', text: 'All styles unlocked' },
56
+ { iconName: 'Download', text: 'High-res export' },
57
+ { iconName: 'Share2', text: 'Share anywhere' },
58
+ ],
59
+ onSecondaryCta: () => undefined,
60
+ onAccept: async () => ({ accepted: true, trialStarted: true }),
61
+ },
62
+ ];
63
+
64
+ const screens = baseScreens.map((s) => withDefaultDemo(s));
65
+
66
+ export const artOnboardingTemplate: OnboardingConfig = {
67
+ category: 'art',
68
+ screens,
69
+ onSkip: () => undefined,
70
+ onComplete: () => undefined,
71
+ };
@@ -0,0 +1,92 @@
1
+ import type { OnboardingConfig } from '../types/config';
2
+ import type { OnboardingScreen } from '../types/screens';
3
+
4
+ const screens: OnboardingScreen[] = [
5
+ {
6
+ id: 'welcome',
7
+ type: 'welcome',
8
+ title: 'Your health, your data, your call',
9
+ subtitle: 'Private by design. Always encrypted on-device.',
10
+ cta: 'Get started',
11
+ heroIconName: 'ShieldCheck',
12
+ highlight: 'HIPAA-ready',
13
+ },
14
+ {
15
+ id: 'goal',
16
+ type: 'goal-question',
17
+ question: 'What brings you here today?',
18
+ multiSelect: false,
19
+ options: [
20
+ { id: 'sleep', label: 'Better sleep', iconName: 'Moon' },
21
+ { id: 'fitness', label: 'Get fitter', iconName: 'Dumbbell' },
22
+ { id: 'nutrition', label: 'Eat healthier', iconName: 'Apple' },
23
+ { id: 'stress', label: 'Manage stress', iconName: 'HeartPulse' },
24
+ ],
25
+ onChange: () => undefined,
26
+ cta: 'Continue',
27
+ },
28
+ {
29
+ id: 'privacy',
30
+ type: 'pain-points',
31
+ title: 'What worries you most about health apps?',
32
+ painPoints: [
33
+ { id: 'data-shared', label: 'Data shared with insurers' },
34
+ { id: 'tracking', label: 'Being tracked across apps' },
35
+ { id: 'ads', label: 'Seeing health ads' },
36
+ { id: 'leaks', label: 'Data breaches' },
37
+ ],
38
+ multiSelect: true,
39
+ onChange: () => undefined,
40
+ cta: 'Continue',
41
+ },
42
+ {
43
+ id: 'permission-priming-notifications',
44
+ type: 'permission-priming',
45
+ permission: 'notifications',
46
+ title: 'Gentle daily reminders',
47
+ description: 'We only ping you when it matters — never spammy.',
48
+ benefits: [
49
+ 'Reminders you can mute any time',
50
+ 'On-device scheduling — nothing leaves your phone',
51
+ 'One-tap unsubscribe from any category',
52
+ ],
53
+ iconName: 'Bell',
54
+ onRequest: async () => 'prompt',
55
+ cta: 'Allow reminders',
56
+ },
57
+ {
58
+ id: 'processing',
59
+ type: 'processing',
60
+ title: 'Building your wellness plan…',
61
+ steps: ['Reading your goals', 'Filtering privacy settings', 'Personalizing the plan'],
62
+ durationMs: 3000,
63
+ onComplete: () => undefined,
64
+ },
65
+ {
66
+ id: 'paywall',
67
+ type: 'paywall',
68
+ title: 'Unlock your full plan',
69
+ subtitle: 'Most users see results in their first week.',
70
+ price: '$6.99 / month',
71
+ trialDays: 7,
72
+ trialPrice: 'Free',
73
+ cta: 'Start 7-day trial',
74
+ secondaryCta: 'Maybe later',
75
+ features: [
76
+ { iconName: 'Lock', text: 'End-to-end encrypted data' },
77
+ { iconName: 'Activity', text: 'Personalized daily plan' },
78
+ { iconName: 'MessageCircle', text: '24/7 human coaching' },
79
+ { iconName: 'Award', text: 'Progress you can share (or not)' },
80
+ ],
81
+ socialProof: { text: 'Trusted by', count: 250000 },
82
+ onSecondaryCta: () => undefined,
83
+ onAccept: async () => ({ accepted: true, trialStarted: true }),
84
+ },
85
+ ];
86
+
87
+ export const healthOnboardingTemplate: OnboardingConfig = {
88
+ category: 'health',
89
+ screens,
90
+ onSkip: () => undefined,
91
+ onComplete: () => undefined,
92
+ };
@@ -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';