@umituz/web-dashboard 3.2.2 → 4.0.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 (125) hide show
  1. package/README.md +202 -14
  2. package/package.json +21 -29
  3. package/src/domains/analytics/components/AnalyticsCard.tsx +23 -21
  4. package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
  5. package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
  6. package/src/domains/analytics/components/MetricCard.tsx +42 -64
  7. package/src/domains/analytics/hooks/index.ts +7 -1
  8. package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
  9. package/src/domains/analytics/index.ts +11 -0
  10. package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
  11. package/src/domains/analytics/services/PerformanceService.ts +117 -69
  12. package/src/domains/analytics/types/analytics.ts +20 -6
  13. package/src/domains/analytics/types/index.ts +3 -0
  14. package/src/domains/analytics/utils/analytics.test.ts +183 -0
  15. package/src/domains/analytics/utils/analytics.ts +26 -9
  16. package/src/domains/analytics/utils/i18nKeys.ts +40 -0
  17. package/src/domains/analytics/utils/index.ts +2 -0
  18. package/src/domains/auth/components/AuthLayout.tsx +63 -54
  19. package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
  20. package/src/domains/auth/components/LoginForm.tsx +167 -226
  21. package/src/domains/auth/components/RegisterForm.tsx +202 -272
  22. package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
  23. package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
  24. package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
  25. package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
  26. package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
  27. package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
  28. package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
  29. package/src/domains/auth/components/shared/index.ts +20 -0
  30. package/src/domains/auth/hooks/index.ts +1 -0
  31. package/src/domains/auth/hooks/useAuth.ts +151 -232
  32. package/src/domains/auth/index.ts +8 -4
  33. package/src/domains/auth/types/auth.ts +26 -5
  34. package/src/domains/auth/types/index.ts +1 -0
  35. package/src/domains/auth/utils/auth.test.ts +182 -0
  36. package/src/domains/auth/utils/auth.ts +71 -130
  37. package/src/domains/auth/utils/i18nKeys.ts +127 -0
  38. package/src/domains/auth/utils/index.ts +7 -1
  39. package/src/domains/auth/utils/secureToken.ts +33 -0
  40. package/src/domains/auth/utils/validationKeys.ts +23 -0
  41. package/src/domains/calendar/hooks/useCalendar.ts +145 -151
  42. package/src/domains/calendar/index.ts +0 -1
  43. package/src/domains/calendar/services/CalendarService.ts +122 -179
  44. package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
  45. package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
  46. package/src/domains/calendar/utils/index.ts +33 -30
  47. package/src/domains/layouts/components/BrandLogo.tsx +12 -9
  48. package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
  49. package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
  50. package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
  51. package/src/domains/layouts/types/layout.ts +7 -1
  52. package/src/domains/layouts/types/user.ts +3 -1
  53. package/src/domains/modules/build.test.tsx +97 -0
  54. package/src/domains/modules/build.tsx +71 -0
  55. package/src/domains/modules/index.ts +2 -0
  56. package/src/domains/modules/types.ts +29 -0
  57. package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
  58. package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
  59. package/src/domains/onboarding/components/PlanStep.tsx +156 -123
  60. package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
  61. package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
  62. package/src/domains/onboarding/components/StepProgress.tsx +62 -0
  63. package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
  64. package/src/domains/onboarding/components/index.ts +4 -0
  65. package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
  66. package/src/domains/onboarding/index.ts +1 -0
  67. package/src/domains/onboarding/types/onboarding.ts +57 -17
  68. package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
  69. package/src/domains/onboarding/utils/index.ts +2 -0
  70. package/src/domains/permissions/PermissionsContext.tsx +103 -0
  71. package/src/domains/permissions/hasPermission.test.ts +50 -0
  72. package/src/domains/permissions/hasPermission.ts +38 -0
  73. package/src/domains/permissions/index.ts +19 -0
  74. package/src/domains/permissions/types.ts +35 -0
  75. package/src/domains/permissions/utils/i18nKeys.ts +11 -0
  76. package/src/domains/session/DemoLogin.tsx +50 -0
  77. package/src/domains/session/OrgSwitcher.tsx +110 -0
  78. package/src/domains/session/SessionContext.tsx +150 -0
  79. package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
  80. package/src/domains/session/index.ts +18 -0
  81. package/src/domains/session/types.ts +47 -0
  82. package/src/domains/session/utils/i18nKeys.ts +12 -0
  83. package/src/domains/settings/components/AuditLogList.tsx +59 -0
  84. package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
  85. package/src/domains/settings/components/SettingsLayout.tsx +75 -60
  86. package/src/domains/settings/components/SettingsSection.tsx +35 -22
  87. package/src/domains/settings/components/UserManagementTable.tsx +87 -0
  88. package/src/domains/settings/components/index.ts +10 -0
  89. package/src/domains/settings/hooks/useSettings.ts +77 -52
  90. package/src/domains/settings/types/settings.ts +2 -0
  91. package/src/domains/settings/utils/i18nKeys.ts +18 -0
  92. package/src/domains/settings/utils/settings.ts +11 -4
  93. package/src/i18n/I18nContext.tsx +33 -0
  94. package/src/i18n/defaultT.test.ts +43 -0
  95. package/src/i18n/defaultT.ts +34 -0
  96. package/src/i18n/index.ts +7 -0
  97. package/src/i18n/types.ts +13 -0
  98. package/src/index.ts +1 -7
  99. package/src/primitives/Button.tsx +30 -0
  100. package/src/primitives/Checkbox.tsx +40 -0
  101. package/src/primitives/Input.tsx +19 -0
  102. package/src/primitives/Skeleton.tsx +26 -0
  103. package/src/primitives/cn.ts +15 -0
  104. package/src/primitives/index.ts +23 -0
  105. package/src/styles/dashboard.css +3045 -0
  106. package/src/styles/tokens.contract.test.ts +66 -0
  107. package/src/styles/tokens.css +124 -0
  108. package/src/domains/billing/components/BillingLayout.tsx +0 -63
  109. package/src/domains/billing/components/BillingPage.tsx +0 -49
  110. package/src/domains/billing/components/BillingPortal.tsx +0 -198
  111. package/src/domains/billing/components/InvoiceCard.tsx +0 -143
  112. package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
  113. package/src/domains/billing/components/PlanComparison.tsx +0 -199
  114. package/src/domains/billing/components/UsageCard.tsx +0 -103
  115. package/src/domains/billing/components/index.ts +0 -13
  116. package/src/domains/billing/constants/billing.ts +0 -69
  117. package/src/domains/billing/hooks/index.ts +0 -7
  118. package/src/domains/billing/hooks/useBilling.ts +0 -385
  119. package/src/domains/billing/index.ts +0 -73
  120. package/src/domains/billing/types/billing.ts +0 -342
  121. package/src/domains/billing/types/index.ts +0 -30
  122. package/src/domains/billing/utils/billing.ts +0 -360
  123. package/src/domains/billing/utils/index.ts +0 -31
  124. package/src/domains/billing/utils/polar.ts +0 -63
  125. package/src/domains/calendar/components/index.ts +0 -7
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Onboarding - Step Progress Indicator
3
+ *
4
+ * Renders the wizard's progress as a row of numbered nodes connected
5
+ * by lines. Stateless: receives all data via props.
6
+ */
7
+
8
+ import { Check } from "lucide-react";
9
+ import { cn } from "../../../primitives";
10
+
11
+ interface StepProgressProps {
12
+ currentStep: number;
13
+ totalSteps: number;
14
+ completedSteps?: number[];
15
+ }
16
+
17
+ export const StepProgress = ({
18
+ currentStep,
19
+ totalSteps,
20
+ completedSteps = [],
21
+ }: StepProgressProps) => {
22
+ return (
23
+ <div
24
+ className="udb-progress"
25
+ role="progressbar"
26
+ aria-valuemin={1}
27
+ aria-valuemax={totalSteps}
28
+ aria-valuenow={currentStep}
29
+ >
30
+ {Array.from({ length: totalSteps }, (_, i) => i + 1).map((step) => {
31
+ const isCompleted = completedSteps.includes(step) || step < currentStep;
32
+ const isCurrent = step === currentStep;
33
+
34
+ return (
35
+ <div key={step} className="udb-progress__segment">
36
+ <div
37
+ className={cn(
38
+ "udb-progress__node",
39
+ (isCompleted || isCurrent) && "udb-progress__node--on",
40
+ )}
41
+ aria-current={isCurrent ? "step" : undefined}
42
+ >
43
+ {isCompleted ? (
44
+ <Check className="udb-progress__check" aria-hidden="true" />
45
+ ) : (
46
+ step
47
+ )}
48
+ </div>
49
+ {step < totalSteps && (
50
+ <div
51
+ className={cn(
52
+ "udb-progress__connector",
53
+ step < currentStep && "udb-progress__connector--on",
54
+ )}
55
+ />
56
+ )}
57
+ </div>
58
+ );
59
+ })}
60
+ </div>
61
+ );
62
+ };
@@ -1,128 +1,107 @@
1
+ /**
2
+ * User Type Step
3
+ *
4
+ * First step of the onboarding wizard: select user type.
5
+ * String props are expected to be already-translated by the consumer
6
+ * (so the consumer can localize each option via i18n).
7
+ */
8
+
1
9
  import { Check } from "lucide-react";
