@umituz/web-dashboard 6.1.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.
- package/README.md +47 -73
- package/package.json +5 -9
- package/src/config/DashboardConfig.ts +19 -68
- package/src/domains/analytics/components/index.ts +0 -6
- package/src/domains/analytics/hooks/index.ts +0 -6
- package/src/domains/analytics/hooks/useAnalytics.ts +2 -20
- package/src/domains/analytics/index.ts +0 -10
- package/src/domains/analytics/services/AnalyticsEngineService.ts +7 -74
- package/src/domains/analytics/services/PerformanceService.ts +27 -138
- package/src/domains/analytics/services/index.ts +0 -4
- package/src/domains/analytics/types/analytics.ts +100 -270
- package/src/domains/analytics/types/index.ts +0 -6
- package/src/domains/analytics/utils/analytics.test.ts +2 -9
- package/src/domains/analytics/utils/analytics.ts +1 -22
- package/src/domains/analytics/utils/index.ts +0 -6
- package/src/domains/calendar/config.ts +0 -7
- package/src/domains/calendar/hooks/index.ts +0 -4
- package/src/domains/calendar/hooks/useCalendar.ts +7 -27
- package/src/domains/calendar/index.ts +0 -4
- package/src/domains/calendar/services/CalendarService.ts +0 -21
- package/src/domains/calendar/services/index.ts +0 -4
- package/src/domains/calendar/types/calendar.types.ts +9 -53
- package/src/domains/calendar/utils/contentItemMapper.test.ts +0 -4
- package/src/domains/calendar/utils/contentItemMapper.ts +0 -31
- package/src/domains/calendar/utils/index.ts +0 -26
- package/src/domains/layouts/components/index.ts +0 -4
- package/src/domains/layouts/hooks/dashboard.ts +0 -21
- package/src/domains/layouts/hooks/index.ts +0 -4
- package/src/domains/layouts/index.ts +0 -6
- package/src/domains/layouts/theme/default.ts +0 -12
- package/src/domains/layouts/theme/index.ts +0 -4
- package/src/domains/layouts/theme/presets.ts +0 -9
- package/src/domains/layouts/theme/utils.ts +0 -15
- package/src/domains/layouts/types/index.ts +0 -4
- package/src/domains/layouts/types/layout.ts +18 -56
- package/src/domains/layouts/types/notification.ts +4 -17
- package/src/domains/layouts/types/sidebar.ts +8 -28
- package/src/domains/layouts/types/theme.ts +23 -59
- package/src/domains/layouts/types/user.ts +9 -30
- package/src/domains/layouts/utils/dashboard.ts +4 -48
- package/src/domains/layouts/utils/index.ts +0 -4
- package/src/domains/modules/build.tsx +2 -16
- package/src/domains/modules/types.ts +8 -23
- package/src/domains/onboarding/calc/calc.test.ts +1 -9
- package/src/domains/onboarding/calc/card-stack-motion-curve.ts +0 -6
- package/src/domains/onboarding/calc/count-with-locale.ts +0 -8
- package/src/domains/onboarding/calc/duration-equal-split.ts +0 -12
- package/src/domains/onboarding/calc/elapsed-millis.ts +0 -6
- package/src/domains/onboarding/calc/progress-percentage.ts +1 -11
- package/src/domains/onboarding/calc/slider-value-bounds.ts +1 -11
- package/src/domains/onboarding/calc/step-index-clamp.ts +0 -9
- package/src/domains/onboarding/calc/swipe-gesture-curve.ts +0 -13
- package/src/domains/onboarding/components/{shared/AnimationWrapper.tsx → AnimationWrapper.tsx} +0 -4
- package/src/domains/onboarding/components/{shared/OnboardingErrorBoundary.tsx → OnboardingErrorBoundary.tsx} +7 -27
- package/src/domains/onboarding/components/OnboardingFlow.test.tsx +8 -30
- package/src/domains/onboarding/components/OnboardingFlow.tsx +13 -61
- package/src/domains/onboarding/components/OnboardingIntl.tsx +1 -11
- package/src/domains/onboarding/components/PrimaryButton.tsx +40 -0
- package/src/domains/onboarding/components/ProgressBar.tsx +32 -0
- package/src/domains/onboarding/components/QuestionWrapper.tsx +28 -0
- package/src/domains/onboarding/components/ScreenContainer.tsx +19 -0
- package/src/domains/onboarding/components/{shared/SwipeCard.test.tsx → SwipeCard.test.tsx} +1 -3
- package/src/domains/onboarding/components/{shared/SwipeCard.tsx → SwipeCard.tsx} +10 -28
- package/src/domains/onboarding/components/renderBundledIcon.tsx +7 -0
- package/src/domains/onboarding/components/screens/AccountGateScreen.tsx +7 -17
- package/src/domains/onboarding/components/screens/AppDemoScreen.tsx +4 -10
- package/src/domains/onboarding/components/screens/ComparisonScreen.tsx +12 -22
- package/src/domains/onboarding/components/screens/GoalQuestionScreen.tsx +9 -6
- package/src/domains/onboarding/components/screens/PainPointsScreen.tsx +10 -10
- package/src/domains/onboarding/components/screens/PaywallScreen.tsx +22 -38
- package/src/domains/onboarding/components/screens/PermissionPrimingScreen.tsx +14 -29
- package/src/domains/onboarding/components/screens/PersonalizedSolutionScreen.tsx +10 -23
- package/src/domains/onboarding/components/screens/PreferencesScreen.tsx +13 -27
- package/src/domains/onboarding/components/screens/ProcessingScreen.tsx +11 -27
- package/src/domains/onboarding/components/screens/SocialProofScreen.tsx +22 -51
- package/src/domains/onboarding/components/screens/TinderCardsScreen.tsx +13 -30
- package/src/domains/onboarding/components/screens/ValueDeliveryScreen.tsx +13 -29
- package/src/domains/onboarding/components/screens/WelcomeScreen.tsx +12 -32
- package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingAnalytics.test.tsx +1 -1
- package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingAnalytics.ts +3 -16
- package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingProgress.test.tsx +2 -2
- package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingProgress.ts +10 -21
- package/src/domains/onboarding/{components/hooks → hooks}/usePermissionPriming.ts +3 -10
- package/src/domains/onboarding/{components/hooks → hooks}/useReducedMotion.ts +0 -5
- package/src/domains/onboarding/index.ts +16 -45
- package/src/domains/onboarding/predicates/has-handler.ts +0 -4
- package/src/domains/onboarding/predicates/icon-name-resolves.ts +1 -17
- package/src/domains/onboarding/predicates/permission-granted.ts +0 -5
- package/src/domains/onboarding/predicates/permission-supports.ts +1 -11
- package/src/domains/onboarding/predicates/predicates.test.ts +6 -20
- package/src/domains/onboarding/predicates/screen-emits-data.ts +0 -4
- package/src/domains/onboarding/predicates/storage-available.ts +0 -8
- package/src/domains/onboarding/templates/TemplateDemoFallback.tsx +19 -39
- package/src/domains/onboarding/templates/art-onboarding.ts +1 -11
- package/src/domains/onboarding/templates/health-onboarding.ts +0 -5
- package/src/domains/onboarding/templates/platform-onboarding.ts +0 -9
- package/src/domains/onboarding/templates/video-onboarding.ts +0 -5
- package/src/domains/onboarding/test/setup.ts +1 -8
- package/src/domains/onboarding/types/config.ts +7 -18
- package/src/domains/onboarding/types/labels.ts +25 -59
- package/src/domains/onboarding/types/screens.ts +3 -11
- package/src/domains/onboarding/utils/analytics-dispatch.ts +2 -21
- package/src/domains/onboarding/utils/default-storage-key.ts +0 -1
- package/src/domains/onboarding/utils/index.ts +0 -1
- package/src/domains/onboarding/utils/local-storage-bridge.ts +1 -21
- package/src/domains/onboarding/utils/permission-bridge.ts +0 -8
- package/src/domains/onboarding/validation/flow-structure-check.ts +0 -6
- package/src/domains/onboarding/validation/permission-status-check.ts +0 -5
- package/src/domains/onboarding/validation/screen-config-check.ts +0 -5
- package/src/domains/onboarding/validation/stored-progress-check.ts +1 -10
- package/src/domains/onboarding/validation/validation.test.ts +1 -6
- package/src/domains/permissions/hasPermission.ts +0 -8
- package/src/domains/permissions/types.ts +0 -20
- package/src/domains/settings/components/index.ts +0 -4
- package/src/domains/settings/hooks/index.ts +0 -4
- package/src/domains/settings/hooks/useSettings.ts +1 -26
- package/src/domains/settings/index.ts +0 -6
- package/src/domains/settings/types/index.ts +0 -4
- package/src/domains/settings/types/settings.ts +23 -67
- package/src/domains/settings/utils/index.ts +0 -4
- package/src/domains/settings/utils/settings.ts +0 -45
- package/src/styles/dashboard.css +818 -1
- package/src/domains/onboarding/components/shared/OptionCard.tsx +0 -73
- package/src/domains/onboarding/components/shared/PrimaryButton.tsx +0 -58
- package/src/domains/onboarding/components/shared/ProgressBar.tsx +0 -51
- package/src/domains/onboarding/components/shared/QuestionWrapper.tsx +0 -36
- package/src/domains/onboarding/components/shared/ScreenContainer.tsx +0 -29
- package/src/domains/onboarding/components/shared/index.ts +0 -16
- package/src/domains/onboarding/components/shared/renderBundledIcon.tsx +0 -20
- package/src/domains/onboarding/onboarding.css +0 -1
- package/src/domains/onboarding/theme/cn.ts +0 -10
- package/src/domains/onboarding/theme/index.css +0 -131
- package/src/domains/onboarding/theme/index.ts +0 -12
- package/src/domains/onboarding/theme/tokens.ts +0 -122
- /package/src/domains/onboarding/{components/hooks → hooks}/index.ts +0 -0
|
@@ -4,22 +4,18 @@ import type {
|
|
|
4
4
|
PermissionPrimingScreenConfig,
|
|
5
5
|
} from '../types/screens';
|
|
6
6
|
|
|
7
|
-
/** Does the permission-priming config expose a usable `onRequest` callback? */
|
|
8
7
|
export function hasOnRequest(config: PermissionPrimingScreenConfig): boolean {
|
|
9
8
|
return typeof config.onRequest === 'function';
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
/** Does the paywall config expose a usable `onAccept` callback? */
|
|
13
11
|
export function hasOnAccept(config: PaywallScreenConfig): boolean {
|
|
14
12
|
return typeof config.onAccept === 'function';
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
/** Does the account-gate config expose a usable `onSignIn` callback? */
|
|
18
15
|
export function hasOnSignIn(config: AccountGateScreenConfig): boolean {
|
|
19
16
|
return typeof config.onSignIn === 'function';
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
/** Is a skip affordance allowed by the account-gate config? */
|
|
23
19
|
export function accountGateAllowsSkip(config: AccountGateScreenConfig): boolean {
|
|
24
20
|
return config.optional === true;
|
|
25
21
|
}
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Icon name registry.
|
|
3
|
-
*
|
|
4
|
-
* Consumers configure icons by string name (`iconName`, `heroIconName`).
|
|
5
|
-
* The library resolves those names against a CURATED, STATICALLY-IMPORTED set
|
|
6
|
-
* of lucide-react icons. A static registry is what keeps the published bundle
|
|
7
|
-
* small: a namespace import (`import * as Icons`) with dynamic lookup would
|
|
8
|
-
* force every one of lucide's ~1,500 icons into the consumer's bundle.
|
|
9
|
-
*
|
|
10
|
-
* Icons not in the registry simply do not render (callers fall back to a
|
|
11
|
-
* default or hide the icon slot). See README "Icons" for the full list.
|
|
12
|
-
*/
|
|
13
1
|
import type { LucideIcon } from 'lucide-react';
|
|
14
2
|
import {
|
|
15
3
|
Activity,
|
|
@@ -70,8 +58,7 @@ const ICON_REGISTRY: Readonly<Record<string, LucideIcon>> = {
|
|
|
70
58
|
Headphones,
|
|
71
59
|
HeartPulse,
|
|
72
60
|
Image,
|
|
73
|
-
|
|
74
|
-
Infinity: InfinityIcon,
|
|
61
|
+
Infinity: InfinityIcon,
|
|
75
62
|
Lock,
|
|
76
63
|
MapPin,
|
|
77
64
|
MessageCircle,
|
|
@@ -96,17 +83,14 @@ const ICON_REGISTRY: Readonly<Record<string, LucideIcon>> = {
|
|
|
96
83
|
Zap,
|
|
97
84
|
};
|
|
98
85
|
|
|
99
|
-
/** The icon names this package can resolve without application help. */
|
|
100
86
|
export function getRegisteredIconNames(): ReadonlyArray<string> {
|
|
101
87
|
return Object.keys(ICON_REGISTRY);
|
|
102
88
|
}
|
|
103
89
|
|
|
104
|
-
/** Does the given name resolve to an icon bundled with this package? */
|
|
105
90
|
export function doesIconNameResolve(name: string | undefined): boolean {
|
|
106
91
|
return typeof name === 'string' && name in ICON_REGISTRY;
|
|
107
92
|
}
|
|
108
93
|
|
|
109
|
-
/** Resolve a bundled icon by name. Returns null when the name is unknown. */
|
|
110
94
|
export function resolveIconByName(name: string | undefined): LucideIcon | null {
|
|
111
95
|
if (!doesIconNameResolve(name)) return null;
|
|
112
96
|
return ICON_REGISTRY[name as string];
|
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import type { PermissionStatus } from '../types/screens';
|
|
2
2
|
|
|
3
|
-
/** Is the permission currently granted by the user? */
|
|
4
3
|
export function isPermissionGranted(status: PermissionStatus): boolean {
|
|
5
4
|
return status === 'granted';
|
|
6
5
|
}
|
|
7
6
|
|
|
8
|
-
/** Did the user explicitly deny this permission? */
|
|
9
7
|
export function isPermissionDenied(status: PermissionStatus): boolean {
|
|
10
8
|
return status === 'denied';
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
/** Is the permission in the initial "ask the user" state? */
|
|
14
11
|
export function isPermissionPrompt(status: PermissionStatus): boolean {
|
|
15
12
|
return status === 'prompt';
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
/** Does the current runtime lack support for this permission? */
|
|
19
15
|
export function isPermissionUnsupported(status: PermissionStatus): boolean {
|
|
20
16
|
return status === 'unsupported';
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
/** Has the permission been decided one way or the other? (granted OR denied) */
|
|
24
19
|
export function isPermissionDecided(status: PermissionStatus): boolean {
|
|
25
20
|
return status === 'granted' || status === 'denied';
|
|
26
21
|
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-feature predicates for the runtime.
|
|
3
|
-
* Each predicate answers "does the user's browser/environment support this API?".
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import type { PermissionType } from '../types/screens';
|
|
7
2
|
|
|
8
3
|
declare const navigator: undefined | {
|
|
@@ -26,28 +21,24 @@ function safeNavigator(): typeof navigator | null {
|
|
|
26
21
|
return navigator;
|
|
27
22
|
}
|
|
28
23
|
|
|
29
|
-
/** Is `Notification` available in the current runtime? */
|
|
30
24
|
export function doesBrowserSupportNotifications(): boolean {
|
|
31
25
|
const win = safeWindow();
|
|
32
26
|
if (!win) return false;
|
|
33
27
|
return typeof win.Notification !== 'undefined';
|
|
34
28
|
}
|
|
35
29
|
|
|
36
|
-
/** Is `getUserMedia` available (camera/microphone capture)? */
|
|
37
30
|
export function doesBrowserSupportMediaCapture(): boolean {
|
|
38
31
|
const nav = safeNavigator();
|
|
39
32
|
if (!nav) return false;
|
|
40
33
|
return Boolean(nav.mediaDevices?.getUserMedia);
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
/** Is geolocation available? */
|
|
44
36
|
export function doesBrowserSupportGeolocation(): boolean {
|
|
45
37
|
const nav = safeNavigator();
|
|
46
38
|
if (!nav) return false;
|
|
47
39
|
return Boolean(nav.geolocation);
|
|
48
40
|
}
|
|
49
41
|
|
|
50
|
-
/** Aggregate check used by `usePermissionPriming` to report "unsupported" cleanly. */
|
|
51
42
|
export function isRuntimeSupportMissing(permission: PermissionType): boolean {
|
|
52
43
|
switch (permission) {
|
|
53
44
|
case 'notifications':
|
|
@@ -59,8 +50,7 @@ export function isRuntimeSupportMissing(permission: PermissionType): boolean {
|
|
|
59
50
|
return !doesBrowserSupportGeolocation();
|
|
60
51
|
case 'photos':
|
|
61
52
|
case 'contacts':
|
|
62
|
-
return true;
|
|
63
|
-
default:
|
|
53
|
+
return true; default:
|
|
64
54
|
return true;
|
|
65
55
|
}
|
|
66
56
|
}
|
|
@@ -27,20 +27,17 @@ import {
|
|
|
27
27
|
import { screenEmitsData } from './screen-emits-data';
|
|
28
28
|
import type { OnboardingScreen } from '../types/screens';
|
|
29
29
|
|
|
30
|
-
// TEST_SCENARIOS.md §24
|
|
31
30
|
describe('icon-name-resolves (§24.4)', () => {
|
|
32
31
|
it('resolves registered names', () => {
|
|
33
32
|
expect(doesIconNameResolve('HeartPulse')).toBe(true);
|
|
34
33
|
expect(doesIconNameResolve('Sparkles')).toBe(true);
|
|
35
|
-
|
|
36
|
-
expect(resolveIconByName('Sparkles')).toBeTruthy();
|
|
34
|
+
expect(resolveIconByName('Sparkles')).toBeTruthy();
|
|
37
35
|
});
|
|
38
36
|
|
|
39
37
|
it('does not resolve unknown, missing, or case-mismatched names', () => {
|
|
40
38
|
expect(doesIconNameResolve('NonExistentIcon')).toBe(false);
|
|
41
39
|
expect(doesIconNameResolve(undefined)).toBe(false);
|
|
42
|
-
expect(doesIconNameResolve('sparkles')).toBe(false);
|
|
43
|
-
expect(resolveIconByName('NonExistentIcon')).toBeNull();
|
|
40
|
+
expect(doesIconNameResolve('sparkles')).toBe(false); expect(resolveIconByName('NonExistentIcon')).toBeNull();
|
|
44
41
|
});
|
|
45
42
|
|
|
46
43
|
it('resolves the reserved-word alias "Infinity"', () => {
|
|
@@ -49,8 +46,7 @@ describe('icon-name-resolves (§24.4)', () => {
|
|
|
49
46
|
});
|
|
50
47
|
|
|
51
48
|
it('resolves every icon used by the bundled templates — including the video set', () => {
|
|
52
|
-
|
|
53
|
-
const templateIcons = [
|
|
49
|
+
const templateIcons = [
|
|
54
50
|
'Activity', 'Apple', 'Award', 'Ban', 'Bell', 'BookOpen', 'Camera', 'Check',
|
|
55
51
|
'CheckCircle2', 'Download', 'Dumbbell', 'GraduationCap', 'Headphones',
|
|
56
52
|
'HeartPulse', 'Image', 'Infinity', 'Lock', 'MapPin', 'MessageCircle',
|
|
@@ -66,14 +62,12 @@ describe('icon-name-resolves (§24.4)', () => {
|
|
|
66
62
|
it('keeps the registry curated (each name maps to a component)', () => {
|
|
67
63
|
const names = getRegisteredIconNames();
|
|
68
64
|
expect(names.length).toBeGreaterThan(30);
|
|
69
|
-
expect(names.length).toBeLessThan(60);
|
|
70
|
-
for (const name of names) {
|
|
65
|
+
expect(names.length).toBeLessThan(60); for (const name of names) {
|
|
71
66
|
expect(resolveIconByName(name), `icon "${name}"`).toBeTruthy();
|
|
72
67
|
}
|
|
73
68
|
});
|
|
74
69
|
});
|
|
75
70
|
|
|
76
|
-
// TEST_SCENARIOS.md §24.1
|
|
77
71
|
describe('permission-granted (§24.1)', () => {
|
|
78
72
|
it('classifies statuses', () => {
|
|
79
73
|
expect(isPermissionGranted('granted')).toBe(true);
|
|
@@ -88,13 +82,9 @@ describe('permission-granted (§24.1)', () => {
|
|
|
88
82
|
});
|
|
89
83
|
});
|
|
90
84
|
|
|
91
|
-
// TEST_SCENARIOS.md §24.2 — jsdom stands in for the "modern browser" runtime.
|
|
92
85
|
describe('permission-supports (§24.2)', () => {
|
|
93
86
|
it('reports support from the live navigator/window', () => {
|
|
94
|
-
|
|
95
|
-
// answer varies by jsdom build, so assert predicate/API agreement instead
|
|
96
|
-
// of hardcoding booleans.
|
|
97
|
-
expect(doesBrowserSupportMediaCapture()).toBe(
|
|
87
|
+
expect(doesBrowserSupportMediaCapture()).toBe(
|
|
98
88
|
Boolean((globalThis as { navigator?: { mediaDevices?: { getUserMedia?: unknown } } })
|
|
99
89
|
.navigator?.mediaDevices?.getUserMedia),
|
|
100
90
|
);
|
|
@@ -117,7 +107,6 @@ describe('permission-supports (§24.2)', () => {
|
|
|
117
107
|
});
|
|
118
108
|
});
|
|
119
109
|
|
|
120
|
-
// TEST_SCENARIOS.md §24.3
|
|
121
110
|
describe('storage-available (§24.3)', () => {
|
|
122
111
|
it('works in the jsdom browser environment', () => {
|
|
123
112
|
expect(isLocalStorageAvailable()).toBe(true);
|
|
@@ -125,8 +114,7 @@ describe('storage-available (§24.3)', () => {
|
|
|
125
114
|
|
|
126
115
|
it('returns false when storage access throws (e.g. blocked cookies)', () => {
|
|
127
116
|
const original = window.localStorage;
|
|
128
|
-
|
|
129
|
-
Object.defineProperty(window, 'localStorage', {
|
|
117
|
+
Object.defineProperty(window, 'localStorage', {
|
|
130
118
|
configurable: true,
|
|
131
119
|
get() {
|
|
132
120
|
throw new DOMException('Access is denied for this document', 'SecurityError');
|
|
@@ -144,7 +132,6 @@ describe('storage-available (§24.3)', () => {
|
|
|
144
132
|
});
|
|
145
133
|
});
|
|
146
134
|
|
|
147
|
-
// TEST_SCENARIOS.md §24.5
|
|
148
135
|
describe('has-handler (§24.5)', () => {
|
|
149
136
|
const paywall = {
|
|
150
137
|
id: 'w',
|
|
@@ -187,7 +174,6 @@ describe('has-handler (§24.5)', () => {
|
|
|
187
174
|
});
|
|
188
175
|
});
|
|
189
176
|
|
|
190
|
-
// TEST_SCENARIOS.md §24.6
|
|
191
177
|
describe('screen-emits-data (§24.6)', () => {
|
|
192
178
|
it('marks view-only screens as non-emitting', () => {
|
|
193
179
|
expect(screenEmitsData({ id: 'a', type: 'welcome' } as OnboardingScreen)).toBe(false);
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { OnboardingScreen } from '../types/screens';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Does this screen type emit a payload to the orchestrator when the user advances?
|
|
5
|
-
* Pure view-only screens (welcome, social-proof, comparison, value-delivery) do not.
|
|
6
|
-
*/
|
|
7
3
|
const VIEW_ONLY_SCREEN_TYPES: ReadonlySet<OnboardingScreen['type']> = new Set([
|
|
8
4
|
'welcome',
|
|
9
5
|
'social-proof',
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
const PROBE_KEY = '@umituz/web-onboarding:probe';
|
|
2
2
|
const PROBE_VALUE = '1';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Can the current environment use `localStorage`?
|
|
6
|
-
* - Server-side / SSR: false
|
|
7
|
-
* - Disabled cookies/storage in the browser: false
|
|
8
|
-
* - Otherwise: true
|
|
9
|
-
*
|
|
10
|
-
* Side effect: writes and removes a tiny probe to verify both write & read work.
|
|
11
|
-
*/
|
|
12
4
|
export function isLocalStorageAvailable(): boolean {
|
|
13
5
|
if (typeof window === 'undefined') return false;
|
|
14
6
|
try {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
+
import { Button } from '@umituz/web-design-system/atoms';
|
|
2
3
|
import { Sparkles } from 'lucide-react';
|
|
3
|
-
import
|
|
4
|
-
import type { OnboardingScreen } from '../types/screens';
|
|
4
|
+
import { cn } from '@umituz/web-design-system/utils/cn';
|
|
5
|
+
import type { DemoConfig, OnboardingScreen } from '../types/screens';
|
|
5
6
|
|
|
6
7
|
export interface TemplateDemoResult {
|
|
7
8
|
variant: string;
|
|
@@ -9,14 +10,6 @@ export interface TemplateDemoResult {
|
|
|
9
10
|
generatedAt: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
* Default demo component used by the art and platform templates.
|
|
14
|
-
* Lets users produce a real artifact in the template without forcing consumers
|
|
15
|
-
* to wire up their own demo before they can preview the flow.
|
|
16
|
-
*
|
|
17
|
-
* Consumers who want their own demo should clone the template and replace
|
|
18
|
-
* `screens[?].demo.component` with their own implementation.
|
|
19
|
-
*/
|
|
20
13
|
function TemplateDemoFallback({ onSave }: { onSave: (result: TemplateDemoResult) => void }) {
|
|
21
14
|
const palettes: ReadonlyArray<{ variant: string; palette: string[] }> = [
|
|
22
15
|
{ variant: 'Minimal', palette: ['#0F172A', '#475569', '#94A3B8', '#F1F5F9', '#FFFFFF'] },
|
|
@@ -29,43 +22,38 @@ function TemplateDemoFallback({ onSave }: { onSave: (result: TemplateDemoResult)
|
|
|
29
22
|
const active = palettes[activeIdx];
|
|
30
23
|
|
|
31
24
|
return (
|
|
32
|
-
<div className="
|
|
33
|
-
<div className="
|
|
25
|
+
<div className="udb-onb-question__body">
|
|
26
|
+
<div className="udb-onb-palettes">
|
|
34
27
|
{palettes.map((p, i) => (
|
|
35
28
|
<button
|
|
36
29
|
key={p.variant}
|
|
37
30
|
type="button"
|
|
38
31
|
onClick={() => setActiveIdx(i)}
|
|
39
|
-
className={
|
|
40
|
-
i === activeIdx
|
|
41
|
-
? 'rounded-lg border-2 border-[rgb(var(--uob-primary-500))] p-2'
|
|
42
|
-
: 'rounded-lg border-2 border-transparent p-2 hover:border-[rgb(var(--uob-border))]'
|
|
43
|
-
}
|
|
32
|
+
className={cn('udb-onb-palette', i === activeIdx && 'udb-onb-palette--active')}
|
|
44
33
|
>
|
|
45
|
-
<div className="
|
|
34
|
+
<div className="udb-onb-palette__strip">
|
|
46
35
|
{p.palette.map((c) => (
|
|
47
|
-
<span key={c} className="
|
|
36
|
+
<span key={c} className="udb-onb-palette__chip" style={{ background: c }} />
|
|
48
37
|
))}
|
|
49
38
|
</div>
|
|
50
|
-
<span className="
|
|
51
|
-
{p.variant}
|
|
52
|
-
</span>
|
|
39
|
+
<span className="udb-onb-palette__name">{p.variant}</span>
|
|
53
40
|
</button>
|
|
54
41
|
))}
|
|
55
42
|
</div>
|
|
56
|
-
<div className="
|
|
57
|
-
<div className="
|
|
43
|
+
<div className="udb-onb-palette-preview">
|
|
44
|
+
<div className="udb-onb-palette__strip udb-onb-palette__strip--lg">
|
|
58
45
|
{active.palette.map((c) => (
|
|
59
|
-
<span key={c} className="
|
|
46
|
+
<span key={c} className="udb-onb-palette__chip" style={{ background: c }} />
|
|
60
47
|
))}
|
|
61
48
|
</div>
|
|
62
|
-
<p className="
|
|
63
|
-
Preview: <strong
|
|
64
|
-
palette
|
|
49
|
+
<p className="udb-onb-lead">
|
|
50
|
+
Preview: <strong>{active.variant}</strong> palette
|
|
65
51
|
</p>
|
|
66
52
|
</div>
|
|
67
|
-
<
|
|
68
|
-
|
|
53
|
+
<Button
|
|
54
|
+
variant="primary"
|
|
55
|
+
size="sm"
|
|
56
|
+
icon={<Sparkles className="udb-onb-icon--sm" />}
|
|
69
57
|
onClick={() =>
|
|
70
58
|
onSave({
|
|
71
59
|
variant: active.variant,
|
|
@@ -73,11 +61,9 @@ function TemplateDemoFallback({ onSave }: { onSave: (result: TemplateDemoResult)
|
|
|
73
61
|
generatedAt: Date.now(),
|
|
74
62
|
})
|
|
75
63
|
}
|
|
76
|
-
className="inline-flex items-center justify-center gap-2 rounded-xl bg-[rgb(var(--uob-primary-500))] px-4 py-3 text-sm font-semibold text-white hover:bg-[rgb(var(--uob-primary-600))]"
|
|
77
64
|
>
|
|
78
|
-
<Sparkles className="h-4 w-4" />
|
|
79
65
|
Save this style
|
|
80
|
-
</
|
|
66
|
+
</Button>
|
|
81
67
|
</div>
|
|
82
68
|
);
|
|
83
69
|
}
|
|
@@ -93,11 +79,6 @@ const fallbackResult: TemplateDemoResult = {
|
|
|
93
79
|
generatedAt: 0,
|
|
94
80
|
};
|
|
95
81
|
|
|
96
|
-
/**
|
|
97
|
-
* Replace an `app-demo` screen in a template with a working default.
|
|
98
|
-
* Used by the art and platform templates so the flow is fully runnable
|
|
99
|
-
* out of the box; consumers can override the demo.component for their app.
|
|
100
|
-
*/
|
|
101
82
|
export function withDefaultDemo(screen: OnboardingScreen, fallback: DemoConfig = fallbackDemo): OnboardingScreen {
|
|
102
83
|
if (screen.type !== 'app-demo') return screen;
|
|
103
84
|
return { ...screen, demo: { ...fallback, ...screen.demo } };
|
|
@@ -107,6 +88,5 @@ export function defaultDemoResult(): TemplateDemoResult {
|
|
|
107
88
|
return { ...fallbackResult };
|
|
108
89
|
}
|
|
109
90
|
|
|
110
|
-
// Re-export for convenience.
|
|
111
91
|
export { TemplateDemoFallback };
|
|
112
92
|
export type { DemoConfig };
|
|
@@ -2,15 +2,6 @@ import type { OnboardingConfig } from '../types/config';
|
|
|
2
2
|
import type { OnboardingScreen } from '../types/screens';
|
|
3
3
|
import { withDefaultDemo, defaultDemoResult } from './TemplateDemoFallback';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Art category — demo-driven, 3–5 screens.
|
|
7
|
-
* Show value fast: by the end of step 3 the user has produced a real artifact.
|
|
8
|
-
*
|
|
9
|
-
* The demo screen ships with a working palette-preview fallback so the template
|
|
10
|
-
* is runnable out of the box. Replace `screens[2].demo.component` to plug in
|
|
11
|
-
* the consumer's own demo.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
5
|
const baseScreens: OnboardingScreen[] = [
|
|
15
6
|
{
|
|
16
7
|
id: 'welcome',
|
|
@@ -40,8 +31,7 @@ const baseScreens: OnboardingScreen[] = [
|
|
|
40
31
|
type: 'app-demo',
|
|
41
32
|
title: 'Your turn — pick a palette',
|
|
42
33
|
demo: {
|
|
43
|
-
|
|
44
|
-
component: () => null,
|
|
34
|
+
component: () => null,
|
|
45
35
|
onSave: () => undefined,
|
|
46
36
|
},
|
|
47
37
|
},
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import type { OnboardingConfig } from '../types/config';
|
|
2
2
|
import type { OnboardingScreen } from '../types/screens';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Health category — privacy-first, 5–7 screens.
|
|
6
|
-
* Trust is the #1 conversion lever: lead with privacy reassurance.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
4
|
const screens: OnboardingScreen[] = [
|
|
10
5
|
{
|
|
11
6
|
id: 'welcome',
|
|
@@ -2,14 +2,6 @@ import type { OnboardingConfig } from '../types/config';
|
|
|
2
2
|
import type { OnboardingScreen } from '../types/screens';
|
|
3
3
|
import { withDefaultDemo, defaultDemoResult } from './TemplateDemoFallback';
|
|
4
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
5
|
const screens: OnboardingScreen[] = [
|
|
14
6
|
{
|
|
15
7
|
id: 'welcome',
|
|
@@ -199,7 +191,6 @@ const screens: OnboardingScreen[] = [
|
|
|
199
191
|
},
|
|
200
192
|
];
|
|
201
193
|
|
|
202
|
-
// Replace the no-op demo component with the built-in working fallback.
|
|
203
194
|
const resolvedScreens = screens.map((s) => withDefaultDemo(s));
|
|
204
195
|
|
|
205
196
|
export const platformOnboardingTemplate: OnboardingConfig = {
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import type { OnboardingConfig } from '../types/config';
|
|
2
2
|
import type { OnboardingScreen } from '../types/screens';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Video category — personalization-led, 6–8 screens.
|
|
6
|
-
* Goal: tune the recommendation engine early.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
4
|
const screens: OnboardingScreen[] = [
|
|
10
5
|
{
|
|
11
6
|
id: 'welcome',
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { afterEach, vi } from 'vitest';
|
|
2
2
|
import { cleanup } from '@testing-library/react';
|
|
3
3
|
|
|
4
|
-
// React 18 testing-library contract: let `act` warnings be raised correctly.
|
|
5
4
|
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
6
5
|
|
|
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
6
|
if (typeof window !== 'undefined')
|
|
13
7
|
Object.defineProperty(window, 'matchMedia', {
|
|
14
8
|
writable: true,
|
|
@@ -17,8 +11,7 @@ Object.defineProperty(window, 'matchMedia', {
|
|
|
17
11
|
matches: query.includes('prefers-reduced-motion'),
|
|
18
12
|
media: query,
|
|
19
13
|
onchange: null,
|
|
20
|
-
addListener: vi.fn(),
|
|
21
|
-
removeListener: vi.fn(),
|
|
14
|
+
addListener: vi.fn(), removeListener: vi.fn(),
|
|
22
15
|
addEventListener: vi.fn(),
|
|
23
16
|
removeEventListener: vi.fn(),
|
|
24
17
|
dispatchEvent: vi.fn(),
|
|
@@ -18,8 +18,7 @@ export type AnalyticsProvider = 'google-analytics' | 'amplitude' | 'mixpanel' |
|
|
|
18
18
|
export interface AnalyticsConfig {
|
|
19
19
|
trackEvents: boolean;
|
|
20
20
|
provider?: AnalyticsProvider;
|
|
21
|
-
|
|
22
|
-
customTracker?: (event: OnboardingAnalyticsEvent) => void;
|
|
21
|
+
customTracker?: (event: OnboardingAnalyticsEvent) => void;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
export type OnboardingAnalyticsEvent =
|
|
@@ -41,28 +40,18 @@ export interface OnboardingConfig {
|
|
|
41
40
|
demo?: DemoConfig;
|
|
42
41
|
paywall?: PaywallScreenConfig;
|
|
43
42
|
analytics?: AnalyticsConfig;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
labels?: Partial<OnboardingLabels>;
|
|
49
|
-
/** BCP-47 locale used for locale-aware formatting (e.g. "250.000" vs "250,000"). */
|
|
50
|
-
locale?: string;
|
|
51
|
-
/** Custom slot rendered above the flow (e.g. brand banner). */
|
|
52
|
-
headerSlot?: ReactNode;
|
|
53
|
-
/** Custom slot rendered below the flow. */
|
|
54
|
-
footerSlot?: ReactNode;
|
|
43
|
+
labels?: Partial<OnboardingLabels>;
|
|
44
|
+
locale?: string;
|
|
45
|
+
headerSlot?: ReactNode;
|
|
46
|
+
footerSlot?: ReactNode;
|
|
55
47
|
onComplete?: (data: OnboardingResult) => void;
|
|
56
48
|
onSkip?: () => void;
|
|
57
|
-
|
|
58
|
-
storageKey?: string;
|
|
49
|
+
storageKey?: string;
|
|
59
50
|
}
|
|
60
51
|
|
|
61
|
-
/** Collected data exposed to onComplete. Typed in place of `Record<string, any>`. */
|
|
62
52
|
export interface OnboardingResult {
|
|
63
53
|
completedSteps: ReadonlyArray<string>;
|
|
64
|
-
|
|
65
|
-
collected: Record<string, unknown>;
|
|
54
|
+
collected: Record<string, unknown>;
|
|
66
55
|
durationMs: number;
|
|
67
56
|
finishedAt: Date;
|
|
68
57
|
}
|
|
@@ -1,83 +1,49 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Every user-facing string the library renders lives here as an overridable
|
|
3
|
-
* label. Applications serving non-English markets pass a `labels` subset (and
|
|
4
|
-
* optionally a `locale`) via `OnboardingConfig`; anything not overridden falls
|
|
5
|
-
* back to the English defaults.
|
|
6
|
-
*
|
|
7
|
-
* Labels are flat, serializable strings — no functions — so they can be fed
|
|
8
|
-
* straight from any translation system (i18next, Lingui, a CMS, …).
|
|
9
|
-
* Pattern labels use `{placeholder}` interpolation, e.g. `"Step {current} of {total}"`.
|
|
10
|
-
*/
|
|
11
1
|
export interface OnboardingLabels {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/** Progress bar label. `{current}`/`{total}` are interpolated. */
|
|
21
|
-
stepOfPattern: string;
|
|
22
|
-
/** Card counter label. `{current}`/`{total}` are interpolated. */
|
|
23
|
-
countOfPattern: string;
|
|
24
|
-
|
|
25
|
-
/** Question screens */
|
|
26
|
-
selectAllThatApply: string;
|
|
2
|
+
continueAction: string;
|
|
3
|
+
back: string;
|
|
4
|
+
skip: string;
|
|
5
|
+
loading: string;
|
|
6
|
+
stepOfPattern: string;
|
|
7
|
+
countOfPattern: string;
|
|
8
|
+
|
|
9
|
+
selectAllThatApply: string;
|
|
27
10
|
selectOne: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
pickTheOne: string;
|
|
32
|
-
/** Preferences screen subtitle. */
|
|
33
|
-
tailorExperience: string;
|
|
11
|
+
pickUpToPattern: string;
|
|
12
|
+
pickTheOne: string;
|
|
13
|
+
tailorExperience: string;
|
|
34
14
|
|
|
35
|
-
|
|
36
|
-
agree: string;
|
|
15
|
+
agree: string;
|
|
37
16
|
disagree: string;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/** Shown when every card has been decided. */
|
|
41
|
-
allDone: string;
|
|
17
|
+
swipeHint: string;
|
|
18
|
+
allDone: string;
|
|
42
19
|
|
|
43
|
-
|
|
44
|
-
withoutUs: string;
|
|
20
|
+
withoutUs: string;
|
|
45
21
|
withUs: string;
|
|
46
22
|
|
|
47
|
-
|
|
48
|
-
previous: string;
|
|
23
|
+
previous: string;
|
|
49
24
|
nextAction: string;
|
|
50
25
|
|
|
51
|
-
|
|
52
|
-
continueWithResult: string;
|
|
26
|
+
continueWithResult: string;
|
|
53
27
|
|
|
54
|
-
|
|
55
|
-
allowAccess: string;
|
|
28
|
+
allowAccess: string;
|
|
56
29
|
permissionRequestFailed: string;
|
|
57
30
|
|
|
58
|
-
|
|
59
|
-
continueWithEmail: string;
|
|
31
|
+
continueWithEmail: string;
|
|
60
32
|
continueWithGoogle: string;
|
|
61
33
|
continueWithApple: string;
|
|
62
34
|
continueWithGitHub: string;
|
|
63
35
|
skipForNow: string;
|
|
64
36
|
signInFailed: string;
|
|
65
37
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
/** `{price}`/`{days}` interpolated, e.g. "$0.99 for the first 7 days". */
|
|
71
|
-
trialIntroPattern: string;
|
|
72
|
-
/** Reassurance line under the paywall CAs. */
|
|
73
|
-
cancelAnytimeNote: string;
|
|
38
|
+
couldNotStartTrial: string;
|
|
39
|
+
trialDaysPattern: string;
|
|
40
|
+
trialIntroPattern: string;
|
|
41
|
+
cancelAnytimeNote: string;
|
|
74
42
|
|
|
75
|
-
|
|
76
|
-
share: string;
|
|
43
|
+
share: string;
|
|
77
44
|
copied: string;
|
|
78
45
|
|
|
79
|
-
|
|
80
|
-
somethingWentWrong: string;
|
|
46
|
+
somethingWentWrong: string;
|
|
81
47
|
unexpectedError: string;
|
|
82
48
|
tryAgain: string;
|
|
83
49
|
}
|