@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.
Files changed (135) hide show
  1. package/README.md +47 -73
  2. package/package.json +5 -9
  3. package/src/config/DashboardConfig.ts +19 -68
  4. package/src/domains/analytics/components/index.ts +0 -6
  5. package/src/domains/analytics/hooks/index.ts +0 -6
  6. package/src/domains/analytics/hooks/useAnalytics.ts +2 -20
  7. package/src/domains/analytics/index.ts +0 -10
  8. package/src/domains/analytics/services/AnalyticsEngineService.ts +7 -74
  9. package/src/domains/analytics/services/PerformanceService.ts +27 -138
  10. package/src/domains/analytics/services/index.ts +0 -4
  11. package/src/domains/analytics/types/analytics.ts +100 -270
  12. package/src/domains/analytics/types/index.ts +0 -6
  13. package/src/domains/analytics/utils/analytics.test.ts +2 -9
  14. package/src/domains/analytics/utils/analytics.ts +1 -22
  15. package/src/domains/analytics/utils/index.ts +0 -6
  16. package/src/domains/calendar/config.ts +0 -7
  17. package/src/domains/calendar/hooks/index.ts +0 -4
  18. package/src/domains/calendar/hooks/useCalendar.ts +7 -27
  19. package/src/domains/calendar/index.ts +0 -4
  20. package/src/domains/calendar/services/CalendarService.ts +0 -21
  21. package/src/domains/calendar/services/index.ts +0 -4
  22. package/src/domains/calendar/types/calendar.types.ts +9 -53
  23. package/src/domains/calendar/utils/contentItemMapper.test.ts +0 -4
  24. package/src/domains/calendar/utils/contentItemMapper.ts +0 -31
  25. package/src/domains/calendar/utils/index.ts +0 -26
  26. package/src/domains/layouts/components/index.ts +0 -4
  27. package/src/domains/layouts/hooks/dashboard.ts +0 -21
  28. package/src/domains/layouts/hooks/index.ts +0 -4
  29. package/src/domains/layouts/index.ts +0 -6
  30. package/src/domains/layouts/theme/default.ts +0 -12
  31. package/src/domains/layouts/theme/index.ts +0 -4
  32. package/src/domains/layouts/theme/presets.ts +0 -9
  33. package/src/domains/layouts/theme/utils.ts +0 -15
  34. package/src/domains/layouts/types/index.ts +0 -4
  35. package/src/domains/layouts/types/layout.ts +18 -56
  36. package/src/domains/layouts/types/notification.ts +4 -17
  37. package/src/domains/layouts/types/sidebar.ts +8 -28
  38. package/src/domains/layouts/types/theme.ts +23 -59
  39. package/src/domains/layouts/types/user.ts +9 -30
  40. package/src/domains/layouts/utils/dashboard.ts +4 -48
  41. package/src/domains/layouts/utils/index.ts +0 -4
  42. package/src/domains/modules/build.tsx +2 -16
  43. package/src/domains/modules/types.ts +8 -23
  44. package/src/domains/onboarding/calc/calc.test.ts +1 -9
  45. package/src/domains/onboarding/calc/card-stack-motion-curve.ts +0 -6
  46. package/src/domains/onboarding/calc/count-with-locale.ts +0 -8
  47. package/src/domains/onboarding/calc/duration-equal-split.ts +0 -12
  48. package/src/domains/onboarding/calc/elapsed-millis.ts +0 -6
  49. package/src/domains/onboarding/calc/progress-percentage.ts +1 -11
  50. package/src/domains/onboarding/calc/slider-value-bounds.ts +1 -11
  51. package/src/domains/onboarding/calc/step-index-clamp.ts +0 -9
  52. package/src/domains/onboarding/calc/swipe-gesture-curve.ts +0 -13
  53. package/src/domains/onboarding/components/{shared/AnimationWrapper.tsx → AnimationWrapper.tsx} +0 -4
  54. package/src/domains/onboarding/components/{shared/OnboardingErrorBoundary.tsx → OnboardingErrorBoundary.tsx} +7 -27
  55. package/src/domains/onboarding/components/OnboardingFlow.test.tsx +8 -30
  56. package/src/domains/onboarding/components/OnboardingFlow.tsx +13 -61
  57. package/src/domains/onboarding/components/OnboardingIntl.tsx +1 -11
  58. package/src/domains/onboarding/components/PrimaryButton.tsx +40 -0
  59. package/src/domains/onboarding/components/ProgressBar.tsx +32 -0
  60. package/src/domains/onboarding/components/QuestionWrapper.tsx +28 -0
  61. package/src/domains/onboarding/components/ScreenContainer.tsx +19 -0
  62. package/src/domains/onboarding/components/{shared/SwipeCard.test.tsx → SwipeCard.test.tsx} +1 -3
  63. package/src/domains/onboarding/components/{shared/SwipeCard.tsx → SwipeCard.tsx} +10 -28
  64. package/src/domains/onboarding/components/renderBundledIcon.tsx +7 -0
  65. package/src/domains/onboarding/components/screens/AccountGateScreen.tsx +7 -17
  66. package/src/domains/onboarding/components/screens/AppDemoScreen.tsx +4 -10
  67. package/src/domains/onboarding/components/screens/ComparisonScreen.tsx +12 -22
  68. package/src/domains/onboarding/components/screens/GoalQuestionScreen.tsx +9 -6
  69. package/src/domains/onboarding/components/screens/PainPointsScreen.tsx +10 -10
  70. package/src/domains/onboarding/components/screens/PaywallScreen.tsx +22 -38
  71. package/src/domains/onboarding/components/screens/PermissionPrimingScreen.tsx +14 -29
  72. package/src/domains/onboarding/components/screens/PersonalizedSolutionScreen.tsx +10 -23
  73. package/src/domains/onboarding/components/screens/PreferencesScreen.tsx +13 -27
  74. package/src/domains/onboarding/components/screens/ProcessingScreen.tsx +11 -27
  75. package/src/domains/onboarding/components/screens/SocialProofScreen.tsx +22 -51
  76. package/src/domains/onboarding/components/screens/TinderCardsScreen.tsx +13 -30
  77. package/src/domains/onboarding/components/screens/ValueDeliveryScreen.tsx +13 -29
  78. package/src/domains/onboarding/components/screens/WelcomeScreen.tsx +12 -32
  79. package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingAnalytics.test.tsx +1 -1
  80. package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingAnalytics.ts +3 -16
  81. package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingProgress.test.tsx +2 -2
  82. package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingProgress.ts +10 -21
  83. package/src/domains/onboarding/{components/hooks → hooks}/usePermissionPriming.ts +3 -10
  84. package/src/domains/onboarding/{components/hooks → hooks}/useReducedMotion.ts +0 -5
  85. package/src/domains/onboarding/index.ts +16 -45
  86. package/src/domains/onboarding/predicates/has-handler.ts +0 -4
  87. package/src/domains/onboarding/predicates/icon-name-resolves.ts +1 -17
  88. package/src/domains/onboarding/predicates/permission-granted.ts +0 -5
  89. package/src/domains/onboarding/predicates/permission-supports.ts +1 -11
  90. package/src/domains/onboarding/predicates/predicates.test.ts +6 -20
  91. package/src/domains/onboarding/predicates/screen-emits-data.ts +0 -4
  92. package/src/domains/onboarding/predicates/storage-available.ts +0 -8
  93. package/src/domains/onboarding/templates/TemplateDemoFallback.tsx +19 -39
  94. package/src/domains/onboarding/templates/art-onboarding.ts +1 -11
  95. package/src/domains/onboarding/templates/health-onboarding.ts +0 -5
  96. package/src/domains/onboarding/templates/platform-onboarding.ts +0 -9
  97. package/src/domains/onboarding/templates/video-onboarding.ts +0 -5
  98. package/src/domains/onboarding/test/setup.ts +1 -8
  99. package/src/domains/onboarding/types/config.ts +7 -18
  100. package/src/domains/onboarding/types/labels.ts +25 -59
  101. package/src/domains/onboarding/types/screens.ts +3 -11
  102. package/src/domains/onboarding/utils/analytics-dispatch.ts +2 -21
  103. package/src/domains/onboarding/utils/default-storage-key.ts +0 -1
  104. package/src/domains/onboarding/utils/index.ts +0 -1
  105. package/src/domains/onboarding/utils/local-storage-bridge.ts +1 -21
  106. package/src/domains/onboarding/utils/permission-bridge.ts +0 -8
  107. package/src/domains/onboarding/validation/flow-structure-check.ts +0 -6
  108. package/src/domains/onboarding/validation/permission-status-check.ts +0 -5
  109. package/src/domains/onboarding/validation/screen-config-check.ts +0 -5
  110. package/src/domains/onboarding/validation/stored-progress-check.ts +1 -10
  111. package/src/domains/onboarding/validation/validation.test.ts +1 -6
  112. package/src/domains/permissions/hasPermission.ts +0 -8
  113. package/src/domains/permissions/types.ts +0 -20
  114. package/src/domains/settings/components/index.ts +0 -4
  115. package/src/domains/settings/hooks/index.ts +0 -4
  116. package/src/domains/settings/hooks/useSettings.ts +1 -26
  117. package/src/domains/settings/index.ts +0 -6
  118. package/src/domains/settings/types/index.ts +0 -4
  119. package/src/domains/settings/types/settings.ts +23 -67
  120. package/src/domains/settings/utils/index.ts +0 -4
  121. package/src/domains/settings/utils/settings.ts +0 -45
  122. package/src/styles/dashboard.css +818 -2
  123. package/src/domains/onboarding/components/shared/OptionCard.tsx +0 -73
  124. package/src/domains/onboarding/components/shared/PrimaryButton.tsx +0 -58
  125. package/src/domains/onboarding/components/shared/ProgressBar.tsx +0 -51
  126. package/src/domains/onboarding/components/shared/QuestionWrapper.tsx +0 -36
  127. package/src/domains/onboarding/components/shared/ScreenContainer.tsx +0 -29
  128. package/src/domains/onboarding/components/shared/index.ts +0 -16
  129. package/src/domains/onboarding/components/shared/renderBundledIcon.tsx +0 -20
  130. package/src/domains/onboarding/onboarding.css +0 -1
  131. package/src/domains/onboarding/theme/cn.ts +0 -10
  132. package/src/domains/onboarding/theme/index.css +0 -131
  133. package/src/domains/onboarding/theme/index.ts +0 -12
  134. package/src/domains/onboarding/theme/tokens.ts +0 -122
  135. /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 '../shared/PrimaryButton';
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="flex flex-1 flex-col gap-6">
20
- {config.title && (
21
- <h2 className="text-2xl font-semibold text-[rgb(var(--uob-text-primary))] sm:text-3xl">
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 '../shared/PrimaryButton';
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="flex flex-1 flex-col gap-6">
16
- {config.title && (
17
- <h2 className="text-2xl font-semibold text-[rgb(var(--uob-text-primary))] sm:text-3xl">
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={cn('flex flex-col gap-3 rounded-2xl border p-5', colorClass)}>
48
- <h3 className="text-lg font-semibold text-[rgb(var(--uob-text-primary))]">{heading}</h3>
49
- <ul className="flex flex-col gap-2">
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="flex items-start gap-2 text-sm text-[rgb(var(--uob-text-primary))]">
52
- <Icon className={cn('mt-0.5 h-4 w-4 shrink-0', iconColor)} aria-hidden="true" />
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 '../shared/OptionCard';
3
- import { PrimaryButton } from '../shared/PrimaryButton';
4
- import { QuestionWrapper } from '../shared/QuestionWrapper';
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="flex flex-col gap-2"
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
- option={option}
45
+ label={option.label}
46
+ description={option.description}
47
+ icon={renderBundledIcon(option.iconName)}
45
48
  selected={selected.includes(option.id)}
46
- onToggle={toggle}
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 '../shared/OptionCard';
3
- import { PrimaryButton } from '../shared/PrimaryButton';
4
- import { QuestionWrapper } from '../shared/QuestionWrapper';
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="flex flex-col gap-2"
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
- option={point}
51
+ label={point.label}
52
+ description={point.description}
53
+ icon={renderBundledIcon(point.iconName)}
54
54
  selected={selected.includes(point.id)}
55
- onToggle={toggle}
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 '../shared/PrimaryButton';
5
- import { renderBundledIcon } from '../shared/renderBundledIcon';
6
- import { useOnboardingAnalytics } from '../hooks/useOnboardingAnalytics';
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="flex flex-1 flex-col gap-6">
59
- <div className="flex flex-col gap-2 text-center">
60
- <h2 className="text-3xl font-bold text-[rgb(var(--uob-text-primary))] sm:text-4xl">
61
- {config.title}
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="rounded-2xl border-2 border-[rgb(var(--uob-primary-500))] bg-[rgb(var(--uob-primary-50))] p-6 text-center"
60
+ className="udb-onb-price"
70
61
  >
71
- <div className="text-3xl font-bold text-[rgb(var(--uob-primary-700))]">{config.price}</div>
72
- <div className="mt-1 text-sm text-[rgb(var(--uob-text-muted))]">
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="flex flex-col gap-2">
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="mt-0.5 text-[rgb(var(--uob-primary-500))]">
93
- {renderBundledIcon(feature.iconName, 'h-5 w-5')}
79
+ <span className="udb-onb-feature__icon">
80
+ {renderBundledIcon(feature.iconName, 'udb-onb-icon')}
94
81
  </span>
95
82
  )}
96
- <span className="text-sm text-[rgb(var(--uob-text-primary))]">{feature.text}</span>
83
+ <span>{feature.text}</span>
97
84
  </li>
98
85
  ))}
