@umituz/web-dashboard 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 -1
  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,37 +1,16 @@
1
- /**
2
- * Dashboard Types - User
3
- *
4
- * Type definitions for user profile
5
- */
6
-
7
1
  import type { ComponentType } from "react";
8
2
 
9
- /**
10
- * User profile info for header
11
- */
12
3
  export interface DashboardUser {
13
- /** User ID */
14
- id: string;
15
- /** Display name */
16
- name?: string;
17
- /** Email address */
18
- email?: string;
19
- /** Avatar URL */
20
- avatar?: string;
21
- /** Whether user has mobile app access */
22
- hasMobileApp?: boolean;
23
- /** Whether user has web app access */
24
- hasWebApp?: boolean;
4
+ id: string;
5
+ name?: string;
6
+ email?: string;
7
+ avatar?: string;
8
+ hasMobileApp?: boolean;
9
+ hasWebApp?: boolean;
25
10
  }
26
11
 
27
- /**
28
- * Navigation item for user menu
29
- */
30
12
  export interface UserNavMenuItem {
31
- /** Display label */
32
- label: string;
33
- /** Icon component */
34
- icon: ComponentType<{ className?: string }>;
35
- /** Route path */
36
- path: string;
13
+ label: string;
14
+ icon: ComponentType<{ className?: string }>;
15
+ path: string;
37
16
  }
