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