99
86
  </ul>
100
87
  {config.socialProof && (
101
- <p className="text-center text-sm text-[rgb(var(--uob-text-muted))]">
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="flex flex-col gap-2">
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="text-sm font-semibold text-[rgb(var(--uob-text-muted))] underline-offset-4 hover:underline disabled:opacity-50"
110
+ className="udb-onb-link"
127
111
  >
128
112
  {config.secondaryCta}
129
113
  </button>
130
114
  )}
131
115
  </div>
132
- <p className="flex items-center justify-center gap-1 text-xs text-[rgb(var(--uob-text-muted))]">
133
- <ShieldCheck className="h-3.5 w-3.5" />
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 '../shared/PrimaryButton';
3
- import { renderBundledIcon } from '../shared/renderBundledIcon';
4
- import { usePermissionPriming } from '../hooks/usePermissionPriming';
5
- import { useOnboardingAnalytics } from '../hooks/useOnboardingAnalytics';
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="flex flex-1 flex-col gap-6">
70
+ <div className="udb-onb-proof">
75
71
  {doesIconNameResolve(iconName) && (
76
- <div
77
- className="flex h-20 w-20 items-center justify-center rounded-2xl bg-[rgb(var(--uob-primary-50))] text-[rgb(var(--uob-primary-500))]"
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="flex flex-col gap-2">
84
- <h2 className="text-2xl font-semibold text-[rgb(var(--uob-text-primary))] sm:text-3xl">
85
- {config.title}
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="flex flex-col gap-2">
80
+ <ul className="udb-onb-list">
90
81
  {config.benefits.map((benefit) => (
91
- <li
92
- key={benefit}
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 '../shared/PrimaryButton';
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="flex flex-1 flex-col gap-6">
18
- {config.title && (
19
- <h2 className="text-2xl font-semibold text-[rgb(var(--uob-text-primary))] sm:text-3xl">
20
- {config.title}
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="flex items-start gap-3 rounded-xl border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] p-4"
27
+ className="udb-onb-solution"
34
28
  >
35
- <CheckCircle2
36
- className="mt-0.5 h-5 w-5 shrink-0 text-[rgb(var(--uob-success))]"
37
- aria-hidden="true"
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 '../shared/PrimaryButton';
3
- import { QuestionWrapper } from '../shared/QuestionWrapper';
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="flex flex-col gap-5">
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="flex flex-col gap-2">
89
- <label className="text-sm font-semibold text-[rgb(var(--uob-text-primary))]" htmlFor={fieldId}>
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="rounded-md border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] p-2 text-sm"
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="flex flex-wrap gap-2" role="group" aria-label={item.label}>
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="flex flex-col gap-1">
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="flex justify-between text-xs text-[rgb(var(--uob-text-muted))]">
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 '../../theme/cn';
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="flex flex-1 flex-col items-center justify-center gap-8 text-center">
28
- {config.title && (
29
- <h2 className="text-2xl font-semibold text-[rgb(var(--uob-text-primary))] sm:text-3xl">
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
- 'flex items-center gap-3 rounded-lg border p-3 text-left text-sm',
42
- isDone
43
- ? 'border-[rgb(var(--uob-success))] bg-[rgb(var(--uob-success))]/10'
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={cn(
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 className="text-[rgb(var(--uob-text-primary))]">{step}</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="ml-auto h-2 w-2 animate-pulse rounded-full bg-[rgb(var(--uob-primary-500))]"
52
+ className="udb-onb-step__pulse"
69
53
  aria-hidden="true"
70
54
  />
71
55
  )}