@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,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 '../shared/PrimaryButton';
5
- import { cn } from '../../theme/cn';
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="flex flex-1 flex-col gap-8">
26
- {config.title && (
27
- <h2 className="text-2xl font-semibold text-[rgb(var(--uob-text-primary))] sm:text-3xl">
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="flex max-w-xl flex-col items-center gap-4 rounded-2xl border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] p-8 text-center"
36
+ className="udb-onb-quote"
40
37
  >
41
38
  {current.rating !== undefined && (
42
- <div className="flex items-center gap-1" aria-label={`Rated ${current.rating} out of 5`}>
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="text-lg text-[rgb(var(--uob-text-primary))]">“{current.content}”</p>
57
- <footer className="flex flex-col items-center gap-1">
58
- {current.avatar && (
59
- <img
60
- src={current.avatar}
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="flex items-center justify-between gap-4">
74
- <div className="flex gap-1.5" role="tablist" aria-label="Testimonials">
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="flex gap-2">
71
+ <div className="udb-onb-proof__nav">
93
72
  {total > 1 && (
94
73
  <>
95
- <button
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
- </button>
102
- <button
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
- </button>
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 '../shared/SwipeCard';
4
- import { PrimaryButton } from '../shared/PrimaryButton';
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
- // Refs make decision handlers always see the latest state without recreating callbacks
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="flex flex-1 flex-col items-center justify-center gap-6">
50
- <p className="text-lg text-[rgb(var(--uob-text-primary))]">{labels.allDone}</p>
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="flex flex-1 flex-col gap-6">
60
- {config.title && (
61
- <h2 className="text-2xl font-semibold text-[rgb(var(--uob-text-primary))] sm:text-3xl">
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="w-full"
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="flex items-center justify-between gap-2">
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 '../shared/PrimaryButton';
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
- // navigator.share rejects with AbortError when the user dismisses the sheet —
48
- // that is an expected outcome, not a failure worth logging.
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="flex flex-1 flex-col items-center justify-center gap-6 text-center">
58
- <div
59
- className="flex h-20 w-20 items-center justify-center rounded-2xl bg-[rgb(var(--uob-primary-50))] text-[rgb(var(--uob-primary-500))]"
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="text-3xl font-bold text-[rgb(var(--uob-text-primary))]">{config.title}</h2>
65
- <div className="w-full max-w-md rounded-2xl border border-[rgb(var(--uob-border))] bg-[rgb(var(--uob-surface))] p-6 text-left">
66
- <pre className="overflow-x-auto whitespace-pre-wrap text-sm text-[rgb(var(--uob-text-primary))]">
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="flex flex-col items-center gap-1 text-sm text-[rgb(var(--uob-text-muted))]">
72
- <button
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
- </button>
62
+ </Button>
79
63
  <span>{config.shareConfig.text}</span>
80
64
  </div>
81
65
  )}
@@ -1,5 +1,5 @@
1
- import { PrimaryButton } from '../shared/PrimaryButton';
2
- import { renderBundledIcon } from '../shared/renderBundledIcon';
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="flex flex-1 flex-col items-center justify-center gap-8 text-center">
16
+ <div className="udb-onb-stage udb-onb-stage--wide">
17
17
  {doesIconNameResolve(config.heroIconName) && (
18
- <div
19
- className="flex h-24 w-24 items-center justify-center rounded-3xl bg-[rgb(var(--uob-primary-50))] text-[rgb(var(--uob-primary-500))]"
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="flex flex-col gap-3">
26
- {config.title && (
27
- <h1 className="text-4xl font-bold text-[rgb(var(--uob-text-primary))] sm:text-5xl">
28
- {config.title}
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.cta && (
41
- <PrimaryButton onClick={config.onCta ?? onNext} loading={isLoading}>
42
- {config.cta}
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 '../../types/config';
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 '../../types/config';
6
- import { trackOnboardingEvent } from '../../utils/analytics-dispatch';
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
- /** Alias of `trackEvent` kept for call-site readability on funnel events. */
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 '../../utils/default-storage-key';
5
- import type { OnboardingScreen } from '../../types/screens';
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 '../../types/screens';
3
- import type { OnboardingResult } from '../../types/config';
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 '../../calc/step-index-clamp';
8
+ } from '../calc/step-index-clamp';
9
9
  import {
10
10
  computeProgressFraction,
11
11
  computeRemainingSteps,
12
- } from '../../calc/progress-percentage';
12
+ } from '../calc/progress-percentage';
13
13
  import {
14
14
  buildOnboardingResult,
15
15
  clearOnboardingProgress,
16
16
  loadOnboardingProgress,
17
17
  saveOnboardingProgress,
18
- } from '../../utils/local-storage-bridge';
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
- /** Disable persistence (e.g. tests, ephemeral previews). */
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; // 0..1
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
- /** When the current run started (restored from storage on rehydration). */
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
- // useReducer reads `initialState` only on mount, so this is effectively mount-only.
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
- // Persist whenever the state changes.
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 '../../types/screens';
2
+ import type { PermissionStatus, PermissionType } from '../types/screens';
3
3
  import {
4
4
  isPermissionDecided,
5
5
  isPermissionUnsupported,
6
- } from '../../predicates/permission-granted';
7
- import { queryPermissionStatus, requestPermission } from '../../utils/permission-bridge';
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
- // Shared / atomic
42
- export {
43
- ProgressBar,
44
- QuestionWrapper,
45
- OptionCard,
46
- SwipeCard,
47
- AnimationWrapper,
48
- ScreenContainer,
49
- PrimaryButton,
50
- OnboardingErrorBoundary,
51
- } from './components/shared';
52
- export type { ScreenTransition } from './components/shared';
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 './components/hooks';
50
+ } from './hooks';
61
51
  export type {
62
52
  UseOnboardingProgressReturn,
63
53
  UsePermissionPrimingReturn,
64
54
  UseOnboardingAnalyticsReturn,
65
- } from './components/hooks';
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,