@@ -1,16 +1,3 @@
1
- /**
2
- * Dashboard Utilities
3
- *
4
- * Utility functions for dashboard operations
5
- */
6
-
7
- /**
8
- * Format notification timestamp to relative time
9
- *
10
- * @param createdAt - Notification creation timestamp
11
- * @param t - i18n translation function
12
- * @returns Formatted time string
13
- */
14
1
  export function formatNotificationTime(
15
2
  createdAt: Date | string | number,
16
3
  t: (key: string, params?: Record<string, unknown>) => string
@@ -24,61 +11,35 @@ export function formatNotificationTime(
24
11
  return t("dashboard.activityFeed.times.daysAgo", { val: Math.floor(secs / 86400) });
25
12
  }
26
13
 
27
- /**
28
- * Check if a path is active
29
- *
30
- * @param currentPath - Current location pathname
31
- * @param targetPath - Target path to check
32
- * @returns True if paths match
33
- */
34
14
  export function isPathActive(currentPath: string, targetPath: string): boolean {
35
15
  return currentPath === targetPath;
36
16
  }
37
17
 
38
- /**
39
- * Get page title from sidebar configuration
40
- *
41
- * @param pathname - Current pathname
42
- * @param sidebarGroups - Sidebar groups configuration
43
- * @param extraTitleMap - Extra title mappings
44
- * @returns Page title or null
45
- */
46
18
  export function getPageTitle(
47
19
  pathname: string,
48
20
  sidebarGroups: Array<{ items: Array<{ path: string; label: string }> }>,
49
21
  extraTitleMap?: Record<string, string>
50
22
  ): string | null {
51
- // Search in sidebar groups
52
- for (const group of sidebarGroups) {
23
+ for (const group of sidebarGroups) {
53
24
  const item = group.items.find((i) => i.path === pathname);
54
25
  if (item) return item.label;
55
26
  }
56
27
 
57
- // Search in extra title map
58
- if (extraTitleMap?.[pathname]) {
28
+ if (extraTitleMap?.[pathname]) {
59
29
  return extraTitleMap[pathname];
60
30
  }
61
31
 
62
32
  return null;
63
33
  }
64
34
 
65
- /**
66
- * Filter sidebar items by app type and enabled status
67
- *
68
- * @param items - Sidebar items to filter
69
- * @param user - Current user object
70
- * @returns Filtered items
71
- */
72
35
  export function filterSidebarItems<T extends { enabled?: boolean; requiredApp?: "mobile" | "web" }>(
73
36
  items: T[],
74
37
  user?: { hasMobileApp?: boolean; hasWebApp?: boolean }
75
38
  ): T[] {
76
39
  return items.filter((item) => {
77
- // Skip disabled items
78
- if (item.enabled === false) return false;
40
+ if (item.enabled === false) return false;
79
41
 
80
- // Skip items that require specific app types
81
- if (!item.requiredApp) return true;
42
+ if (!item.requiredApp) return true;
82
43
  if (item.requiredApp === "mobile") return user?.hasMobileApp ?? false;
83
44
  if (item.requiredApp === "web") return user?.hasWebApp ?? false;
84
45
 
@@ -86,11 +47,6 @@ export function filterSidebarItems<T extends { enabled?: boolean; requiredApp?:
86
47
  });
87
48
  }
88
49
 
89
- /**
90
- * Generate notification ID
91
- *
92
- * @returns Unique notification ID
93
- */
94
50
  export function generateNotificationId(): string {
95
51
  return crypto.randomUUID();
96
52
  }
@@ -1,7 +1,3 @@
1
- /**
2
- * Layout Utils Export
3
- */
4
-
5
1
  export {
6
2
  formatNotificationTime,
7
3
  isPathActive,
@@ -1,14 +1,3 @@
1
- /**
2
- * buildRoutes / buildNav — derive route configs and sidebar groups
3
- * from a single DashboardModule list.
4
- *
5
- * - buildRoutes wraps every permissioned module's element in
6
- * <RequirePermission>, so hosts only map the result to <Route>.
7
- * - buildNav filters by the same permissions via `can`, hides
8
- * inNav:false detail routes, groups by navGroup (first-appearance
9
- * order) and sorts items by `order` (stable for ties).
10
- */
11
-
12
1
  import type { ReactNode } from "react";
13
2
  import type { SidebarGroup } from "../layouts/types";
14
3
  import { RequirePermission } from "../permissions";
@@ -33,8 +22,7 @@ export const buildRoutes = (modules: readonly DashboardModule[]): RouteConfig[]
33
22
  }));
34
23
 
35
24
  export interface BuildNavOptions {
36
- /** Permission check, e.g. usePermissions().can. */
37
- can: (permission: Permission) => boolean;
25
+ can: (permission: Permission) => boolean;
38
26
  }
39
27
 
40
28
  export const buildNav = (
@@ -45,9 +33,7 @@ export const buildNav = (
45
33
  .filter((module) => module.inNav !== false)
46
34
  .filter((module) => !module.permission || can(module.permission));
47
35
 
48
- // Groups keep their first-appearance order in the module list;
49
- // `order` sorts items within their group.
50
- const grouped = new Map<string, DashboardModule[]>();
36
+ const grouped = new Map<string, DashboardModule[]>();
51
37
  for (const module of visible) {
52
38
  const items = grouped.get(module.navGroup) ?? [];
53
39
  items.push(module);
@@ -1,29 +1,14 @@
1
- /**
2
- * Dashboard module model — the single source of truth a host app
3
- * registers once per module; routes and sidebar nav are both derived
4
- * from it (see build.ts). Generalizes the texora/visora pattern
5
- * where MODULE_ROUTES and NAV_GROUPS were hand-kept in sync.
6
- */
7
-
8
1
  import type { ReactNode } from "react";
9
2
  import type { LucideIcon } from "lucide-react";
10
3
  import type { Permission } from "../permissions/types";
11
4
 
12
5
  export interface DashboardModule {
13
- /** Route path (may contain params, e.g. "/sales/:id"). */
14
- path: string;
15
- /** i18n key (or plain label) for nav + page headers. */
16
- labelKey: string;
17
- /** lucide-react icon for the nav item. */
18
- icon: LucideIcon;
19
- /** Nav group title key — modules with the same key land in one group. */
20
- navGroup: string;
21
- /** Page content; permission-guarded automatically when `permission` is set. */
22
- element: ReactNode;
23
- /** Permission required to see/use this module (optional). */
24
- permission?: Permission;
25
- /** Sort weight inside its nav group (lower first, default last). */
26
- order?: number;
27
- /** Include in the nav (default true — set false for detail routes). */
28
- inNav?: boolean;
6
+ path: string;
7
+ labelKey: string;
8
+ icon: LucideIcon;
9
+ navGroup: string;
10
+ element: ReactNode;
11
+ permission?: Permission;
12
+ order?: number;
13
+ inNav?: boolean;
29
14
  }
@@ -32,7 +32,6 @@ import {
32
32
  getCardTransitionDurationMs,
33
33
  } from './card-stack-motion-curve';
34
34
 
35
- // TEST_SCENARIOS.md §22
36
35
  describe('progress-percentage (§22.1)', () => {
37
36
  it('computes percentages without NaN or Infinity', () => {
38
37
  expect(computeProgressPercentage(0, 5)).toBe(0);
@@ -56,7 +55,6 @@ describe('progress-percentage (§22.1)', () => {
56
55
  });
57
56
  });
58
57
 
59
- // TEST_SCENARIOS.md §22.2
60
58
  describe('step-index-clamp (§22.2)', () => {
61
59
  it('clamps arbitrary indices into range', () => {
62
60
  expect(clampStepIndex(0, 5)).toBe(0);
@@ -78,12 +76,10 @@ describe('step-index-clamp (§22.2)', () => {
78
76
  });
79
77
  });
80
78
 
81
- // TEST_SCENARIOS.md §22.3
82
79
  describe('swipe-gesture-curve (§22.3)', () => {
83
80
  it('registers right swipes only above the threshold', () => {
84
81
  expect(didCrossRightSwipeThreshold(50)).toBe(false);
85
- expect(didCrossRightSwipeThreshold(120)).toBe(false); // boundary is not "above"
86
- expect(didCrossRightSwipeThreshold(150)).toBe(true);
82
+ expect(didCrossRightSwipeThreshold(120)).toBe(false); expect(didCrossRightSwipeThreshold(150)).toBe(true);
87
83
  });
88
84
 
89
85
  it('registers left swipes only below the negative threshold', () => {
@@ -96,7 +92,6 @@ describe('swipe-gesture-curve (§22.3)', () => {
96
92
  });
97
93
  });
98
94
 
99
- // TEST_SCENARIOS.md §22.4
100
95
  describe('slider-value-bounds (§22.4)', () => {
101
96
  it('clamps values into bounds', () => {
102
97
  expect(clampValueToBounds(50, 0, 100, 'x')).toBe(50);
@@ -123,7 +118,6 @@ describe('slider-value-bounds (§22.4)', () => {
123
118
  });
124
119
  });
125
120
 
126
- // TEST_SCENARIOS.md §22.5
127
121
  describe('duration-equal-split (§22.5)', () => {
128
122
  it('splits a total duration evenly', () => {
129
123
  expect(splitTotalDuration(3000, 3)).toBe(1000);
@@ -140,7 +134,6 @@ describe('duration-equal-split (§22.5)', () => {
140
134
  });
141
135
  });
142
136
 
143
- // TEST_SCENARIOS.md §22.6
144
137
  describe('count-with-locale (§22.6)', () => {
145
138
  it('formats counts for human display', () => {
146
139
  expect(formatCountWithLocale(250000, 'en-US')).toBe('250,000');
@@ -156,7 +149,6 @@ describe('count-with-locale (§22.6)', () => {
156
149
  });
157
150
  });
158
151
 
159
- // TEST_SCENARIOS.md §22.7
160
152
  describe('elapsed-millis (§22.7)', () => {
161
153
  it('returns the positive difference', () => {
162
154
  const start = new Date('2026-01-01T10:00:00Z');
@@ -1,9 +1,3 @@
1
- /**
2
- * Card stack motion math.
3
- * Owns the scale/opacity transitions used by TinderCardsScreen and similar
4
- * stack-style screens. Tuning these in one place keeps the stack feel consistent.
5
- */
6
-
7
1
  const MOTION_ENTER_DURATION_MS = 250;
8
2
  const INITIAL_SCALE = 0.95;
9
3
  const STEADY_SCALE = 1;
@@ -1,11 +1,3 @@
1
- /**
2
- * Locale-aware count formatting.
3
- * Used wherever a large count (e.g. "Trusted by 250,000+") is shown to the user.
4
- * The consumer never has to call .toLocaleString() directly.
5
- */
6
-
7
- /** Format a number for human display. e.g. 250000 -> "250,000" (en) / "250.000" (de-DE).
8
- * An omitted locale uses the host default; an invalid locale falls back to en-US. */
9
1
  export function formatCountWithLocale(value: number, locale?: string): string {
10
2
  if (!Number.isFinite(value)) return '0';
11
3
  try {
@@ -1,24 +1,12 @@
1
- /**
2
- * Duration equal-split math.
3
- * Splits a total processing duration into equal per-step slices.
4
- * Falls back to a sensible default when the consumer doesn't provide a total.
5
- */
6
-
7
1
  const DEFAULT_STEP_DURATION_MS = 1500;
8
2
  const MIN_PARTS = 1;
9
3
 
10
- /**
11
- * Per-step duration when a total is given.
12
- * Returns 0 if the total is unset OR the parts list is empty —
13
- * never divides by zero.
14
- */
15
4
  export function splitTotalDuration(totalMs: number | undefined, parts: number): number {
16
5
  if (totalMs === undefined || totalMs <= 0) return DEFAULT_STEP_DURATION_MS;
17
6
  const safeParts = Math.max(MIN_PARTS, parts);
18
7
  return totalMs / safeParts;
19
8
  }
20
9
 
21
- /** The default per-step duration when the consumer omits the total. */
22
10
  export function getDefaultStepDurationMs(): number {
23
11
  return DEFAULT_STEP_DURATION_MS;
24
12
  }
@@ -1,11 +1,5 @@
1
- /**
2
- * Elapsed-millisecond math.
3
- * Single source of truth for "how long did this take?".
4
- */
5
-
6
1
  const ZERO_MS = 0;
7
2
 
8
- /** Milliseconds between two Dates. Never negative — clamps to 0. */
9
3
  export function elapsedMillis(start: Date, end: Date): number {
10
4
  const diff = end.getTime() - start.getTime();
11
5
  return diff > ZERO_MS ? diff : ZERO_MS;
@@ -1,15 +1,8 @@
1
- /**
2
- * Progress percentage calculations.
3
- * Owns the math for "how far are we through the flow?".
4
- * Zero hardcoded fallbacks — every input is explicit.
5
- */
6
-
7
1
  const ZERO_FRACTION = 0;
8
2
  const PERCENT_MULTIPLIER = 100;
9
3
 
10
4
  function defensiveDenominator(totalSteps: number): number {
11
- // Defensive: an empty flow must report 0%, never NaN or Infinity.
12
- return totalSteps > 0 ? totalSteps : 1;
5
+ return totalSteps > 0 ? totalSteps : 1;
13
6
  }
14
7
 
15
8
  function defensiveCurrentStep(currentStep: number, totalSteps: number): number {
@@ -18,19 +11,16 @@ function defensiveCurrentStep(currentStep: number, totalSteps: number): number {
18
11
  return currentStep;
19
12
  }
20
13
 
21
- /** Progress as a 0..1 fraction. `1` means "all steps visited". */
22
14
  export function computeProgressFraction(currentStep: number, totalSteps: number): number {
23
15
  if (totalSteps <= 0) return ZERO_FRACTION;
24
16
  const safeCurrent = defensiveCurrentStep(currentStep, totalSteps);
25
17
  return safeCurrent / defensiveDenominator(totalSteps);
26
18
  }
27
19
 
28
- /** Progress as a 0..100 percentage. Safe for the UI bar. */
29
20
  export function computeProgressPercentage(currentStep: number, totalSteps: number): number {
30
21
  return computeProgressFraction(currentStep, totalSteps) * PERCENT_MULTIPLIER;
31
22
  }
32
23
 
33
- /** Number of steps remaining (including the current one). */
34
24
  export function computeRemainingSteps(currentStep: number, totalSteps: number): number {
35
25
  if (totalSteps <= 0) return 0;
36
26
  return Math.max(0, totalSteps - defensiveCurrentStep(currentStep, totalSteps));
@@ -1,13 +1,6 @@
1
- /**
2
- * Slider value math.
3
- * Owns the bounds enforcement for preference sliders.
4
- * Misconfigured bounds (min > max) are surfaced via console — never silently masked.
5
- */
6
-
7
1
  const SLIDER_MIN_IF_UNSET = 0;
8
2
  const SLIDER_MAX_IF_UNSET = 100;
9
3
 
10
- /** Defensive bounds for a slider: if the config omits one, fall back to a sensible default. */
11
4
  export function resolveSliderBounds(
12
5
  min: number | undefined,
13
6
  max: number | undefined,
@@ -18,12 +11,10 @@ export function resolveSliderBounds(
18
11
  };
19
12
  }
20
13
 
21
- /** Were the configured bounds logically valid? */
22
14
  export function areSliderBoundsValid(min: number, max: number): boolean {
23
15
  return min <= max;
24
16
  }
25
17
 
26
- /** Clamp a value into the [min, max] range, surfacing misconfigured bounds to the console. */
27
18
  export function clampValueToBounds(
28
19
  value: number,
29
20
  min: number,
@@ -31,8 +22,7 @@ export function clampValueToBounds(
31
22
  preferenceId: string,
32
23
  ): number {
33
24
  if (!areSliderBoundsValid(min, max)) {
34
- // Surface the misconfiguration — never silently mask.
35
-
25
+
36
26
  console.error(
37
27
  `[web-onboarding] preference "${preferenceId}" has min > max (${min} > ${max}); returning value as-is.`,
38
28
  );
@@ -1,13 +1,6 @@
1
- /**
2
- * Step index math.
3
- * Owns the bounds for "where can the cursor be in the flow?".
4
- * Pure, no React, no side effects.
5
- */
6
-
7
1
  const MIN_INDEX = 0;
8
2
  const MIN_TOTAL_STEPS = 1;
9
3
 
10
- /** Clamp an arbitrary index into the valid [0, totalSteps-1] range. */
11
4
  export function clampStepIndex(index: number, totalSteps: number): number {
12
5
  if (totalSteps < MIN_TOTAL_STEPS) return MIN_INDEX;
13
6
  if (index < MIN_INDEX) return MIN_INDEX;
@@ -15,14 +8,12 @@ export function clampStepIndex(index: number, totalSteps: number): number {
15
8
  return index;
16
9
  }
17
10
 
18
- /** Move one step forward, stopping at the last step. */
19
11
  export function advanceStepIndex(currentIndex: number, totalSteps: number): number {
20
12
  if (totalSteps < MIN_TOTAL_STEPS) return MIN_INDEX;
21
13
  const lastIndex = totalSteps - MIN_TOTAL_STEPS;
22
14
  return Math.min(currentIndex + MIN_TOTAL_STEPS, lastIndex);
23
15
  }
24
16
 
25
- /** Move one step backward, stopping at the first step. */
26
17
  export function retreatStepIndex(currentIndex: number): number {
27
18
  return Math.max(MIN_INDEX, currentIndex - MIN_TOTAL_STEPS);
28
19
  }
@@ -1,12 +1,5 @@
1
- /**
2
- * Swipe gesture math.
3
- * Owns every numeric value used by SwipeCard's drag/rotation/opacity curves.
4
- * Centralizing them here means the gesture "feel" can be tuned in one place.
5
- */
6
-
7
1
  const SWIPE_THRESHOLD_PX = 120;
8
2
  const DRAG_RANGE_PX = 200;
9
- /** Drag distance (px) at which the decision badges reach half opacity. */
10
3
  const OPACITY_MIDPOINT_PX = 50;
11
4
  const FULL_OPACITY = 1;
12
5
  const HALF_OPACITY = 0.5;
@@ -14,10 +7,8 @@ const ZERO_OPACITY = 0;
14
7
  const MAX_ROTATION_DEG = 15;
15
8
  const ZERO_ROTATION_DEG = 0;
16
9
 
17
- /** Minimum horizontal distance the user must drag to commit a swipe decision. */
18
10
  export const SWIPE_DECISION_THRESHOLD_PX = SWIPE_THRESHOLD_PX;
19
11
 
20
- /** Card rotation expressed in degrees, mapped from -200..+200 px drag. */
21
12
  export const SWIPE_ROTATION_RANGE: [number, number, number] = [
22
13
  -DRAG_RANGE_PX,
23
14
  ZERO_ROTATION_DEG,
@@ -30,7 +21,6 @@ export const SWIPE_ROTATION_VALUES: [number, number, number] = [
30
21
  MAX_ROTATION_DEG,
31
22
  ];
32
23
 
33
- /** DISAGREE badge opacity curve, mapped from -200..0 px drag. */
34
24
  export const SWIPE_LEFT_OPACITY_INPUT: [number, number, number] = [
35
25
  -DRAG_RANGE_PX,
36
26
  -OPACITY_MIDPOINT_PX,
@@ -43,7 +33,6 @@ export const SWIPE_LEFT_OPACITY_VALUES: [number, number, number] = [
43
33
  ZERO_OPACITY,
44
34
  ];
45
35
 
46
- /** AGREE badge opacity curve, mapped from 0..+200 px drag. */
47
36
  export const SWIPE_RIGHT_OPACITY_INPUT: [number, number, number] = [
48
37
  0,
49
38
  OPACITY_MIDPOINT_PX,
@@ -56,12 +45,10 @@ export const SWIPE_RIGHT_OPACITY_VALUES: [number, number, number] = [
56
45
  FULL_OPACITY,
57
46
  ];
58
47
 
59
- /** Did the gesture cross the threshold to register as a right-swipe? */
60
48
  export function didCrossRightSwipeThreshold(offsetX: number): boolean {
61
49
  return offsetX > SWIPE_THRESHOLD_PX;
62
50
  }
63
51
 
64
- /** Did the gesture cross the threshold to register as a left-swipe? */
65
52
  export function didCrossLeftSwipeThreshold(offsetX: number): boolean {
66
53
  return offsetX < -SWIPE_THRESHOLD_PX;
67
54
  }
@@ -36,10 +36,6 @@ const variantsByTransition: Record<
36
36
  },
37
37
  };
38
38
 
39
- /**
40
- * Wraps children with a transition keyed on stepKey.
41
- * Respects `prefers-reduced-motion` by disabling transitions.
42
- */
43
39
  export function AnimationWrapper({
44
40
  stepKey,
45
41
  children,
@@ -1,28 +1,19 @@
1
1
  import { Component, Fragment, type ContextType, type ErrorInfo, type ReactNode } from 'react';
2
2
  import { AlertTriangle } from 'lucide-react';
3
3
  import { PrimaryButton } from './PrimaryButton';
4
- import { OnboardingIntlContext, type OnboardingIntl } from '../OnboardingIntl';
4
+ import { OnboardingIntlContext, type OnboardingIntl } from './OnboardingIntl';
5
5
 
6
6
  export interface OnboardingErrorBoundaryProps {
7
7
  children: ReactNode;
8
- /** Called when the boundary trips — useful for analytics. */
9
8
  onError?: (error: Error, info: ErrorInfo) => void;
10
- /** Render a custom error UI. */
11
9
  renderError?: (error: Error, retry: () => void) => ReactNode;
12
10
  }
13
11
 
14
12
  interface OnboardingErrorBoundaryState {
15
13
  error: Error | null;
16
- /** Bumped on retry so the children subtree fully remounts, not just re-renders. */
17
14
  attempt: number;
18
15
  }
19
16
 
20
- /**
21
- * Catches render-time errors inside the onboarding flow.
22
- * Critical path rule: a broken screen must never produce a blank white page.
23
- * Retry clears the error AND remounts the children (a fresh subtree gets a
24
- * fresh chance at rendering — deterministic throws aside).
25
- */
26
17
  export class OnboardingErrorBoundary extends Component<
27
18
  OnboardingErrorBoundaryProps,
28
19
  OnboardingErrorBoundaryState
@@ -38,8 +29,7 @@ export class OnboardingErrorBoundary extends Component<
38
29
 
39
30
  componentDidCatch(error: Error, info: ErrorInfo): void {
40
31
  this.props.onError?.(error, info);
41
-
42
- console.error('[web-onboarding] render error', error, info.componentStack);
32
+ console.error('[onboarding] render error', error, info.componentStack);
43
33
  }
44
34
 
45
35
  retry = (): void => {
@@ -58,22 +48,12 @@ export class OnboardingErrorBoundary extends Component<
58
48
 
59
49
  const { labels } = intl;
60
50
  return (
61
- <div
62
- role="alert"
63
- className="uob-flow mx-auto flex min-h-screen w-full max-w-md flex-col items-center justify-center gap-4 px-6 text-center"
64
- >
65
- <div
66
- className="flex h-16 w-16 items-center justify-center rounded-2xl bg-[rgb(var(--uob-error))]/10 text-[rgb(var(--uob-error))]"
67
- aria-hidden="true"
68
- >
69
- <AlertTriangle className="h-8 w-8" />
51
+ <div role="alert" className="udb-onb udb-onb-errorpage">
52
+ <div className="udb-onb-errorpage__icon" aria-hidden="true">
53
+ <AlertTriangle className="udb-onb-icon--lg" />
70
54
  </div>
71
- <h2 className="text-2xl font-semibold text-[rgb(var(--uob-text-primary))]">
72
- {labels.somethingWentWrong}
73
- </h2>
74
- <p className="text-sm text-[rgb(var(--uob-text-muted))]">
75
- {error.message || labels.unexpectedError}
76
- </p>
55
+ <h2 className="udb-onb-title">{labels.somethingWentWrong}</h2>
56
+ <p className="udb-onb-lead">{error.message || labels.unexpectedError}</p>
77
57
  <PrimaryButton onClick={this.retry}>{labels.tryAgain}</PrimaryButton>
78
58
  </div>
79
59
  );