@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { PrimaryButton } from '../
|
|
2
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
3
3
|
import { useOnboardingIntl } from '../OnboardingIntl';
|
|
4
4
|
import type { AppDemoScreenConfig } from '../../types/screens';
|
|
5
5
|
|
|
@@ -11,18 +11,12 @@ export interface AppDemoScreenProps {
|
|
|
11
11
|
export function AppDemoScreen({ config, onNext }: AppDemoScreenProps) {
|
|
12
12
|
const { labels } = useOnboardingIntl();
|
|
13
13
|
const [result, setResult] = useState<unknown>(null);
|
|
14
|
-
// The demo component is a consumer-supplied component reference held in config —
|
|
15
|
-
// aliasing it here is a lookup, not creation.
|
|
16
14
|
const Demo = config.demo.component;
|
|
17
15
|
|
|
18
16
|
return (
|
|
19
|
-
<div className="
|
|
20
|
-
{config.title &&
|
|
21
|
-
|
|
22
|
-
{config.title}
|
|
23
|
-
</h2>
|
|
24
|
-
)}
|
|
25
|
-
<div className="rounded-2xl border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface-muted))] p-4">
|
|
17
|
+
<div className="udb-onb-proof">
|
|
18
|
+
{config.title && <h2 className="udb-onb-title">{config.title}</h2>}
|
|
19
|
+
<div className="udb-onb-demo">
|
|
26
20
|
<Demo
|
|
27
21
|
{...((config.demo.props ?? {}) as Record<string, unknown>)}
|
|
28
22
|
onSave={(value: unknown) => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Check, X } from 'lucide-react';
|
|
2
|
-
import { PrimaryButton } from '../
|
|
3
|
-
import { cn } from '../../theme/cn';
|
|
2
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
4
3
|
import { useOnboardingIntl } from '../OnboardingIntl';
|
|
5
4
|
import type { ComparisonScreenConfig } from '../../types/screens';
|
|
6
5
|
|
|
@@ -12,13 +11,9 @@ export interface ComparisonScreenProps {
|
|
|
12
11
|
export function ComparisonScreen({ config, onNext }: ComparisonScreenProps) {
|
|
13
12
|
const { labels } = useOnboardingIntl();
|
|
14
13
|
return (
|
|
15
|
-
<div className="
|
|
16
|
-
{config.title &&
|
|
17
|
-
|
|
18
|
-
{config.title}
|
|
19
|
-
</h2>
|
|
20
|
-
)}
|
|
21
|
-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
14
|
+
<div className="udb-onb-proof">
|
|
15
|
+
{config.title && <h2 className="udb-onb-title">{config.title}</h2>}
|
|
16
|
+
<div className="udb-onb-compare">
|
|
22
17
|
<ComparisonColumn heading={labels.withoutUs} items={config.before} tone="negative" />
|
|
23
18
|
<ComparisonColumn heading={labels.withUs} items={config.after} tone="positive" />
|
|
24
19
|
</div>
|
|
@@ -35,21 +30,16 @@ interface ComparisonColumnProps {
|
|
|
35
30
|
|
|
36
31
|
function ComparisonColumn({ heading, items, tone }: ComparisonColumnProps) {
|
|
37
32
|
const Icon = tone === 'positive' ? Check : X;
|
|
38
|
-
const colorClass =
|
|
39
|
-
tone === 'positive'
|
|
40
|
-
? 'border-[rgb(var(--uob-success))] bg-[rgb(var(--uob-surface))]'
|
|
41
|
-
: 'border-[rgb(var(--uob-error))] bg-[rgb(var(--uob-surface))]';
|
|
42
|
-
const iconColor =
|
|
43
|
-
tone === 'positive'
|
|
44
|
-
? 'text-[rgb(var(--uob-success))]'
|
|
45
|
-
: 'text-[rgb(var(--uob-error))]';
|
|
46
33
|
return (
|
|
47
|
-
<div className={
|
|
48
|
-
<h3 className="
|
|
49
|
-
<ul className="
|
|
34
|
+
<div className={`udb-onb-compare__col udb-onb-compare__col--${tone === 'positive' ? 'pos' : 'neg'}`}>
|
|
35
|
+
<h3 className="udb-onb-compare__heading">{heading}</h3>
|
|
36
|
+
<ul className="udb-onb-list">
|
|
50
37
|
{items.map((item) => (
|
|
51
|
-
<li key={item} className="
|
|
52
|
-
<Icon
|
|
38
|
+
<li key={item} className="udb-onb-compare__item">
|
|
39
|
+
<Icon
|
|
40
|
+
className={`udb-onb-icon--sm udb-onb-compare__icon udb-onb-compare__icon--${tone === 'positive' ? 'pos' : 'neg'}`}
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
/>
|
|
53
43
|
<span>{item}</span>
|
|
54
44
|
</li>
|
|
55
45
|
))}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
|
-
import { OptionCard } from '
|
|
3
|
-
import { PrimaryButton } from '../
|
|
4
|
-
import { QuestionWrapper } from '../
|
|
2
|
+
import { OptionCard } from '@umituz/web-design-system/molecules';
|
|
3
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
4
|
+
import { QuestionWrapper } from '../QuestionWrapper';
|
|
5
|
+
import { renderBundledIcon } from '../renderBundledIcon';
|
|
5
6
|
import { useOnboardingIntl } from '../OnboardingIntl';
|
|
6
7
|
import type { GoalQuestionScreenConfig } from '../../types/screens';
|
|
7
8
|
|
|
@@ -34,16 +35,18 @@ export function GoalQuestionScreen({ config, onNext, initialSelected }: GoalQues
|
|
|
34
35
|
subtitle={multiSelect ? labels.selectAllThatApply : labels.selectOne}
|
|
35
36
|
>
|
|
36
37
|
<div
|
|
37
|
-
className="
|
|
38
|
+
className="udb-onb-list"
|
|
38
39
|
role={multiSelect ? 'group' : 'radiogroup'}
|
|
39
40
|
aria-label={config.question}
|
|
40
41
|
>
|
|
41
42
|
{config.options.map((option) => (
|
|
42
43
|
<OptionCard
|
|
43
44
|
key={option.id}
|
|
44
|
-
|
|
45
|
+
label={option.label}
|
|
46
|
+
description={option.description}
|
|
47
|
+
icon={renderBundledIcon(option.iconName)}
|
|
45
48
|
selected={selected.includes(option.id)}
|
|
46
|
-
|
|
49
|
+
onSelect={() => toggle(option.id)}
|
|
47
50
|
multiSelect={multiSelect}
|
|
48
51
|
/>
|
|
49
52
|
))}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
|
-
import { OptionCard } from '
|
|
3
|
-
import { PrimaryButton } from '../
|
|
4
|
-
import { QuestionWrapper } from '../
|
|
2
|
+
import { OptionCard } from '@umituz/web-design-system/molecules';
|
|
3
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
4
|
+
import { QuestionWrapper } from '../QuestionWrapper';
|
|
5
|
+
import { renderBundledIcon } from '../renderBundledIcon';
|
|
5
6
|
import { formatLabel, useOnboardingIntl } from '../OnboardingIntl';
|
|
6
7
|
import type { PainPointsScreenConfig } from '../../types/screens';
|
|
7
8
|
|
|
@@ -38,21 +39,20 @@ export function PainPointsScreen({ config, onNext, initialSelected }: PainPoints
|
|
|
38
39
|
);
|
|
39
40
|
|
|
40
41
|
return (
|
|
41
|
-
<QuestionWrapper
|
|
42
|
-
title={config.title}
|
|
43
|
-
subtitle={subtitle}
|
|
44
|
-
>
|
|
42
|
+
<QuestionWrapper title={config.title} subtitle={subtitle}>
|
|
45
43
|
<div
|
|
46
|
-
className="
|
|
44
|
+
className="udb-onb-list"
|
|
47
45
|
role={multiSelect ? 'group' : 'radiogroup'}
|
|
48
46
|
aria-label={config.title}
|
|
49
47
|
>
|
|
50
48
|
{config.painPoints.map((point) => (
|
|
51
49
|
<OptionCard
|
|
52
50
|
key={point.id}
|
|
53
|
-
|
|
51
|
+
label={point.label}
|
|
52
|
+
description={point.description}
|
|
53
|
+
icon={renderBundledIcon(point.iconName)}
|
|
54
54
|
selected={selected.includes(point.id)}
|
|
55
|
-
|
|
55
|
+
onSelect={() => toggle(point.id)}
|
|
56
56
|
multiSelect={multiSelect}
|
|
57
57
|
/>
|
|
58
58
|
))}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { motion } from 'framer-motion';
|
|
3
3
|
import { ShieldCheck } from 'lucide-react';
|
|
4
|
-
import { PrimaryButton } from '../
|
|
5
|
-
import { renderBundledIcon } from '../
|
|
6
|
-
import { useOnboardingAnalytics } from '
|
|
4
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
5
|
+
import { renderBundledIcon } from '../renderBundledIcon';
|
|
6
|
+
import { useOnboardingAnalytics } from '../../hooks/useOnboardingAnalytics';
|
|
7
7
|
import { doesIconNameResolve } from '../../predicates/icon-name-resolves';
|
|
8
8
|
import { formatCountWithLocale } from '../../calc/count-with-locale';
|
|
9
|
+
import { cn } from '@umituz/web-design-system/utils/cn';
|
|
9
10
|
import { formatLabel, useOnboardingIntl } from '../OnboardingIntl';
|
|
10
11
|
import type { PaywallScreenConfig } from '../../types/screens';
|
|
11
12
|
import type { AnalyticsConfig } from '../../types/config';
|
|
@@ -16,20 +17,12 @@ export interface PaywallScreenProps {
|
|
|
16
17
|
analytics?: AnalyticsConfig;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
-
* Hard paywall with trial.
|
|
21
|
-
*
|
|
22
|
-
* Error contract: if `onAccept` throws (network failure, provider outage), the
|
|
23
|
-
* user STAYS on this screen with a retryable error. A failed purchase attempt
|
|
24
|
-
* must never silently advance the user past the paywall.
|
|
25
|
-
*/
|
|
26
20
|
export function PaywallScreen({ config, onNext, analytics }: PaywallScreenProps) {
|
|
27
21
|
const { labels, locale } = useOnboardingIntl();
|
|
28
22
|
const { trackEvent } = useOnboardingAnalytics(analytics);
|
|
29
23
|
const [busy, setBusy] = useState(false);
|
|
30
24
|
const [error, setError] = useState<string | null>(null);
|
|
31
25
|
|
|
32
|
-
// "Viewed" is a view event — fire when the paywall is shown, not on interaction.
|
|
33
26
|
useEffect(() => {
|
|
34
27
|
trackEvent({ name: 'paywall_viewed', stepId: config.id, timestamp: Date.now() });
|
|
35
28
|
}, [trackEvent, config.id]);
|
|
@@ -55,21 +48,19 @@ export function PaywallScreen({ config, onNext, analytics }: PaywallScreenProps)
|
|
|
55
48
|
}
|
|
56
49
|
|
|
57
50
|
return (
|
|
58
|
-
<div className="
|
|
59
|
-
<div className="
|
|
60
|
-
<h2 className="
|
|
61
|
-
|
|
62
|
-
</h2>
|
|
63
|
-
<p className="text-base text-[rgb(var(--uob-text-muted))]">{config.subtitle}</p>
|
|
51
|
+
<div className="udb-onb-proof">
|
|
52
|
+
<div className="udb-onb-question__head udb-onb-question__head--center">
|
|
53
|
+
<h2 className="udb-onb-title udb-onb-title--display">{config.title}</h2>
|
|
54
|
+
<p className="udb-onb-lead">{config.subtitle}</p>
|
|
64
55
|
</div>
|
|
65
56
|
<motion.div
|
|
66
57
|
initial={{ scale: 0.95, opacity: 0 }}
|
|
67
58
|
animate={{ scale: 1, opacity: 1 }}
|
|
68
59
|
transition={{ duration: 0.3 }}
|
|
69
|
-
className="
|
|
60
|
+
className="udb-onb-price"
|
|
70
61
|
>
|
|
71
|
-
<div className="
|
|
72
|
-
<div className="
|
|
62
|
+
<div className="udb-onb-price__amount">{config.price}</div>
|
|
63
|
+
<div className="udb-onb-price__terms">
|
|
73
64
|
{config.trialPrice !== undefined
|
|
74
65
|
? formatLabel(labels.trialIntroPattern, {
|
|
75
66
|
price: config.trialPrice,
|
|
@@ -78,39 +69,32 @@ export function PaywallScreen({ config, onNext, analytics }: PaywallScreenProps)
|
|
|
78
69
|
: formatLabel(labels.trialDaysPattern, { count: config.trialDays })}
|
|
79
70
|
</div>
|
|
80
71
|
</motion.div>
|
|
81
|
-
<ul className="
|
|
72
|
+
<ul className="udb-onb-list">
|
|
82
73
|
{config.features.map((feature, i) => (
|
|
83
74
|
<li
|
|
84
75
|
key={`${feature.text}-${i}`}
|
|
85
|
-
className={
|
|
86
|
-
feature.highlighted
|
|
87
|
-
? 'flex items-start gap-3 rounded-lg border border-[rgb(var(--uob-primary-500))] bg-[rgb(var(--uob-primary-50))] p-3'
|
|
88
|
-
: 'flex items-start gap-3 rounded-lg border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] p-3'
|
|
89
|
-
}
|
|
76
|
+
className={cn('udb-onb-feature', feature.highlighted && 'udb-onb-feature--hl')}
|
|
90
77
|
>
|
|
91
78
|
{doesIconNameResolve(feature.iconName) && (
|
|
92
|
-
<span className="
|
|
93
|
-
{renderBundledIcon(feature.iconName, '
|
|
79
|
+
<span className="udb-onb-feature__icon">
|
|
80
|
+
{renderBundledIcon(feature.iconName, 'udb-onb-icon')}
|
|
94
81
|
</span>
|
|
95
82
|
)}
|
|
96
|
-
<span
|
|
83
|
+
<span>{feature.text}</span>
|
|
97
84
|
</li>
|
|
98
85
|
))}
|
|
99
86
|
</ul>
|
|
100
87
|
{config.socialProof && (
|
|
101
|
-
<p className="
|
|
88
|
+
<p className="udb-onb-note">
|
|
102
89
|
{config.socialProof.text} ({formatCountWithLocale(config.socialProof.count, locale)}+ users)
|
|
103
90
|
</p>
|
|
104
91
|
)}
|
|
105
92
|
{error && (
|
|
106
|
-
<p
|
|
107
|
-
role="alert"
|
|
108
|
-
className="rounded-md border border-[rgb(var(--uob-error))] bg-[rgb(var(--uob-error))]/10 p-3 text-sm text-[rgb(var(--uob-error))]"
|
|
109
|
-
>
|
|
93
|
+
<p role="alert" className="udb-onb-error">
|
|
110
94
|
{error}
|
|
111
95
|
</p>
|
|
112
96
|
)}
|
|
113
|
-
<div className="
|
|
97
|
+
<div className="udb-onb-question__body">
|
|
114
98
|
<PrimaryButton onClick={handleAccept} loading={busy}>
|
|
115
99
|
{config.cta}
|
|
116
100
|
</PrimaryButton>
|
|
@@ -123,14 +107,14 @@ export function PaywallScreen({ config, onNext, analytics }: PaywallScreenProps)
|
|
|
123
107
|
config.onSecondaryCta?.();
|
|
124
108
|
onNext({ accepted: false, trialStarted: false });
|
|
125
109
|
}}
|
|
126
|
-
className="
|
|
110
|
+
className="udb-onb-link"
|
|
127
111
|
>
|
|
128
112
|
{config.secondaryCta}
|
|
129
113
|
</button>
|
|
130
114
|
)}
|
|
131
115
|
</div>
|
|
132
|
-
<p className="
|
|
133
|
-
<ShieldCheck className="
|
|
116
|
+
<p className="udb-onb-note">
|
|
117
|
+
<ShieldCheck className="udb-onb-icon--xs" />
|
|
134
118
|
{labels.cancelAnytimeNote}
|
|
135
119
|
</p>
|
|
136
120
|
</div>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { PrimaryButton } from '../
|
|
3
|
-
import { renderBundledIcon } from '../
|
|
4
|
-
import { usePermissionPriming } from '
|
|
5
|
-
import { useOnboardingAnalytics } from '
|
|
2
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
3
|
+
import { renderBundledIcon } from '../renderBundledIcon';
|
|
4
|
+
import { usePermissionPriming } from '../../hooks/usePermissionPriming';
|
|
5
|
+
import { useOnboardingAnalytics } from '../../hooks/useOnboardingAnalytics';
|
|
6
6
|
import { doesIconNameResolve } from '../../predicates/icon-name-resolves';
|
|
7
7
|
import { isPermissionGranted } from '../../predicates/permission-granted';
|
|
8
8
|
import { useOnboardingIntl } from '../OnboardingIntl';
|
|
@@ -27,13 +27,9 @@ export interface PermissionPrimingScreenProps {
|
|
|
27
27
|
export function PermissionPrimingScreen({ config, onNext, analytics }: PermissionPrimingScreenProps) {
|
|
28
28
|
const { labels } = useOnboardingIntl();
|
|
29
29
|
const { trackEvent } = useOnboardingAnalytics(analytics);
|
|
30
|
-
// Hook tracks the live permission state for UI feedback; the screen invokes the
|
|
31
|
-
// consumer-provided `config.onRequest` so the consumer controls the actual mechanism.
|
|
32
30
|
const { status } = usePermissionPriming(config.permission);
|
|
33
31
|
const [isRequesting, setIsRequesting] = useState(false);
|
|
34
32
|
const [error, setError] = useState<string | null>(null);
|
|
35
|
-
// Configured icon wins; fall back to the permission's default glyph when the
|
|
36
|
-
// configured name is missing or not in the bundled registry.
|
|
37
33
|
const iconName = doesIconNameResolve(config.iconName)
|
|
38
34
|
? config.iconName
|
|
39
35
|
: PERMISSION_DEFAULT_ICON[config.permission];
|
|
@@ -71,37 +67,26 @@ export function PermissionPrimingScreen({ config, onNext, analytics }: Permissio
|
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
return (
|
|
74
|
-
<div className="
|
|
70
|
+
<div className="udb-onb-proof">
|
|
75
71
|
{doesIconNameResolve(iconName) && (
|
|
76
|
-
<div
|
|
77
|
-
|
|
78
|
-
aria-hidden="true"
|
|
79
|
-
>
|
|
80
|
-
{renderBundledIcon(iconName, 'h-10 w-10')}
|
|
72
|
+
<div className="udb-onb-hero udb-onb-hero--lg" aria-hidden="true">
|
|
73
|
+
{renderBundledIcon(iconName, 'udb-onb-icon--lg')}
|
|
81
74
|
</div>
|
|
82
75
|
)}
|
|
83
|
-
<div className="
|
|
84
|
-
<h2 className="
|
|
85
|
-
|
|
86
|
-
</h2>
|
|
87
|
-
<p className="text-base text-[rgb(var(--uob-text-muted))]">{config.description}</p>
|
|
76
|
+
<div className="udb-onb-question__head">
|
|
77
|
+
<h2 className="udb-onb-title">{config.title}</h2>
|
|
78
|
+
<p className="udb-onb-lead">{config.description}</p>
|
|
88
79
|
</div>
|
|
89
|
-
<ul className="
|
|
80
|
+
<ul className="udb-onb-list">
|
|
90
81
|
{config.benefits.map((benefit) => (
|
|
91
|
-
<li
|
|
92
|
-
|
|
93
|
-
className="flex items-start gap-2 rounded-lg border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] p-3 text-sm text-[rgb(var(--uob-text-primary))]"
|
|
94
|
-
>
|
|
95
|
-
<span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-[rgb(var(--uob-primary-500))]" />
|
|
82
|
+
<li key={benefit} className="udb-onb-benefit">
|
|
83
|
+
<span className="udb-onb-benefit__dot" />
|
|
96
84
|
<span>{benefit}</span>
|
|
97
85
|
</li>
|
|
98
86
|
))}
|
|
99
87
|
</ul>
|
|
100
88
|
{error && (
|
|
101
|
-
<p
|
|
102
|
-
role="alert"
|
|
103
|
-
className="rounded-md border border-[rgb(var(--uob-error))] bg-[rgb(var(--uob-error))]/10 p-3 text-sm text-[rgb(var(--uob-error))]"
|
|
104
|
-
>
|
|
89
|
+
<p role="alert" className="udb-onb-error">
|
|
105
90
|
{error}
|
|
106
91
|
</p>
|
|
107
92
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { motion } from 'framer-motion';
|
|
2
2
|
import { CheckCircle2 } from 'lucide-react';
|
|
3
|
-
import { PrimaryButton } from '../
|
|
3
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
4
4
|
import { useOnboardingIntl } from '../OnboardingIntl';
|
|
5
5
|
import type { PersonalizedSolutionScreenConfig } from '../../types/screens';
|
|
6
6
|
|
|
@@ -14,35 +14,22 @@ export function PersonalizedSolutionScreen({ config, onNext }: PersonalizedSolut
|
|
|
14
14
|
const visible = config.solutions.filter((s) => config.selectedPainPointIds.includes(s.painPointId));
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
|
-
<div className="
|
|
18
|
-
{config.title &&
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
</h2>
|
|
22
|
-
)}
|
|
23
|
-
{config.subtitle && (
|
|
24
|
-
<p className="text-base text-[rgb(var(--uob-text-muted))]">{config.subtitle}</p>
|
|
25
|
-
)}
|
|
26
|
-
<ul className="flex flex-col gap-3">
|
|
17
|
+
<div className="udb-onb-proof">
|
|
18
|
+
{config.title && <h2 className="udb-onb-title">{config.title}</h2>}
|
|
19
|
+
{config.subtitle && <p className="udb-onb-lead">{config.subtitle}</p>}
|
|
20
|
+
<ul className="udb-onb-solutions">
|
|
27
21
|
{visible.map((solution, i) => (
|
|
28
22
|
<motion.li
|
|
29
23
|
key={solution.painPointId}
|
|
30
24
|
initial={{ opacity: 0, x: -12 }}
|
|
31
25
|
animate={{ opacity: 1, x: 0 }}
|
|
32
26
|
transition={{ delay: i * 0.1, duration: 0.3 }}
|
|
33
|
-
className="
|
|
27
|
+
className="udb-onb-solution"
|
|
34
28
|
>
|
|
35
|
-
<CheckCircle2
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<div className="flex flex-1 flex-col gap-1">
|
|
40
|
-
<p className="text-base text-[rgb(var(--uob-text-primary))]">{solution.solution}</p>
|
|
41
|
-
{solution.statistic && (
|
|
42
|
-
<p className="text-sm font-semibold text-[rgb(var(--uob-primary-500))]">
|
|
43
|
-
{solution.statistic}
|
|
44
|
-
</p>
|
|
45
|
-
)}
|
|
29
|
+
<CheckCircle2 className="udb-onb-icon udb-onb-solution__icon" aria-hidden="true" />
|
|
30
|
+
<div className="udb-onb-solution__body">
|
|
31
|
+
<p>{solution.solution}</p>
|
|
32
|
+
{solution.statistic && <p className="udb-onb-solution__stat">{solution.statistic}</p>}
|
|
46
33
|
</div>
|
|
47
34
|
</motion.li>
|
|
48
35
|
))}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useId, useState } from 'react';
|
|
2
|
-
import { PrimaryButton } from '../
|
|
3
|
-
import { QuestionWrapper } from '../
|
|
2
|
+
import { PrimaryButton } from '../PrimaryButton';
|
|
3
|
+
import { QuestionWrapper } from '../QuestionWrapper';
|
|
4
4
|
import { useOnboardingIntl } from '../OnboardingIntl';
|
|
5
|
+
import { cn } from '@umituz/web-design-system/utils/cn';
|
|
5
6
|
import {
|
|
6
7
|
clampValueToBounds,
|
|
7
8
|
resolveSliderBounds,
|
|
@@ -64,7 +65,7 @@ export function PreferencesScreen({ config, onNext }: PreferencesScreenProps) {
|
|
|
64
65
|
|
|
65
66
|
return (
|
|
66
67
|
<QuestionWrapper title={config.title} subtitle={labels.tailorExperience}>
|
|
67
|
-
<div className="
|
|
68
|
+
<div className="udb-onb-prefs">
|
|
68
69
|
{config.preferences.map((item) => (
|
|
69
70
|
<PreferenceRow key={item.id} item={item} value={values[item.id]} onChange={(v) => update(item.id, v)} />
|
|
70
71
|
))}
|
|
@@ -85,8 +86,8 @@ interface PreferenceRowProps {
|
|
|
85
86
|
function PreferenceRow({ item, value, onChange }: PreferenceRowProps) {
|
|
86
87
|
const fieldId = useId();
|
|
87
88
|
return (
|
|
88
|
-
<div className="
|
|
89
|
-
<label className="
|
|
89
|
+
<div className="udb-onb-field">
|
|
90
|
+
<label className="udb-onb-field__label" htmlFor={fieldId}>
|
|
90
91
|
{item.label}
|
|
91
92
|
</label>
|
|
92
93
|
<PreferenceControl item={item} value={value} onChange={onChange} fieldId={fieldId} />
|
|
@@ -104,7 +105,7 @@ function PreferenceControl({ item, value, onChange, fieldId }: PreferenceControl
|
|
|
104
105
|
return (
|
|
105
106
|
<select
|
|
106
107
|
id={fieldId}
|
|
107
|
-
className="
|
|
108
|
+
className="udb-onb-select"
|
|
108
109
|
value={typeof value === 'string' ? value : ''}
|
|
109
110
|
onChange={(e) => onChange(e.target.value)}
|
|
110
111
|
>
|
|
@@ -118,7 +119,7 @@ function PreferenceControl({ item, value, onChange, fieldId }: PreferenceControl
|
|
|
118
119
|
case 'multi-select': {
|
|
119
120
|
const list = Array.isArray(value) ? value : [];
|
|
120
121
|
return (
|
|
121
|
-
<div className="
|
|
122
|
+
<div className="udb-onb-chips" role="group" aria-label={item.label}>
|
|
122
123
|
{item.options?.map((opt) => {
|
|
123
124
|
const isOn = list.includes(opt.id);
|
|
124
125
|
return (
|
|
@@ -127,11 +128,7 @@ function PreferenceControl({ item, value, onChange, fieldId }: PreferenceControl
|
|
|
127
128
|
type="button"
|
|
128
129
|
aria-pressed={isOn}
|
|
129
130
|
onClick={() => onChange(toggleMulti(list, opt.id))}
|
|
130
|
-
className={
|
|
131
|
-
isOn
|
|
132
|
-
? 'rounded-full bg-[rgb(var(--uob-primary-500))] px-3 py-1 text-sm text-white'
|
|
133
|
-
: 'rounded-full border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] px-3 py-1 text-sm hover:border-[rgb(var(--uob-primary-500))]'
|
|
134
|
-
}
|
|
131
|
+
className={cn('udb-onb-chip', isOn && 'udb-onb-chip--on')}
|
|
135
132
|
>
|
|
136
133
|
{opt.label}
|
|
137
134
|
</button>
|
|
@@ -148,26 +145,16 @@ function PreferenceControl({ item, value, onChange, fieldId }: PreferenceControl
|
|
|
148
145
|
role="switch"
|
|
149
146
|
aria-checked={value === true}
|
|
150
147
|
onClick={() => onChange(!value)}
|
|
151
|
-
className={
|
|
152
|
-
value === true
|
|
153
|
-
? 'h-6 w-11 rounded-full bg-[rgb(var(--uob-primary-500))] p-0.5'
|
|
154
|
-
: 'h-6 w-11 rounded-full bg-[rgb(var(--uob-border))] p-0.5'
|
|
155
|
-
}
|
|
148
|
+
className={cn('udb-onb-switch', value === true && 'udb-onb-switch--on')}
|
|
156
149
|
>
|
|
157
|
-
<span
|
|
158
|
-
className={
|
|
159
|
-
value === true
|
|
160
|
-
? 'block h-5 w-5 translate-x-5 rounded-full bg-white transition-transform'
|
|
161
|
-
: 'block h-5 w-5 translate-x-0 rounded-full bg-white transition-transform'
|
|
162
|
-
}
|
|
163
|
-
/>
|
|
150
|
+
<span className="udb-onb-switch__thumb" />
|
|
164
151
|
</button>
|
|
165
152
|
);
|
|
166
153
|
case 'slider': {
|
|
167
154
|
const { min, max } = resolveSliderBounds(item.min, item.max);
|
|
168
155
|
const numeric = typeof value === 'number' ? value : min;
|
|
169
156
|
return (
|
|
170
|
-
<div className="
|
|
157
|
+
<div className="udb-onb-slider">
|
|
171
158
|
<input
|
|
172
159
|
type="range"
|
|
173
160
|
id={fieldId}
|
|
@@ -178,9 +165,8 @@ function PreferenceControl({ item, value, onChange, fieldId }: PreferenceControl
|
|
|
178
165
|
aria-valuemin={min}
|
|
179
166
|
aria-valuemax={max}
|
|
180
167
|
aria-valuenow={numeric}
|
|
181
|
-
className="w-full"
|
|
182
168
|
/>
|
|
183
|
-
<div className="
|
|
169
|
+
<div className="udb-onb-slider__scale">
|
|
184
170
|
<span>{min}</span>
|
|
185
171
|
<span>{numeric}</span>
|
|
186
172
|
<span>{max}</span>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
3
|
import { Check } from 'lucide-react';
|
|
4
|
-
import { cn } from '
|
|
4
|
+
import { cn } from '@umituz/web-design-system/utils/cn';
|
|
5
5
|
import { splitTotalDuration } from '../../calc/duration-equal-split';
|
|
6
6
|
import type { ProcessingScreenConfig } from '../../types/screens';
|
|
7
7
|
|
|
@@ -24,13 +24,9 @@ export function ProcessingScreen({ config, onComplete }: ProcessingScreenProps)
|
|
|
24
24
|
}, [currentStep, config.steps.length, stepDuration, onComplete]);
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<div className="
|
|
28
|
-
{config.title &&
|
|
29
|
-
|
|
30
|
-
{config.title}
|
|
31
|
-
</h2>
|
|
32
|
-
)}
|
|
33
|
-
<ul className="flex w-full max-w-md flex-col gap-3" aria-live="polite">
|
|
27
|
+
<div className="udb-onb-stage">
|
|
28
|
+
{config.title && <h2 className="udb-onb-title">{config.title}</h2>}
|
|
29
|
+
<ul className="udb-onb-steps" aria-live="polite">
|
|
34
30
|
{config.steps.map((step, i) => {
|
|
35
31
|
const isDone = i < currentStep;
|
|
36
32
|
const isActive = i === currentStep;
|
|
@@ -38,34 +34,22 @@ export function ProcessingScreen({ config, onComplete }: ProcessingScreenProps)
|
|
|
38
34
|
<li
|
|
39
35
|
key={`${step}-${i}`}
|
|
40
36
|
className={cn(
|
|
41
|
-
'
|
|
42
|
-
isDone
|
|
43
|
-
|
|
44
|
-
: isActive
|
|
45
|
-
? 'border-[rgb(var(--uob-primary-500))] bg-[rgb(var(--uob-primary-50))]'
|
|
46
|
-
: 'border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))]',
|
|
37
|
+
'udb-onb-step',
|
|
38
|
+
isDone && 'udb-onb-step--done',
|
|
39
|
+
isActive && 'udb-onb-step--active',
|
|
47
40
|
)}
|
|
48
41
|
>
|
|
49
|
-
<span
|
|
50
|
-
className={
|
|
51
|
-
'flex h-6 w-6 shrink-0 items-center justify-center rounded-full',
|
|
52
|
-
isDone
|
|
53
|
-
? 'bg-[rgb(var(--uob-success))] text-white'
|
|
54
|
-
: isActive
|
|
55
|
-
? 'bg-[rgb(var(--uob-primary-500))] text-white'
|
|
56
|
-
: 'bg-[rgb(var(--uob-border))] text-[rgb(var(--uob-text-muted))]',
|
|
57
|
-
)}
|
|
58
|
-
>
|
|
59
|
-
{isDone ? <Check className="h-4 w-4" /> : <span className="text-xs">{i + 1}</span>}
|
|
42
|
+
<span className="udb-onb-step__badge">
|
|
43
|
+
{isDone ? <Check className="udb-onb-icon--sm" /> : <span>{i + 1}</span>}
|
|
60
44
|
</span>
|
|
61
|
-
<span
|
|
45
|
+
<span>{step}</span>
|
|
62
46
|
<AnimatePresence>
|
|
63
47
|
{isActive && (
|
|
64
48
|
<motion.span
|
|
65
49
|
initial={{ opacity: 0 }}
|
|
66
50
|
animate={{ opacity: 1 }}
|
|
67
51
|
exit={{ opacity: 0 }}
|
|
68
|
-
className="
|
|
52
|
+
className="udb-onb-step__pulse"
|
|
69
53
|
aria-hidden="true"
|
|
70
54
|
/>
|
|
71
55
|
)}
|