@umituz/web-dashboard 6.1.0 → 6.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -2
- 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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
3
|
import { Star } from 'lucide-react';
|
|
4
|
-
import { PrimaryButton } from '../
|
|
5
|
-
import {
|
|
4
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
5
|
+
import { Button } from '@umituz/web-design-system/atoms';
|
|
6
|
+
import { cn } from '@umituz/web-design-system/utils/cn';
|
|
6
7
|
import { useOnboardingIntl } from '../OnboardingIntl';
|
|
7
8
|
import type { SocialProofScreenConfig } from '../../types/screens';
|
|
8
9
|
|
|
@@ -22,13 +23,9 @@ export function SocialProofScreen({ config, onNext }: SocialProofScreenProps) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
return (
|
|
25
|
-
<div className="
|
|
26
|
-
{config.title &&
|
|
27
|
-
|
|
28
|
-
{config.title}
|
|
29
|
-
</h2>
|
|
30
|
-
)}
|
|
31
|
-
<div className="flex flex-1 flex-col items-center justify-center">
|
|
26
|
+
<div className="udb-onb-proof">
|
|
27
|
+
{config.title && <h2 className="udb-onb-title">{config.title}</h2>}
|
|
28
|
+
<div className="udb-onb-proof__stage">
|
|
32
29
|
<AnimatePresence mode="wait">
|
|
33
30
|
<motion.blockquote
|
|
34
31
|
key={`${current.name}-${index}`}
|
|
@@ -36,42 +33,29 @@ export function SocialProofScreen({ config, onNext }: SocialProofScreenProps) {
|
|
|
36
33
|
animate={{ opacity: 1, y: 0 }}
|
|
37
34
|
exit={{ opacity: 0, y: -12 }}
|
|
38
35
|
transition={{ duration: 0.3 }}
|
|
39
|
-
className="
|
|
36
|
+
className="udb-onb-quote"
|
|
40
37
|
>
|
|
41
38
|
{current.rating !== undefined && (
|
|
42
|
-
<div className="
|
|
39
|
+
<div className="udb-onb-quote__stars" aria-label={`Rated ${current.rating} out of 5`}>
|
|
43
40
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
44
41
|
<Star
|
|
45
42
|
key={i}
|
|
46
|
-
className={cn(
|
|
47
|
-
'h-5 w-5',
|
|
48
|
-
i < current.rating!
|
|
49
|
-
? 'fill-current text-[rgb(var(--uob-warning))]'
|
|
50
|
-
: 'text-[rgb(var(--uob-border))]',
|
|
51
|
-
)}
|
|
43
|
+
className={cn('udb-onb-quote__star', i < current.rating! && 'udb-onb-quote__star--filled')}
|
|
52
44
|
/>
|
|
53
45
|
))}
|
|
54
46
|
</div>
|
|
55
47
|
)}
|
|
56
|
-
<p className="
|
|
57
|
-
<footer className="
|
|
58
|
-
{current.avatar &&
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
alt=""
|
|
62
|
-
className="h-12 w-12 rounded-full object-cover"
|
|
63
|
-
/>
|
|
64
|
-
)}
|
|
65
|
-
<cite className="text-sm font-semibold not-italic text-[rgb(var(--uob-text-primary))]">
|
|
66
|
-
{current.name}
|
|
67
|
-
</cite>
|
|
68
|
-
<span className="text-sm text-[rgb(var(--uob-text-muted))]">{current.role}</span>
|
|
48
|
+
<p className="udb-onb-quote__content">“{current.content}”</p>
|
|
49
|
+
<footer className="udb-onb-quote__footer">
|
|
50
|
+
{current.avatar && <img src={current.avatar} alt="" className="udb-onb-quote__avatar" />}
|
|
51
|
+
<cite className="udb-onb-quote__cite">{current.name}</cite>
|
|
52
|
+
<span className="udb-onb-quote__role">{current.role}</span>
|
|
69
53
|
</footer>
|
|
70
54
|
</motion.blockquote>
|
|
71
55
|
</AnimatePresence>
|
|
72
56
|
</div>
|
|
73
|
-
<div className="
|
|
74
|
-
<div className="
|
|
57
|
+
<div className="udb-onb-proof__bar">
|
|
58
|
+
<div className="udb-onb-dots" role="tablist" aria-label="Testimonials">
|
|
75
59
|
{config.testimonials.map((t, i) => (
|
|
76
60
|
<button
|
|
77
61
|
key={`${t.name}-${i}`}
|
|
@@ -80,32 +64,19 @@ export function SocialProofScreen({ config, onNext }: SocialProofScreenProps) {
|
|
|
80
64
|
aria-selected={i === index}
|
|
81
65
|
aria-label={`${t.name}: ${t.content}`}
|
|
82
66
|
onClick={() => setIndex(i)}
|
|
83
|
-
className={cn(
|
|
84
|
-
'h-2 w-2 rounded-full transition-all',
|
|
85
|
-
i === index
|
|
86
|
-
? 'w-6 bg-[rgb(var(--uob-primary-500))]'
|
|
87
|
-
: 'bg-[rgb(var(--uob-border))]',
|
|
88
|
-
)}
|
|
67
|
+
className={cn('udb-onb-dot', i === index && 'udb-onb-dot--active')}
|
|
89
68
|
/>
|
|
90
69
|
))}
|
|
91
70
|
</div>
|
|
92
|
-
<div className="
|
|
71
|
+
<div className="udb-onb-proof__nav">
|
|
93
72
|
{total > 1 && (
|
|
94
73
|
<>
|
|
95
|
-
<
|
|
96
|
-
type="button"
|
|
97
|
-
onClick={() => go(-1)}
|
|
98
|
-
className="rounded-md border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] px-3 py-1 text-sm hover:bg-[rgb(var(--uob-surface-muted))]"
|
|
99
|
-
>
|
|
74
|
+
<Button variant="outline" size="sm" onClick={() => go(-1)}>
|
|
100
75
|
{labels.previous}
|
|
101
|
-
</
|
|
102
|
-
<
|
|
103
|
-
type="button"
|
|
104
|
-
onClick={() => go(1)}
|
|
105
|
-
className="rounded-md border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] px-3 py-1 text-sm hover:bg-[rgb(var(--uob-surface-muted))]"
|
|
106
|
-
>
|
|
76
|
+
</Button>
|
|
77
|
+
<Button variant="outline" size="sm" onClick={() => go(1)}>
|
|
107
78
|
{labels.nextAction}
|
|
108
|
-
</
|
|
79
|
+
</Button>
|
|
109
80
|
</>
|
|
110
81
|
)}
|
|
111
82
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
2
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
3
|
-
import { SwipeCard } from '../
|
|
4
|
-
import { PrimaryButton } from '../
|
|
3
|
+
import { SwipeCard } from '../SwipeCard';
|
|
4
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
5
5
|
import {
|
|
6
6
|
getCardEnterAnimate,
|
|
7
7
|
getCardEnterExit,
|
|
@@ -22,9 +22,7 @@ export function TinderCardsScreen({ config, onNext }: TinderCardsScreenProps) {
|
|
|
22
22
|
const [liked, setLiked] = useState<string[]>([]);
|
|
23
23
|
const total = config.statements.length;
|
|
24
24
|
const current = config.statements[index];
|
|
25
|
-
|
|
26
|
-
// on every index change — which would have remounted SwipeCard and lost gestures.
|
|
27
|
-
const indexRef = useRef(index);
|
|
25
|
+
const indexRef = useRef(index);
|
|
28
26
|
const likedRef = useRef(liked);
|
|
29
27
|
useEffect(() => {
|
|
30
28
|
indexRef.current = index;
|
|
@@ -46,8 +44,8 @@ export function TinderCardsScreen({ config, onNext }: TinderCardsScreenProps) {
|
|
|
46
44
|
|
|
47
45
|
if (!current) {
|
|
48
46
|
return (
|
|
49
|
-
<div className="
|
|
50
|
-
<p className="
|
|
47
|
+
<div className="udb-onb-stage">
|
|
48
|
+
<p className="udb-onb-lead">{labels.allDone}</p>
|
|
51
49
|
<PrimaryButton onClick={() => onNext({ liked: likedRef.current })}>
|
|
52
50
|
{config.cta ?? labels.continueAction}
|
|
53
51
|
</PrimaryButton>
|
|
@@ -56,13 +54,9 @@ export function TinderCardsScreen({ config, onNext }: TinderCardsScreenProps) {
|
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
return (
|
|
59
|
-
<div className="
|
|
60
|
-
{config.title &&
|
|
61
|
-
|
|
62
|
-
{config.title}
|
|
63
|
-
</h2>
|
|
64
|
-
)}
|
|
65
|
-
<div className="flex flex-1 items-center justify-center">
|
|
57
|
+
<div className="udb-onb-proof">
|
|
58
|
+
{config.title && <h2 className="udb-onb-title">{config.title}</h2>}
|
|
59
|
+
<div className="udb-onb-proof__stage">
|
|
66
60
|
<AnimatePresence mode="wait">
|
|
67
61
|
<motion.div
|
|
68
62
|
key={current.id}
|
|
@@ -70,7 +64,7 @@ export function TinderCardsScreen({ config, onNext }: TinderCardsScreenProps) {
|
|
|
70
64
|
animate={{ opacity: getCardEnterAnimate().opacity, scale: getCardEnterAnimate().scale }}
|
|
71
65
|
exit={{ opacity: getCardEnterExit().opacity, scale: getCardEnterExit().scale }}
|
|
72
66
|
transition={{ duration: getCardTransitionDurationMs() / 1000 }}
|
|
73
|
-
className="
|
|
67
|
+
className="udb-onb-stage"
|
|
74
68
|
>
|
|
75
69
|
<SwipeCard
|
|
76
70
|
text={current.text}
|
|
@@ -81,26 +75,15 @@ export function TinderCardsScreen({ config, onNext }: TinderCardsScreenProps) {
|
|
|
81
75
|
</motion.div>
|
|
82
76
|
</AnimatePresence>
|
|
83
77
|
</div>
|
|
84
|
-
<div className="
|
|
85
|
-
<button
|
|
86
|
-
type="button"
|
|
87
|
-
onClick={onSwipeLeft}
|
|
88
|
-
className="flex-1 rounded-xl border-2 border-[rgb(var(--uob-error))] py-3 font-semibold text-[rgb(var(--uob-error))] transition-colors hover:bg-[rgb(var(--uob-error))] hover:text-white"
|
|
89
|
-
>
|
|
78
|
+
<div className="udb-onb-proof__nav">
|
|
79
|
+
<button type="button" onClick={onSwipeLeft} className="udb-onb-choice udb-onb-choice--no">
|
|
90
80
|
{labels.disagree}
|
|
91
81
|
</button>
|
|
92
|
-
<button
|
|
93
|
-
type="button"
|
|
94
|
-
onClick={onSwipeRight}
|
|
95
|
-
className="flex-1 rounded-xl border-2 border-[rgb(var(--uob-success))] py-3 font-semibold text-[rgb(var(--uob-success))] transition-colors hover:bg-[rgb(var(--uob-success))] hover:text-white"
|
|
96
|
-
>
|
|
82
|
+
<button type="button" onClick={onSwipeRight} className="udb-onb-choice udb-onb-choice--yes">
|
|
97
83
|
{labels.agree}
|
|
98
84
|
</button>
|
|
99
85
|
</div>
|
|
100
|
-
<p
|
|
101
|
-
className="text-center text-sm text-[rgb(var(--uob-text-muted))]"
|
|
102
|
-
aria-live="polite"
|
|
103
|
-
>
|
|
86
|
+
<p className="udb-onb-note" aria-live="polite">
|
|
104
87
|
{formatLabel(labels.countOfPattern, { current: index + 1, total })}
|
|
105
88
|
</p>
|
|
106
89
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Button } from '@umituz/web-design-system/atoms';
|
|
2
3
|
import { Sparkles } from 'lucide-react';
|
|
3
|
-
import { PrimaryButton } from '../
|
|
4
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
4
5
|
import { useOnboardingIntl } from '../OnboardingIntl';
|
|
5
6
|
import type { ValueDeliveryScreenConfig } from '../../types/screens';
|
|
6
7
|
|
|
@@ -9,11 +10,6 @@ export interface ValueDeliveryScreenProps<TResult = unknown> {
|
|
|
9
10
|
onNext: () => void;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
* Shows the artifact produced earlier in the flow.
|
|
14
|
-
* The default body is a JSON dump of `result` — deliberately generic. Screens are
|
|
15
|
-
* expected to be customized per product; this default keeps templates runnable.
|
|
16
|
-
*/
|
|
17
13
|
export function ValueDeliveryScreen<TResult = unknown>({
|
|
18
14
|
config,
|
|
19
15
|
onNext,
|
|
@@ -44,38 +40,26 @@ export function ValueDeliveryScreen<TResult = unknown>({
|
|
|
44
40
|
copiedTimer.current = window.setTimeout(() => setCopied(false), 2000);
|
|
45
41
|
}
|
|
46
42
|
} catch (error) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (!(error instanceof DOMException && error.name === 'AbortError')) {
|
|
50
|
-
|
|
51
|
-
console.error('[web-onboarding] share failed', error);
|
|
43
|
+
if (!(error instanceof DOMException && error.name === 'AbortError')) {
|
|
44
|
+
console.error('[onboarding] share failed', error);
|
|
52
45
|
}
|
|
53
46
|
}
|
|
54
47
|
}
|
|
55
48
|
|
|
56
49
|
return (
|
|
57
|
-
<div className="
|
|
58
|
-
<div
|
|
59
|
-
className="
|
|
60
|
-
aria-hidden="true"
|
|
61
|
-
>
|
|
62
|
-
<Sparkles className="h-10 w-10" />
|
|
50
|
+
<div className="udb-onb-stage">
|
|
51
|
+
<div className="udb-onb-hero udb-onb-hero--lg" aria-hidden="true">
|
|
52
|
+
<Sparkles className="udb-onb-icon--lg" />
|
|
63
53
|
</div>
|
|
64
|
-
<h2 className="
|
|
65
|
-
<div className="
|
|
66
|
-
<pre className="
|
|
67
|
-
{JSON.stringify(config.result, null, 2)}
|
|
68
|
-
</pre>
|
|
54
|
+
<h2 className="udb-onb-title udb-onb-title--display">{config.title}</h2>
|
|
55
|
+
<div className="udb-onb-result">
|
|
56
|
+
<pre className="udb-onb-result__pre">{JSON.stringify(config.result, null, 2)}</pre>
|
|
69
57
|
</div>
|
|
70
58
|
{config.shareConfig && (
|
|
71
|
-
<div className="
|
|
72
|
-
<
|
|
73
|
-
type="button"
|
|
74
|
-
onClick={handleShare}
|
|
75
|
-
className="rounded-xl border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] px-4 py-2 text-sm font-semibold text-[rgb(var(--uob-text-primary))] transition-colors hover:border-[rgb(var(--uob-primary-500))]"
|
|
76
|
-
>
|
|
59
|
+
<div className="udb-onb-share">
|
|
60
|
+
<Button variant="outline" size="sm" onClick={handleShare}>
|
|
77
61
|
{copied ? labels.copied : labels.share}
|
|
78
|
-
</
|
|
62
|
+
</Button>
|
|
79
63
|
<span>{config.shareConfig.text}</span>
|
|
80
64
|
</div>
|
|
81
65
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PrimaryButton } from '../
|
|
2
|
-
import { renderBundledIcon } from '../
|
|
1
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
2
|
+
import { renderBundledIcon } from '../renderBundledIcon';
|
|
3
3
|
import { doesIconNameResolve } from '../../predicates/icon-name-resolves';
|
|
4
4
|
import { useOnboardingIntl } from '../OnboardingIntl';
|
|
5
5
|
import type { WelcomeScreenConfig } from '../../types/screens';
|
|
@@ -13,40 +13,20 @@ export interface WelcomeScreenProps {
|
|
|
13
13
|
export function WelcomeScreen({ config, onNext, isLoading }: WelcomeScreenProps) {
|
|
14
14
|
const { labels } = useOnboardingIntl();
|
|
15
15
|
return (
|
|
16
|
-
<div className="
|
|
16
|
+
<div className="udb-onb-stage udb-onb-stage--wide">
|
|
17
17
|
{doesIconNameResolve(config.heroIconName) && (
|
|
18
|
-
<div
|
|
19
|
-
|
|
20
|
-
aria-hidden="true"
|
|
21
|
-
>
|
|
22
|
-
{renderBundledIcon(config.heroIconName, 'h-12 w-12')}
|
|
18
|
+
<div className="udb-onb-hero udb-onb-hero--xl" aria-hidden="true">
|
|
19
|
+
{renderBundledIcon(config.heroIconName, 'udb-onb-icon--xl')}
|
|
23
20
|
</div>
|
|
24
21
|
)}
|
|
25
|
-
<div className="
|
|
26
|
-
{config.title &&
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
</h1>
|
|
30
|
-
)}
|
|
31
|
-
{config.subtitle && (
|
|
32
|
-
<p className="text-lg text-[rgb(var(--uob-text-muted))]">{config.subtitle}</p>
|
|
33
|
-
)}
|
|
34
|
-
{config.highlight && (
|
|
35
|
-
<p className="text-sm font-semibold uppercase tracking-wide text-[rgb(var(--uob-primary-500))]">
|
|
36
|
-
{config.highlight}
|
|
37
|
-
</p>
|
|
38
|
-
)}
|
|
22
|
+
<div className="udb-onb-welcome__group">
|
|
23
|
+
{config.title && <h1 className="udb-onb-welcome__title">{config.title}</h1>}
|
|
24
|
+
{config.subtitle && <p className="udb-onb-welcome__subtitle">{config.subtitle}</p>}
|
|
25
|
+
{config.highlight && <p className="udb-onb-welcome__highlight">{config.highlight}</p>}
|
|
39
26
|
</div>
|
|
40
|
-
{config.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</PrimaryButton>
|
|
44
|
-
)}
|
|
45
|
-
{!config.cta && (
|
|
46
|
-
<PrimaryButton onClick={config.onCta ?? onNext} loading={isLoading}>
|
|
47
|
-
{labels.continueAction}
|
|
48
|
-
</PrimaryButton>
|
|
49
|
-
)}
|
|
27
|
+
<PrimaryButton onClick={config.onCta ?? onNext} loading={isLoading}>
|
|
28
|
+
{config.cta ?? labels.continueAction}
|
|
29
|
+
</PrimaryButton>
|
|
50
30
|
</div>
|
|
51
31
|
);
|
|
52
32
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { renderHook } from '@testing-library/react';
|
|
3
3
|
import { useOnboardingAnalytics } from './useOnboardingAnalytics';
|
|
4
|
-
import type { OnboardingAnalyticsEvent } from '
|
|
4
|
+
import type { OnboardingAnalyticsEvent } from '../types/config';
|
|
5
5
|
|
|
6
6
|
const event: OnboardingAnalyticsEvent = {
|
|
7
7
|
name: 'onboarding_step_completed',
|
|
@@ -2,28 +2,15 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
|
2
2
|
import type {
|
|
3
3
|
AnalyticsConfig,
|
|
4
4
|
OnboardingAnalyticsEvent,
|
|
5
|
-
} from '
|
|
6
|
-
import { trackOnboardingEvent } from '
|
|
5
|
+
} from '../types/config';
|
|
6
|
+
import { trackOnboardingEvent } from '../utils/analytics-dispatch';
|
|
7
7
|
|
|
8
8
|
export interface UseOnboardingAnalyticsReturn {
|
|
9
9
|
trackEvent: (event: OnboardingAnalyticsEvent) => void;
|
|
10
|
-
|
|
11
|
-
trackFunnel: (event: OnboardingAnalyticsEvent) => void;
|
|
10
|
+
trackFunnel: (event: OnboardingAnalyticsEvent) => void;
|
|
12
11
|
enabled: boolean;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
* Hook façade for analytics dispatch.
|
|
17
|
-
* Single responsibility: provide a stable callback that honors the analytics config.
|
|
18
|
-
*
|
|
19
|
-
* The returned callbacks are identity-stable across re-renders even when the
|
|
20
|
-
* consumer recreates the `config` object every render — event handlers and
|
|
21
|
-
* effects that depend on them do not re-fire spuriously. The latest config is
|
|
22
|
-
* read through a ref synchronized after each commit.
|
|
23
|
-
*
|
|
24
|
-
* Dispatch never throws: analytics failures are logged by the dispatcher and
|
|
25
|
-
* dropped (see `utils/analytics-dispatch`).
|
|
26
|
-
*/
|
|
27
14
|
export function useOnboardingAnalytics(config: AnalyticsConfig | undefined): UseOnboardingAnalyticsReturn {
|
|
28
15
|
const configRef = useRef(config);
|
|
29
16
|
useEffect(() => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { act, renderHook } from '@testing-library/react';
|
|
3
3
|
import { useOnboardingProgress } from './useOnboardingProgress';
|
|
4
|
-
import { defaultStorageKey } from '
|
|
5
|
-
import type { OnboardingScreen } from '
|
|
4
|
+
import { defaultStorageKey } from '../utils/default-storage-key';
|
|
5
|
+
import type { OnboardingScreen } from '../types/screens';
|
|
6
6
|
|
|
7
7
|
function makeScreens(count: number): OnboardingScreen[] {
|
|
8
8
|
return Array.from({ length: count }, (_, i) => ({
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useReducer } from 'react';
|
|
2
|
-
import type { OnboardingScreen } from '
|
|
3
|
-
import type { OnboardingResult } from '
|
|
2
|
+
import type { OnboardingScreen } from '../types/screens';
|
|
3
|
+
import type { OnboardingResult } from '../types/config';
|
|
4
4
|
import {
|
|
5
5
|
advanceStepIndex,
|
|
6
6
|
clampStepIndex,
|
|
7
7
|
retreatStepIndex,
|
|
8
|
-
} from '
|
|
8
|
+
} from '../calc/step-index-clamp';
|
|
9
9
|
import {
|
|
10
10
|
computeProgressFraction,
|
|
11
11
|
computeRemainingSteps,
|
|
12
|
-
} from '
|
|
12
|
+
} from '../calc/progress-percentage';
|
|
13
13
|
import {
|
|
14
14
|
buildOnboardingResult,
|
|
15
15
|
clearOnboardingProgress,
|
|
16
16
|
loadOnboardingProgress,
|
|
17
17
|
saveOnboardingProgress,
|
|
18
|
-
} from '
|
|
18
|
+
} from '../utils/local-storage-bridge';
|
|
19
19
|
|
|
20
20
|
interface ProgressState {
|
|
21
21
|
currentStep: number;
|
|
@@ -97,22 +97,19 @@ function reducer(state: ProgressState, action: ProgressAction): ProgressState {
|
|
|
97
97
|
interface UseOnboardingProgressOptions {
|
|
98
98
|
screens: ReadonlyArray<OnboardingScreen>;
|
|
99
99
|
storageKey?: string;
|
|
100
|
-
|
|
101
|
-
ephemeral?: boolean;
|
|
100
|
+
ephemeral?: boolean;
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
export interface UseOnboardingProgressReturn {
|
|
105
104
|
currentStep: number;
|
|
106
105
|
totalSteps: number;
|
|
107
|
-
progress: number;
|
|
108
|
-
completedSteps: ReadonlyArray<string>;
|
|
106
|
+
progress: number; completedSteps: ReadonlyArray<string>;
|
|
109
107
|
collected: Record<string, unknown>;
|
|
110
108
|
remainingSteps: number;
|
|
111
109
|
isFirst: boolean;
|
|
112
110
|
isLast: boolean;
|
|
113
111
|
isFinished: boolean;
|
|
114
|
-
|
|
115
|
-
startedAt: Date;
|
|
112
|
+
startedAt: Date;
|
|
116
113
|
next: (data?: unknown) => void;
|
|
117
114
|
prev: () => void;
|
|
118
115
|
goTo: (index: number) => void;
|
|
@@ -121,11 +118,6 @@ export interface UseOnboardingProgressReturn {
|
|
|
121
118
|
result: OnboardingResult | null;
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
/**
|
|
125
|
-
* Owns the state machine for an onboarding flow.
|
|
126
|
-
* Single responsibility: step transitions, persistence, completion.
|
|
127
|
-
* Math is delegated to `src/calc/`; storage I/O to `src/utils/local-storage-bridge`.
|
|
128
|
-
*/
|
|
129
121
|
export function useOnboardingProgress(
|
|
130
122
|
options: UseOnboardingProgressOptions,
|
|
131
123
|
): UseOnboardingProgressReturn {
|
|
@@ -157,14 +149,11 @@ export function useOnboardingProgress(
|
|
|
157
149
|
lastUpdated: now,
|
|
158
150
|
finishedAt: null,
|
|
159
151
|
};
|
|
160
|
-
|
|
161
|
-
// We recompute if the screen set actually changes (different totalSteps).
|
|
162
|
-
}, [totalSteps, storageKey, ephemeral]);
|
|
152
|
+
}, [totalSteps, storageKey, ephemeral]);
|
|
163
153
|
|
|
164
154
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
165
155
|
|
|
166
|
-
|
|
167
|
-
useEffect(() => {
|
|
156
|
+
useEffect(() => {
|
|
168
157
|
if (ephemeral) return;
|
|
169
158
|
saveOnboardingProgress(storageKey, {
|
|
170
159
|
currentStep: state.currentStep,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
import type { PermissionStatus, PermissionType } from '
|
|
2
|
+
import type { PermissionStatus, PermissionType } from '../types/screens';
|
|
3
3
|
import {
|
|
4
4
|
isPermissionDecided,
|
|
5
5
|
isPermissionUnsupported,
|
|
6
|
-
} from '
|
|
7
|
-
import { queryPermissionStatus, requestPermission } from '
|
|
6
|
+
} from '../predicates/permission-granted';
|
|
7
|
+
import { queryPermissionStatus, requestPermission } from '../utils/permission-bridge';
|
|
8
8
|
|
|
9
9
|
export interface UsePermissionPrimingReturn {
|
|
10
10
|
status: PermissionStatus;
|
|
@@ -13,13 +13,6 @@ export interface UsePermissionPrimingReturn {
|
|
|
13
13
|
reset: () => void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Owns permission state for a single permission type.
|
|
18
|
-
* Single responsibility: query + request lifecycle, no UI.
|
|
19
|
-
*
|
|
20
|
-
* The `requestPermission` callback is a no-op (returns the current status)
|
|
21
|
-
* once the permission has been decided or the runtime reports no support.
|
|
22
|
-
*/
|
|
23
16
|
export function usePermissionPriming(permission: PermissionType): UsePermissionPrimingReturn {
|
|
24
17
|
const [status, setStatus] = useState<PermissionStatus>('prompt');
|
|
25
18
|
const [isRequesting, setIsRequesting] = useState(false);
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Detects the user's `prefers-reduced-motion` setting.
|
|
5
|
-
* Single responsibility: surface the OS-level preference to consumers so
|
|
6
|
-
* motion-heavy UI (Framer Motion, transitions) can opt out for accessibility.
|
|
7
|
-
*/
|
|
8
3
|
export function useReducedMotion(): boolean {
|
|
9
4
|
const getInitial = (): boolean => {
|
|
10
5
|
if (typeof window === 'undefined' || !window.matchMedia) return false;
|
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @umituz/web-onboarding — Public API surface.
|
|
3
|
-
* Designed for tree-shaking: only import what you use.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import './theme/index.css';
|
|
7
|
-
|
|
8
|
-
// Orchestrator (the entry point)
|
|
9
1
|
export { OnboardingFlow } from './components/OnboardingFlow';
|
|
10
2
|
export type { OnboardingFlowProps } from './components/OnboardingFlow';
|
|
11
3
|
|
|
12
|
-
// Localization / labels (i18n hooks for consumers + class-component access
|
|
13
|
-
// to the same context the function components use)
|
|
14
4
|
export {
|
|
15
5
|
OnboardingIntlProvider,
|
|
16
6
|
OnboardingIntlContext,
|
|
@@ -20,7 +10,6 @@ export {
|
|
|
20
10
|
} from './components/OnboardingIntl';
|
|
21
11
|
export type { OnboardingIntl, OnboardingIntlProviderProps } from './components/OnboardingIntl';
|
|
22
12
|
|
|
23
|
-
// Screens
|
|
24
13
|
export {
|
|
25
14
|
WelcomeScreen,
|
|
26
15
|
GoalQuestionScreen,
|
|
@@ -38,33 +27,33 @@ export {
|
|
|
38
27
|
PaywallScreen,
|
|
39
28
|
} from './components/screens';
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
export {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} from './components/
|
|
52
|
-
export type {
|
|
30
|
+
export { ProgressBar } from './components/ProgressBar';
|
|
31
|
+
export type { ProgressBarProps } from './components/ProgressBar';
|
|
32
|
+
export { QuestionWrapper } from './components/QuestionWrapper';
|
|
33
|
+
export type { QuestionWrapperProps } from './components/QuestionWrapper';
|
|
34
|
+
export { SwipeCard } from './components/SwipeCard';
|
|
35
|
+
export type { SwipeCardProps } from './components/SwipeCard';
|
|
36
|
+
export { AnimationWrapper } from './components/AnimationWrapper';
|
|
37
|
+
export type { AnimationWrapperProps, ScreenTransition } from './components/AnimationWrapper';
|
|
38
|
+
export { ScreenContainer } from './components/ScreenContainer';
|
|
39
|
+
export type { ScreenContainerProps } from './components/ScreenContainer';
|
|
40
|
+
export { PrimaryButton } from './components/PrimaryButton';
|
|
41
|
+
export type { PrimaryButtonProps } from './components/PrimaryButton';
|
|
42
|
+
export { OnboardingErrorBoundary } from './components/OnboardingErrorBoundary';
|
|
43
|
+
export type { OnboardingErrorBoundaryProps } from './components/OnboardingErrorBoundary';
|
|
53
44
|
|
|
54
|
-
// Hooks
|
|
55
45
|
export {
|
|
56
46
|
useOnboardingProgress,
|
|
57
47
|
usePermissionPriming,
|
|
58
48
|
useOnboardingAnalytics,
|
|
59
49
|
useReducedMotion,
|
|
60
|
-
} from './
|
|
50
|
+
} from './hooks';
|
|
61
51
|
export type {
|
|
62
52
|
UseOnboardingProgressReturn,
|
|
63
53
|
UsePermissionPrimingReturn,
|
|
64
54
|
UseOnboardingAnalyticsReturn,
|
|
65
|
-
} from './
|
|
55
|
+
} from './hooks';
|
|
66
56
|
|
|
67
|
-
// Templates
|
|
68
57
|
export {
|
|
69
58
|
healthOnboardingTemplate,
|
|
70
59
|
artOnboardingTemplate,
|
|
@@ -76,7 +65,6 @@ export {
|
|
|
76
65
|
} from './templates';
|
|
77
66
|
export type { TemplateDemoResult } from './templates';
|
|
78
67
|
|
|
79
|
-
// Calculations (pure math)
|
|
80
68
|
export {
|
|
81
69
|
computeProgressFraction,
|
|
82
70
|
computeProgressPercentage,
|
|
@@ -107,7 +95,6 @@ export {
|
|
|
107
95
|
} from './calc';
|
|
108
96
|
export type { CardMotionState } from './calc';
|
|
109
97
|
|
|
110
|
-
// Validation
|
|
111
98
|
export {
|
|
112
99
|
checkFlowStructure,
|
|
113
100
|
checkScreenConfig,
|
|
@@ -118,7 +105,6 @@ export {
|
|
|
118
105
|
} from './validation';
|
|
119
106
|
export type { ValidationOutcome, StoredProgressShape, StoredProgressOutcome } from './validation';
|
|
120
107
|
|
|
121
|
-
// Predicates
|
|
122
108
|
export {
|
|
123
109
|
isPermissionGranted,
|
|
124
110
|
isPermissionDenied,
|
|
@@ -140,7 +126,6 @@ export {
|
|
|
140
126
|
screenEmitsData,
|
|
141
127
|
} from './predicates';
|
|
142
128
|
|
|
143
|
-
// I/O utilities
|
|
144
129
|
export {
|
|
145
130
|
trackOnboardingEvent,
|
|
146
131
|
dispatchAnalyticsEvent,
|
|
@@ -154,20 +139,6 @@ export {
|
|
|
154
139
|
} from './utils';
|
|
155
140
|
export type { RehydratedProgress } from './utils';
|
|
156
141
|
|
|
157
|
-
// Theme tokens (for consumers that want to rebrand)
|
|
158
|
-
export {
|
|
159
|
-
spacing,
|
|
160
|
-
radius,
|
|
161
|
-
typography,
|
|
162
|
-
zIndex,
|
|
163
|
-
motion as motionTokens,
|
|
164
|
-
breakpoint,
|
|
165
|
-
colorCssVar,
|
|
166
|
-
cn,
|
|
167
|
-
} from './theme';
|
|
168
|
-
export type { SpacingToken, RadiusToken, FontSizeToken } from './theme';
|
|
169
|
-
|
|
170
|
-
// Types
|
|
171
142
|
export type {
|
|
172
143
|
OnboardingScreen,
|
|
173
144
|
OnboardingScreenType,
|