2
- import { cn } from "@umituz/web-design-system/utils";
3
- import type { OnboardingState } from "../types/onboarding";
10
+ import { useT } from "../../../i18n";
11
+ import { cn } from "../../../primitives";
12
+ import type { OnboardingState, UserTypeOption } from "../types/onboarding";
4
13
  import type { ComponentType } from "react";
14
+ import { ONBOARDING_KEYS } from "../utils/i18nKeys";
5
15
 
6
- interface UserTypeOption {
7
- id: string;
8
- label: string;
9
- description: string;
10
- icon?: ComponentType<{ className?: string }>;
11
- badge?: string;
12
- }
13
-
14
- interface UserTypeStepProps {
16
+ export interface UserTypeStepProps {
15
17
  /** Current onboarding state */
16
18
  state: OnboardingState;
17
19
  /** Update state function */
18
20
  updateState: (updates: Partial<OnboardingState>) => void;
19
- /** User type options */
21
+ /** User type options — labels/desc are translated by the consumer */
20
22
  options?: UserTypeOption[];
23
+ /** Custom icon component type (avoids `React.ComponentType` import here) */
24
+ iconComponent?: ComponentType<{ className?: string }>;
21
25
  }
22
26
 
23
27
  /**
24
- * User Type Selection Step
25
- *
26
- * First step of onboarding - select user type/category
28
+ * Default options — every string is an i18n key. The consumer
29
+ * supplies translations; nothing in this file is hardcoded copy.
27
30
  */
31
+ const DEFAULT_OPTIONS: UserTypeOption[] = [
32
+ { id: "founder", labelKey: ONBOARDING_KEYS.defaults.founder, descriptionKey: ONBOARDING_KEYS.defaults.founderDescription },
33
+ { id: "creator", labelKey: ONBOARDING_KEYS.defaults.contentCreator, descriptionKey: ONBOARDING_KEYS.defaults.contentCreatorDescription },
34
+ { id: "agency", labelKey: ONBOARDING_KEYS.defaults.agency, descriptionKey: ONBOARDING_KEYS.defaults.agencyDescription },
35
+ { id: "enterprise", labelKey: ONBOARDING_KEYS.defaults.enterprise, descriptionKey: ONBOARDING_KEYS.defaults.enterpriseDescription },
36
+ { id: "small-business", labelKey: ONBOARDING_KEYS.defaults.smallBusiness, descriptionKey: ONBOARDING_KEYS.defaults.smallBusinessDescription },
37
+ { id: "personal", labelKey: ONBOARDING_KEYS.defaults.personal, descriptionKey: ONBOARDING_KEYS.defaults.personalDescription },
38
+ ];
39
+
28
40
  export const UserTypeStep = ({
29
41
  state,
30
42
  updateState,
31
43
  options = [],
32
44
  }: UserTypeStepProps) => {
33
- // Default options if not provided
34
- const defaultOptions: UserTypeOption[] = [
35
- {
36
- id: "founder",
37
- label: "Founder",
38
- description: "Building your own startup or business",
39
- },
40
- {
41
- id: "creator",
42
- label: "Content Creator",
43
- description: "Creating content for social media and platforms",
44
- },
45
- {
46
- id: "agency",
47
- label: "Agency",
48
- description: "Managing multiple client accounts",
49
- },
50
- {
51
- id: "enterprise",
52
- label: "Enterprise",
53
- description: "Large scale organization with teams",
54
- },
55
- {
56
- id: "small-business",
57
- label: "Small Business",
58
- description: "Local or niche business owner",
59
- },
60
- {
61
- id: "personal",
62
- label: "Personal",
63
- description: "Individual looking to grow personal brand",
64
- },
65
- ];
66
-
67
- const userTypeOptions = options.length > 0 ? options : defaultOptions;
45
+ const t = useT();
46
+ const userTypeOptions = options.length > 0 ? options : DEFAULT_OPTIONS;
68
47
 
69
48
  return (
70
- <div className="w-full max-w-xl">
71
- <div className="text-center mb-10">
72
- <h1 className="text-3xl md:text-4xl font-extrabold text-foreground mb-3">
73
- What describes you best?
49
+ <div className="udb-step">
50
+ <div className="udb-step__header">
51
+ <h1 className="udb-step__title">
52
+ {t(ONBOARDING_KEYS.userType.title)}
74
53
  </h1>
75
- <p className="text-muted-foreground">
76
- Select the option that best describes your situation
54
+ <p className="udb-step__description">
55
+ {t(ONBOARDING_KEYS.userType.description)}
77
56
  </p>
78
57
  </div>
79
58
 
80
- <div className="grid grid-cols-1 gap-3">
59
+ <div className="udb-step__options udb-step__options--list" role="radiogroup" aria-label={t(ONBOARDING_KEYS.userType.title)}>
81
60
  {userTypeOptions.map((option) => {
82
61
  const isSelected = state.selectedUserType === option.id;
83
62
  const hasBadge = option.badge != null;
84
63
  const hasIcon = option.icon != null;
64
+ const label = option.label ?? (option.labelKey ? t(option.labelKey) : '');
65
+ const description = option.description ?? (option.descriptionKey ? t(option.descriptionKey) : '');
85
66
 
86
67
  return (
87
68
  <button
88
69
  key={option.id}
70
+ type="button"
71
+ role="radio"
72
+ aria-checked={isSelected}
89
73
  onClick={() => updateState({ selectedUserType: option.id })}
90
74
  className={cn(
91
- "w-full flex items-center gap-4 p-5 rounded-2xl border bg-background text-left transition-all group",
92
- isSelected
93
- ? "border-primary ring-2 ring-primary/20 bg-primary/5"
94
- : "border-border hover:border-primary/40 hover:bg-muted/50"
75
+ "udb-choice",
76
+ isSelected && "udb-choice--selected",
95
77
  )}
96
78
  >
97
- <div
79
+ <span
98
80
  className={cn(
99
- "w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",
100
- isSelected
101
- ? "border-primary bg-primary text-white"
102
- : "border-muted-foreground/30"
81
+ "udb-choice__check",
82
+ isSelected && "udb-choice__check--on",
103
83
  )}
84
+ aria-hidden="true"
104
85
  >
105
- {isSelected && <Check className="h-4 w-4" />}
106
- </div>
86
+ {isSelected && <Check className="udb-choice__check-icon" />}
87
+ </span>
107
88
 
108
- <div className="flex-1">
109
- <div className="flex items-center gap-2">
110
- <p className="font-bold text-foreground">{option.label}</p>
89
+ <span className="udb-choice__body">
90
+ <span className="udb-choice__label-row">
91
+ <span className="udb-choice__label">{label}</span>
111
92
  {hasBadge && (
112
- <span className="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full bg-primary/10 text-primary">
93
+ <span className="udb-choice__badge">
113
94
  {option.badge}
114
95
  </span>
115
96
  )}
116
- </div>
117
- <p className="text-sm text-muted-foreground mt-0.5">
118
- {option.description}
119
- </p>
120
- </div>
97
+ </span>
98
+ <span className="udb-choice__description">{description}</span>
99
+ </span>
121
100
 
122
101
  {hasIcon && option.icon && (
123
- <div className="w-10 h-10 rounded-xl bg-muted flex items-center justify-center shrink-0">
124
- <option.icon className="h-5 w-5 text-muted-foreground" />
125
- </div>
102
+ <span className="udb-choice__icon-box">
103
+ <option.icon className="udb-choice__icon" aria-hidden="true" />
104
+ </span>
126
105
  )}
127
106
  </button>
128
107
  );
@@ -7,3 +7,7 @@ export { UserTypeStep } from './UserTypeStep';
7
7
  export { AppFocusStep } from './AppFocusStep';
8
8
  export { PlatformsStep } from './PlatformsStep';
9
9
  export { PlanStep } from './PlanStep';
10
+ export { StepProgress } from './StepProgress';
11
+ export { StepNavigation } from './StepNavigation';
12
+ export { STEP_NAVIGATION_KEYS } from './StepNavigation';
13
+ export type { StepNavigationKey, Translate } from './StepNavigation';
@@ -1,102 +1,108 @@
1
1
  /**
2
2
  * Onboarding Hooks
3
3
  *
4
- * Custom React hooks for onboarding functionality
4
+ * Custom React hooks for onboarding functionality. Navigation callbacks
5
+ * read the current render's state so their boolean return values are
6
+ * truthful; state updaters themselves stay pure.
5
7
  */
6
8
 
7
- import { useState, useCallback } from "react";
9
+ import { useState, useCallback, useMemo, useRef } from "react";
8
10
  import type { OnboardingState, OnboardingConfig } from "../types/onboarding";
9
11
 
12
+ /**
13
+ * Initial onboarding state — single source of truth.
14
+ */
15
+ const buildInitialState = (
16
+ initialState?: Partial<OnboardingState>,
17
+ ): OnboardingState => ({
18
+ currentStep: 1,
19
+ connectedPlatforms: [],
20
+ billingCycle: "monthly",
21
+ stepData: {},
22
+ ...initialState,
23
+ });
24
+
10
25
  /**
11
26
  * Use Onboarding Hook
12
27
  *
13
- * Manages onboarding state and actions
28
+ * Manages onboarding state and actions.
14
29
  *
15
30
  * @param config - Onboarding configuration
16
- * @param initialState - Initial state
31
+ * @param initialState - Initial state overrides
17
32
  * @returns Onboarding state and actions
18
33
  */
19
34
  export function useOnboarding(
20
35
  config: OnboardingConfig,
21
- initialState?: Partial<OnboardingState>
36
+ initialState?: Partial<OnboardingState>,
22
37
  ) {
23
- const [state, setState] = useState<OnboardingState>({
24
- currentStep: 1,
25
- connectedPlatforms: [],
26
- billingCycle: "monthly",
27
- stepData: {},
28
- ...initialState,
29
- });
38
+ const [state, setState] = useState<OnboardingState>(() => buildInitialState(initialState));
30
39
 
31
40
  const totalSteps = config.steps.length;
32
41
 
33
- // Navigation actions
34
- const goToNext = useCallback(() => {
42
+ // Validation and the boolean return value are computed against the
43
+ // current render's state (always fresh — these callbacks are
44
+ // recreated when state changes); the setState updater stays pure and
45
+ // bounds-checked. The previous version flipped a closure variable
46
+ // inside the updater, which returned false before React had run it.
47
+ const goToNext = useCallback((): boolean => {
35
48
  const currentStepConfig = config.steps[state.currentStep - 1];
36
-
37
- // Validate if needed
38
- if (currentStepConfig?.validate) {
39
- const isValid = currentStepConfig.validate(state);
40
- if (!isValid) return false;
49
+ if (currentStepConfig?.validate && !currentStepConfig.validate(state)) {
50
+ return false;
41
51
  }
42
-
43
- if (state.currentStep < totalSteps) {
44
- setState((prev) => ({ ...prev, currentStep: prev.currentStep + 1 }));
45
- return true;
46
- }
47
-
48
- return false;
52
+ if (state.currentStep >= totalSteps) return false;
53
+ setState((prev) =>
54
+ prev.currentStep < totalSteps
55
+ ? { ...prev, currentStep: prev.currentStep + 1 }
56
+ : prev,
57
+ );
58
+ return true;
49
59
  }, [config.steps, state, totalSteps]);
50
60
 
51
- const goToPrev = useCallback(() => {
52
- if (state.currentStep > 1) {
53
- setState((prev) => ({ ...prev, currentStep: prev.currentStep - 1 }));
54
- return true;
55
- }
56
- return false;
57
- }, [state.currentStep]);
61
+ const goToPrev = useCallback((): boolean => {
62
+ if (state.currentStep <= 1) return false;
63
+ setState((prev) =>
64
+ prev.currentStep > 1
65
+ ? { ...prev, currentStep: prev.currentStep - 1 }
66
+ : prev,
67
+ );
68
+ return true;
69
+ }, [state]);
58
70
 
59
71
  const goToStep = useCallback((step: number) => {
60
- if (step >= 1 && step <= totalSteps) {
61
- setState((prev) => ({ ...prev, currentStep: step }));
62
- }
72
+ setState((prev) => {
73
+ if (step >= 1 && step <= totalSteps && step !== prev.currentStep) {
74
+ return { ...prev, currentStep: step };
75
+ }
76
+ return prev;
77
+ });
63
78
  }, [totalSteps]);
64
79
 
65
- // State update action
66
80
  const updateState = useCallback((updates: Partial<OnboardingState>) => {
67
81
  setState((prev) => ({ ...prev, ...updates }));
68
82
  }, []);
69
83
 
70
- // Validation helper
71
- const canGoNext = useCallback(() => {
84
+ // Whether the current step passes its configured validator.
85
+ const canGoNext = useCallback((): boolean => {
72
86
  const currentStepConfig = config.steps[state.currentStep - 1];
73
- if (currentStepConfig?.validate) {
74
- return currentStepConfig.validate(state);
75
- }
76
- return true;
87
+ return currentStepConfig?.validate ? currentStepConfig.validate(state) : true;
77
88
  }, [config.steps, state]);
78
89
 
79
- // Progress calculation
80
- const getProgress = useCallback(() => {
81
- return (state.currentStep / totalSteps) * 100;
90
+ // The `isFirstStep` / `isLastStep` derivations are cheap, but
91
+ // memoizing them avoids re-creating the object on every render.
92
+ const derived = useMemo(() => {
93
+ const isFirstStep = state.currentStep === 1;
94
+ const isLastStep = state.currentStep === totalSteps;
95
+ const progress = totalSteps === 0 ? 0 : (state.currentStep / totalSteps) * 100;
96
+ return { isFirstStep, isLastStep, progress };
82
97
  }, [state.currentStep, totalSteps]);
83
98
 
84
- // Is first step
85
- const isFirstStep = state.currentStep === 1;
86
-
87
- // Is last step
88
- const isLastStep = state.currentStep === totalSteps;
89
-
90
99
  return {
91
- // State
92
100
  state,
93
101
  currentStep: state.currentStep,
94
102
  totalSteps,
95
- isFirstStep,
96
- isLastStep,
97
- progress: getProgress(),
98
-
99
- // Actions
103
+ isFirstStep: derived.isFirstStep,
104
+ isLastStep: derived.isLastStep,
105
+ progress: derived.progress,
100
106
  goToNext,
101
107
  goToPrev,
102
108
  goToStep,
@@ -108,34 +114,38 @@ export function useOnboarding(
108
114
  /**
109
115
  * Use Onboarding Step Hook
110
116
  *
111
- * Hook for managing individual step state
112
- *
113
- * @param stepId - Step identifier
114
- * @returns Step state and actions
117
+ * Hook for managing individual step state.
115
118
  */
116
119
  export function useOnboardingStep(stepId: string) {
117
120
  const [data, setData] = useState<Record<string, unknown>>({});
118
121
  const [isValid, setIsValid] = useState(false);
119
122
  const [isTouched, setIsTouched] = useState(false);
120
123
 
124
+ // Latest-ref mirror of `data` so `validate` can read the most recent
125
+ // value synchronously. State updaters must stay pure — the previous
126
+ // version called setIsValid inside the setData updater.
127
+ const dataRef = useRef<Record<string, unknown>>({});
128
+
121
129
  const updateData = useCallback((updates: Record<string, unknown>) => {
122
- setData((prev) => ({ ...prev, ...updates }));
130
+ const next = { ...dataRef.current, ...updates };
131
+ dataRef.current = next;
132
+ setData(next);
123
133
  setIsTouched(true);
124
134
  }, []);
125
135
 
126
136
  const validate = useCallback((validator: (data: Record<string, unknown>) => boolean) => {
127
- const valid = validator(data);
128
- setIsValid(valid);
129
- return valid;
137
+ setIsValid(validator(dataRef.current));
130
138
  }, []);
131
139
 
132
140
  const reset = useCallback(() => {
141
+ dataRef.current = {};
133
142
  setData({});
134
143
  setIsValid(false);
135
144
  setIsTouched(false);
136
145
  }, []);
137
146
 
138
147
  return {
148
+ stepId,
139
149
  data,
140
150
  isValid,
141
151
  isTouched,
@@ -20,6 +20,7 @@ export {
20
20
  generateOnboardingEvent,
21
21
  isValidEmail,
22
22
  isValidPassword,
23
+ ONBOARDING_KEYS,
23
24
  } from './utils';
24
25
  export type {
25
26
  UserTypeOption,
@@ -5,52 +5,88 @@
5
5
  */
6
6
 
7
7
  import type { LucideIcon } from "lucide-react";
8
- import type { ComponentType, ReactElement, ReactNode } from "react";
8
+ import type { ComponentType, ReactElement } from "react";
9
9
 
10
10
  /**
11
- * User type option for first step
11
+ * Onboarding error — the message is an i18n key.
12
+ */
13
+ export interface OnboardingError {
14
+ /** Step where the error occurred */
15
+ step: number;
16
+ /** i18n key for the localized error message */
17
+ message: string;
18
+ }
19
+
20
+ /**
21
+ * User type option for first step.
22
+ * Either supply `label`/`description` directly (already translated) or
23
+ * supply `labelKey`/`descriptionKey` to be resolved by the consumer's i18n.
12
24
  */
13
25
  export interface UserTypeOption {
14
26
  /** Unique identifier */
15
27
  id: string;
16
- /** Display label */
17
- label: string;
18
- /** Description text */
19
- description: string;
20
- /** Icon component */
28
+ /** Display label (already-translated string) */
29
+ label?: string;
30
+ /** Description text (already-translated) */
31
+ description?: string;
32
+ /** i18n key for the label */
33
+ labelKey?: string;
34
+ /** i18n key for the description */
35
+ descriptionKey?: string;
36
+ /** Icon component (Lucide) */
21
37
  icon?: LucideIcon;
22
38
  /** Badge text */
23
39
  badge?: string;
24
40
  }
25
41
 
26
42
  /**
27
- * Platform connection option
43
+ * Platform connection option.
44
+ * Icons must be Lucide components — emoji is not allowed.
28
45
  */
29
46
  export interface PlatformOption {
30
47
  /** Unique identifier */
31
48
  id: string;
32
49
  /** Display name */
33
50
  name: string;
34
- /** Icon/emoji */
35
- icon: string;
36
- /** Color theme */
51
+ /** Lucide icon component */
52
+ icon: LucideIcon;
53
+ /** Brand color as a plain CSS color string (e.g. "#c026d3") */
37
54
  color?: string;
38
55
  /** Connection status */
39
56
  connected?: boolean;
40
57
  }
41
58
 
59
+ /**
60
+ * App type option for the AppFocusStep.
61
+ * Same shape as PlatformOption (icon as component, not emoji).
62
+ */
63
+ export interface AppTypeOption {
64
+ id: string;
65
+ name: string;
66
+ description: string;
67
+ icon: LucideIcon;
68
+ /**
69
+ * State field toggled by this option. When omitted, the field is
70
+ * derived from the id ("mobile" → hasMobileApp, "web" → hasWebApp);
71
+ * options that resolve to neither render inert.
72
+ */
73
+ stateField?: "hasMobileApp" | "hasWebApp";
74
+ }
75
+
42
76
  /**
43
77
  * Plan option for billing step
44
78
  */
45
79
  export interface PlanOption {
46
80
  /** Unique identifier */
47
81
  id: string;
48
- /** Plan name */
82
+ /** Plan name (already translated) */
49
83
  name: string;
50
- /** Badge text */
84
+ /** Badge text (already translated) — preferred for static labels */
51
85
  badge?: string;
52
- /** Badge color class */
53
- badgeColor?: string;
86
+ /** i18n key for the badge — resolved via `translate` in the consumer */
87
+ badgeKey?: string;
88
+ /** Semantic badge tone rendered by the stylesheet */
89
+ badgeTone?: "muted" | "primary" | "accent";
54
90
  /** Description text */
55
91
  description: string;
56
92
  /** Monthly price */
@@ -115,6 +151,8 @@ export interface OnboardingConfig {
115
151
  cancelRoute?: string;
116
152
  /** Allow skipping steps */
117
153
  allowSkip?: boolean;
154
+ /** Allow canceling the wizard from the header */
155
+ allowCancel?: boolean;
118
156
  /** Show progress indicator */
119
157
  showProgress?: boolean;
120
158
  /** Enable user menu in header */
@@ -199,9 +237,9 @@ export interface StepNavigationProps {
199
237
  canGoNext: boolean;
200
238
  /** Is saving/completing */
201
239
  isSaving?: boolean;
202
- /** Next button label */
240
+ /** Next button label (overrides the i18n key) */
203
241
  nextLabel?: string;
204
- /** Previous button label */
242
+ /** Previous button label (overrides the i18n key) */
205
243
  prevLabel?: string;
206
244
  /** On next */
207
245
  onNext: () => void;
@@ -211,4 +249,6 @@ export interface StepNavigationProps {
211
249
  allowSkip?: boolean;
212
250
  /** On skip */
213
251
  onSkip?: () => void;
252
+ /** Translation function */
253
+ translate: (key: string) => string;
214
254
  }