@wealthx/shadcn 1.5.29 → 1.5.31

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 (39) hide show
  1. package/.turbo/turbo-build.log +98 -98
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-DWJHPNFL.mjs → chunk-4UT3RZ2D.mjs} +32 -16
  4. package/dist/{chunk-RNLIZRAK.mjs → chunk-C6R42PCL.mjs} +1 -1
  5. package/dist/{chunk-EVUY67CY.mjs → chunk-FTQ2AKZ2.mjs} +1 -1
  6. package/dist/{chunk-7T4TYUO3.mjs → chunk-H5ZD63NT.mjs} +31 -16
  7. package/dist/{chunk-SO4RB3XB.mjs → chunk-IEQX4UVP.mjs} +2 -2
  8. package/dist/chunk-IKVF4XE2.mjs +94 -0
  9. package/dist/{chunk-KPGARKFC.mjs → chunk-MS3GNXMB.mjs} +1 -1
  10. package/dist/{chunk-M32QNCD3.mjs → chunk-OSSS56CB.mjs} +1 -1
  11. package/dist/{chunk-36IN7YRM.mjs → chunk-SCGCGVDN.mjs} +1 -1
  12. package/dist/{chunk-KJQ3BVTB.mjs → chunk-X2NIDXFB.mjs} +1 -1
  13. package/dist/components/ui/backoffice-signup-steps.js +98 -48
  14. package/dist/components/ui/backoffice-signup-steps.mjs +4 -4
  15. package/dist/components/ui/bank-statement-generate-dialog.mjs +2 -2
  16. package/dist/components/ui/chat-widget.js +1 -1
  17. package/dist/components/ui/chat-widget.mjs +2 -2
  18. package/dist/components/ui/contact-alert-dialog/index.mjs +2 -2
  19. package/dist/components/ui/field.js +1 -1
  20. package/dist/components/ui/field.mjs +1 -1
  21. package/dist/components/ui/frontend-signup-steps.js +166 -90
  22. package/dist/components/ui/frontend-signup-steps.mjs +4 -5
  23. package/dist/components/ui/password-strength-tooltip.js +81 -107
  24. package/dist/components/ui/password-strength-tooltip.mjs +3 -2
  25. package/dist/components/ui/property-report-dialog.mjs +2 -2
  26. package/dist/components/ui/signup-form-primitives.js +117 -114
  27. package/dist/components/ui/signup-form-primitives.mjs +3 -4
  28. package/dist/components/ui/two-fa-setup-form.js +31 -16
  29. package/dist/components/ui/two-fa-setup-form.mjs +2 -2
  30. package/dist/index.js +133 -68
  31. package/dist/index.mjs +10 -10
  32. package/dist/styles.css +1 -1
  33. package/package.json +1 -1
  34. package/src/components/ui/field.tsx +12 -12
  35. package/src/components/ui/password-strength-tooltip.tsx +89 -47
  36. package/src/components/ui/signup-form-primitives.tsx +34 -16
  37. package/src/components/ui/two-fa-setup-form.tsx +41 -31
  38. package/src/styles/styles-css.ts +1 -1
  39. package/dist/chunk-WHIW6KOB.mjs +0 -57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wealthx/shadcn",
3
- "version": "1.5.29",
3
+ "version": "1.5.31",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -22,7 +22,7 @@ function FieldSet({ className, ...props }: FieldSetProps): ReactElement {
22
22
  className={cn(
23
23
  "flex flex-col gap-6",
24
24
  "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
25
- className,
25
+ className
26
26
  )}
27
27
  data-slot="field-set"
28
28
  {...props}
@@ -45,7 +45,7 @@ function FieldLegend({
45
45
  "mb-3",
46
46
  "data-[variant=legend]:text-label-large",
47
47
  "data-[variant=label]:text-label-medium",
48
- className,
48
+ className
49
49
  )}
50
50
  data-slot="field-legend"
51
51
  data-variant={variant}
@@ -61,7 +61,7 @@ function FieldGroup({ className, ...props }: FieldGroupProps): ReactElement {
61
61
  <div
62
62
  className={cn(
63
63
  "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
64
- className,
64
+ className
65
65
  )}
66
66
  data-slot="field-group"
67
67
  {...props}
@@ -90,7 +90,7 @@ const fieldVariants = cva(
90
90
  defaultVariants: {
91
91
  orientation: "vertical",
92
92
  },
93
- },
93
+ }
94
94
  );
95
95
 
96
96
  export type FieldProps = React.ComponentProps<"div"> &
@@ -122,7 +122,7 @@ function FieldContent({
122
122
  <div
123
123
  className={cn(
124
124
  "group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
125
- className,
125
+ className
126
126
  )}
127
127
  data-slot="field-content"
128
128
  {...props}
@@ -139,7 +139,7 @@ function FieldLabel({ className, ...props }: FieldLabelProps): ReactElement {
139
139
  "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
140
140
  "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
141
141
  "has-data-checked:border-primary has-data-checked:bg-primary/5 dark:has-data-checked:bg-primary/10",
142
- className,
142
+ className
143
143
  )}
144
144
  data-slot="field-label"
145
145
  {...props}
@@ -154,7 +154,7 @@ function FieldTitle({ className, ...props }: FieldTitleProps): ReactElement {
154
154
  <div
155
155
  className={cn(
156
156
  "flex w-fit items-center gap-2 text-label-medium leading-snug group-data-[disabled=true]/field:opacity-50",
157
- className,
157
+ className
158
158
  )}
159
159
  data-slot="field-label"
160
160
  {...props}
@@ -174,7 +174,7 @@ function FieldDescription({
174
174
  "text-caption leading-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
175
175
  "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
176
176
  "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
177
- className,
177
+ className
178
178
  )}
179
179
  data-slot="field-description"
180
180
  {...props}
@@ -195,7 +195,7 @@ function FieldSeparator({
195
195
  <div
196
196
  className={cn(
197
197
  "relative -my-2 h-5 text-body-small group-data-[variant=outline]/field-group:-mb-2",
198
- className,
198
+ className
199
199
  )}
200
200
  data-content={Boolean(children)}
201
201
  data-slot="field-separator"
@@ -234,7 +234,7 @@ function FieldError({
234
234
  }
235
235
 
236
236
  const uniqueErrors = Array.from(
237
- new Map(errors.map((error) => [error?.message, error])).values(),
237
+ new Map(errors.map((error) => [error?.message, error])).values()
238
238
  );
239
239
 
240
240
  if (uniqueErrors.length === 1) {
@@ -245,7 +245,7 @@ function FieldError({
245
245
  <ul className="ml-4 flex list-disc flex-col gap-1">
246
246
  {uniqueErrors.map(
247
247
  (error) =>
248
- error?.message && <li key={error.message}>{error.message}</li>,
248
+ error?.message && <li key={error.message}>{error.message}</li>
249
249
  )}
250
250
  </ul>
251
251
  );
@@ -257,7 +257,7 @@ function FieldError({
257
257
 
258
258
  return (
259
259
  <div
260
- className={cn("text-caption text-destructive", className)}
260
+ className={cn("text-left text-caption text-destructive", className)}
261
261
  data-slot="field-error"
262
262
  role="alert"
263
263
  {...props}
@@ -1,14 +1,15 @@
1
1
  import * as React from "react";
2
2
  import { Check, X } from "lucide-react";
3
- import {
4
- Popover,
5
- PopoverContent,
6
- PopoverTrigger,
7
- } from "@/components/ui/popover";
3
+ import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
4
+ import { cn } from "@/lib/utils";
5
+ import { useThemeVars } from "@/lib/theme-provider";
8
6
 
9
- type ValidationRule = { label: string; test: (p: string) => boolean };
7
+ export type PasswordStrengthRule = {
8
+ label: string;
9
+ test: (p: string) => boolean;
10
+ };
10
11
 
11
- const RULES: ValidationRule[] = [
12
+ export const PASSWORD_STRENGTH_RULES: PasswordStrengthRule[] = [
12
13
  { label: "Minimum 8 characters", test: (p) => p.length >= 8 },
13
14
  { label: "At least one uppercase letter", test: (p) => /[A-Z]/.test(p) },
14
15
  { label: "At least one lowercase letter", test: (p) => /[a-z]/.test(p) },
@@ -24,47 +25,88 @@ export type PasswordStrengthTooltipProps = {
24
25
  password: string;
25
26
  children: React.ReactNode;
26
27
  side?: "top" | "right" | "bottom" | "left";
28
+ onRequestClose?: () => void;
27
29
  };
28
30
 
29
- export function PasswordStrengthTooltip({
30
- open = false,
31
- password,
32
- children,
33
- side = "right",
34
- }: PasswordStrengthTooltipProps) {
31
+ export const PasswordStrengthTooltip = React.forwardRef<
32
+ HTMLDivElement,
33
+ PasswordStrengthTooltipProps
34
+ >(function PasswordStrengthTooltip(
35
+ { open = false, password, children, side = "right" },
36
+ forwardedRef
37
+ ) {
38
+ const themeVars = useThemeVars();
39
+ // anchorRef is used by Positioner for popup placement.
40
+ // forwardedRef (fieldRef from consumer) is used for contains() checks in dismiss logic.
41
+ const anchorRef = React.useRef<HTMLDivElement>(null);
42
+
43
+ const composedRef = React.useCallback(
44
+ (node: HTMLDivElement | null) => {
45
+ (anchorRef as React.MutableRefObject<HTMLDivElement | null>).current =
46
+ node;
47
+ if (typeof forwardedRef === "function") {
48
+ forwardedRef(node);
49
+ } else if (forwardedRef) {
50
+ (
51
+ forwardedRef as React.MutableRefObject<HTMLDivElement | null>
52
+ ).current = node;
53
+ }
54
+ },
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ [forwardedRef]
57
+ );
58
+
35
59
  return (
36
- <Popover open={open}>
37
- <PopoverTrigger asChild>
38
- <div>{children}</div>
39
- </PopoverTrigger>
40
- <PopoverContent
41
- side={side}
42
- align="start"
43
- sideOffset={8}
44
- onOpenAutoFocus={(e) => e.preventDefault()}
45
- className="w-auto max-w-[280px] font-sans"
46
- >
47
- <div className="flex flex-col gap-1.5">
48
- {RULES.map((rule) => {
49
- const valid = password ? rule.test(password) : false;
50
- return (
51
- <div
52
- key={rule.label}
53
- className={`flex items-center gap-1.5 text-[13px] leading-[18px] ${
54
- valid ? "text-success" : "text-destructive"
55
- }`}
56
- >
57
- {valid ? (
58
- <Check size={14} className="shrink-0" />
59
- ) : (
60
- <X size={14} className="shrink-0" />
61
- )}
62
- <span>{rule.label}</span>
63
- </div>
64
- );
65
- })}
66
- </div>
67
- </PopoverContent>
68
- </Popover>
60
+ // PopoverPrimitive.Root with no Trigger — popup is fully controlled via `open` prop.
61
+ // The wrapper div has no trigger behaviors injected, so clicking the input inside
62
+ // works on the first click without interference.
63
+ <PopoverPrimitive.Root open={open}>
64
+ <div ref={composedRef}>{children}</div>
65
+ <PopoverPrimitive.Portal>
66
+ <PopoverPrimitive.Positioner
67
+ anchor={anchorRef}
68
+ side={side}
69
+ align="start"
70
+ sideOffset={8}
71
+ className="z-[200]"
72
+ >
73
+ <PopoverPrimitive.Popup
74
+ initialFocus={false}
75
+ finalFocus={false}
76
+ className={cn(
77
+ "border border-border bg-popover shadow-md outline-hidden",
78
+ "w-auto max-w-[280px] p-3 font-sans",
79
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
80
+ "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
81
+ "data-ending-style:animate-out data-ending-style:fade-out-0 data-ending-style:zoom-out-95 data-ending-style:fill-mode-forwards",
82
+ "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95"
83
+ )}
84
+ style={themeVars as React.CSSProperties}
85
+ >
86
+ <div className="flex flex-col gap-1.5">
87
+ {PASSWORD_STRENGTH_RULES.map((rule) => {
88
+ const valid = password ? rule.test(password) : false;
89
+ return (
90
+ <div
91
+ key={rule.label}
92
+ className={cn(
93
+ "flex items-center gap-1.5 text-[13px] leading-[18px]",
94
+ valid ? "text-success" : "text-destructive"
95
+ )}
96
+ >
97
+ {valid ? (
98
+ <Check size={14} className="shrink-0" />
99
+ ) : (
100
+ <X size={14} className="shrink-0" />
101
+ )}
102
+ <span>{rule.label}</span>
103
+ </div>
104
+ );
105
+ })}
106
+ </div>
107
+ </PopoverPrimitive.Popup>
108
+ </PopoverPrimitive.Positioner>
109
+ </PopoverPrimitive.Portal>
110
+ </PopoverPrimitive.Root>
69
111
  );
70
- }
112
+ });
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
1
+ import React, { useEffect, useRef, useState } from "react";
2
2
  import { EyeIcon, EyeOffIcon, Trash2Icon } from "lucide-react";
3
3
  import { Button } from "./button";
4
4
  import { Field, FieldError, FieldLabel } from "./field";
@@ -10,7 +10,10 @@ import {
10
10
  SelectTrigger,
11
11
  SelectValue,
12
12
  } from "./select";
13
- import { PasswordStrengthTooltip } from "./password-strength-tooltip";
13
+ import {
14
+ PASSWORD_STRENGTH_RULES,
15
+ PasswordStrengthTooltip,
16
+ } from "./password-strength-tooltip";
14
17
 
15
18
  // ---------------------------------------------------------------------------
16
19
  // SectionHeading
@@ -52,14 +55,6 @@ export function FormField({ label, required, children }: FormFieldProps) {
52
55
  // PasswordField — input with show/hide toggle + optional strength popover
53
56
  // ---------------------------------------------------------------------------
54
57
 
55
- const STRENGTH_RULES = [
56
- { test: (p: string) => p.length >= 8 },
57
- { test: (p: string) => /[A-Z]/.test(p) },
58
- { test: (p: string) => /[a-z]/.test(p) },
59
- { test: (p: string) => /\d/.test(p) },
60
- { test: (p: string) => /[^A-Za-z0-9]/.test(p) },
61
- ];
62
-
63
58
  export interface PasswordFieldProps {
64
59
  label: string;
65
60
  placeholder: string;
@@ -79,16 +74,30 @@ export function PasswordField({
79
74
  const [error, setError] = useState("");
80
75
  const [touched, setTouched] = useState(false);
81
76
  const [value, setValue] = useState("");
82
- const [typingStarted, setTypingStarted] = useState(false);
77
+ const [dismissed, setDismissed] = useState(false);
78
+ const wrapperRef = useRef<HTMLDivElement>(null);
83
79
 
84
- const allRulesPass = STRENGTH_RULES.every((r) => r.test(value));
85
- const isPopoverOpen = !!showStrengthPopover && typingStarted && !allRulesPass;
80
+ const allRulesPass = PASSWORD_STRENGTH_RULES.every((r) => r.test(value));
81
+ const isPopoverOpen =
82
+ !!showStrengthPopover && !dismissed && value.length > 0 && !allRulesPass;
86
83
 
87
84
  const validate = (v: string) => {
88
- if (v.length > 0 && v.length < 8) return "Min. 8 characters required";
85
+ if (!v) return "";
86
+ if (v.length < 8) return "Min. 8 characters required";
87
+ if (!PASSWORD_STRENGTH_RULES.every((r) => r.test(v)))
88
+ return "Password doesn't meet all requirements";
89
89
  return "";
90
90
  };
91
91
 
92
+ useEffect(() => {
93
+ if (!isPopoverOpen) return;
94
+ const onMouseDown = (e: MouseEvent) => {
95
+ if (!wrapperRef.current?.contains(e.target as Node)) setDismissed(true);
96
+ };
97
+ document.addEventListener("mousedown", onMouseDown, true);
98
+ return () => document.removeEventListener("mousedown", onMouseDown, true);
99
+ }, [isPopoverOpen]);
100
+
92
101
  const inputEl = (
93
102
  <div className="relative">
94
103
  <Input
@@ -100,8 +109,16 @@ export function PasswordField({
100
109
  const v = e.target.value;
101
110
  setValue(v);
102
111
  onValueChange?.(v);
103
- if (!typingStarted && v.length > 0) setTypingStarted(true);
104
112
  if (touched) setError(validate(v));
113
+ setDismissed(false);
114
+ }}
115
+ onFocus={() => setDismissed(false)}
116
+ onKeyDown={(e: React.KeyboardEvent) => {
117
+ if (e.key === "Tab") {
118
+ setTouched(true);
119
+ setError(validate(value));
120
+ setDismissed(true);
121
+ }
105
122
  }}
106
123
  onBlur={() => {
107
124
  setTouched(true);
@@ -131,6 +148,7 @@ export function PasswordField({
131
148
  </FieldLabel>
132
149
  {showStrengthPopover ? (
133
150
  <PasswordStrengthTooltip
151
+ ref={wrapperRef}
134
152
  open={isPopoverOpen}
135
153
  password={value}
136
154
  side="right"
@@ -141,7 +159,7 @@ export function PasswordField({
141
159
  inputEl
142
160
  )}
143
161
  <div className="min-h-5">
144
- {touched && error && <FieldError>{error}</FieldError>}
162
+ {touched && error && !isPopoverOpen && <FieldError>{error}</FieldError>}
145
163
  </div>
146
164
  </Field>
147
165
  );
@@ -1,4 +1,3 @@
1
- import * as React from "react";
2
1
  import { useState } from "react";
3
2
  import { Smartphone } from "lucide-react";
4
3
  import { Button } from "@/components/ui/button";
@@ -6,19 +5,20 @@ import { Field, FieldError } from "@/components/ui/field";
6
5
  import { InputGroup, InputGroupInput } from "@/components/ui/input-group";
7
6
  import { Label } from "@/components/ui/label";
8
7
  import { cn } from "@/lib/utils";
8
+ import type { ReactNode } from "react";
9
9
 
10
10
  // ─── Types ────────────────────────────────────────────────────────────────────
11
11
 
12
12
  export type TwoFAApp = {
13
13
  name: string;
14
- icon?: React.ReactNode;
15
- qrNode?: React.ReactNode;
14
+ icon?: ReactNode;
15
+ qrNode?: ReactNode;
16
16
  };
17
17
 
18
18
  export type TwoFASetupFormProps = {
19
19
  title?: string;
20
20
  apps?: TwoFAApp[];
21
- qrCodeNode?: React.ReactNode;
21
+ qrCodeNode?: ReactNode;
22
22
  onVerify: (token: string) => Promise<void>;
23
23
  onSetupLater?: () => void;
24
24
  required?: boolean;
@@ -30,6 +30,17 @@ const DEFAULT_APPS: TwoFAApp[] = [
30
30
  { name: "Microsoft Authenticator" },
31
31
  ];
32
32
 
33
+ const STEP_META = {
34
+ 1: {
35
+ subtitle: "We recommend Google Authenticator or Microsoft Authenticator",
36
+ defaultTitle: "2FA Setup",
37
+ },
38
+ 2: {
39
+ title: "Open Auth App and Scan Code",
40
+ subtitle: "Open your authenticator app and scan the QR code",
41
+ },
42
+ } as const;
43
+
33
44
  // ─── AppDownloadStep ─────────────────────────────────────────────────────────
34
45
 
35
46
  function AppDownloadStep({
@@ -45,27 +56,28 @@ function AppDownloadStep({
45
56
  }) {
46
57
  return (
47
58
  <div className="flex flex-col gap-6">
48
- <div className="flex gap-4">
59
+ <div className="flex flex-col gap-3">
49
60
  {apps.map((app) => (
50
61
  <div
51
62
  key={app.name}
52
- className="flex flex-1 flex-col items-center gap-3 border border-border bg-muted/30 p-4 text-center"
63
+ className="flex items-center gap-4 border border-border bg-muted/30 p-4"
53
64
  >
54
- <div className="flex items-center gap-2 text-sm font-medium text-foreground">
55
- {app.icon ?? (
56
- <Smartphone size={18} className="text-muted-foreground" />
65
+ <div className="flex shrink-0 items-center justify-center">
66
+ {app.qrNode ?? (
67
+ <div className="flex h-[80px] w-[80px] items-center justify-center border border-dashed border-border bg-muted/50 text-[11px] text-muted-foreground">
68
+ Scan
69
+ </div>
57
70
  )}
58
- {app.name}
59
71
  </div>
60
- {app.qrNode ? (
61
- <div className="flex items-center justify-center">
62
- {app.qrNode}
63
- </div>
64
- ) : (
65
- <div className="flex h-[100px] w-[100px] items-center justify-center border border-dashed border-border bg-muted/50 text-[11px] text-muted-foreground">
66
- Scan to download
72
+ <div className="flex flex-col gap-1">
73
+ <div className="flex items-center gap-2 text-sm font-medium text-foreground">
74
+ {app.icon ?? (
75
+ <Smartphone size={16} className="text-muted-foreground" />
76
+ )}
77
+ {app.name}
67
78
  </div>
68
- )}
79
+ <p className="text-xs text-muted-foreground">Scan to download</p>
80
+ </div>
69
81
  </div>
70
82
  ))}
71
83
  </div>
@@ -97,7 +109,7 @@ function VerificationStep({
97
109
  onSetupLater,
98
110
  required,
99
111
  }: {
100
- qrCodeNode?: React.ReactNode;
112
+ qrCodeNode?: ReactNode;
101
113
  onVerify: (token: string) => Promise<void>;
102
114
  onBack: () => void;
103
115
  onSetupLater?: () => void;
@@ -116,8 +128,10 @@ function VerificationStep({
116
128
  setIsLoading(true);
117
129
  try {
118
130
  await onVerify(token);
119
- } catch {
120
- setError("Invalid code. Please try again.");
131
+ } catch (err) {
132
+ setError(
133
+ err instanceof Error ? err.message : "Invalid code. Please try again."
134
+ );
121
135
  } finally {
122
136
  setIsLoading(false);
123
137
  }
@@ -126,9 +140,7 @@ function VerificationStep({
126
140
  return (
127
141
  <div className="flex flex-col gap-6">
128
142
  <div className="flex flex-col items-center gap-3">
129
- {qrCodeNode ? (
130
- <div className="flex items-center justify-center">{qrCodeNode}</div>
131
- ) : (
143
+ {qrCodeNode ?? (
132
144
  <div className="flex h-[140px] w-[140px] items-center justify-center border border-dashed border-border bg-muted/50 text-xs text-muted-foreground">
133
145
  QR Code
134
146
  </div>
@@ -185,7 +197,7 @@ function VerificationStep({
185
197
  // ─── TwoFASetupForm ───────────────────────────────────────────────────────────
186
198
 
187
199
  export function TwoFASetupForm({
188
- title = "2FA Setup",
200
+ title,
189
201
  apps = DEFAULT_APPS,
190
202
  qrCodeNode,
191
203
  onVerify,
@@ -195,15 +207,13 @@ export function TwoFASetupForm({
195
207
  }: TwoFASetupFormProps) {
196
208
  const [step, setStep] = useState<1 | 2>(1);
197
209
 
198
- const stepTitle = step === 1 ? title : "Open Auth App and Scan Code";
199
- const stepSubtitle =
200
- step === 1
201
- ? "We recommend Google Authenticator or Microsoft Authenticator"
202
- : "Open your authenticator app and scan the QR code";
210
+ const stepTitle =
211
+ step === 1 ? title ?? STEP_META[1].defaultTitle : STEP_META[2].title;
212
+ const stepSubtitle = STEP_META[step].subtitle;
203
213
 
204
214
  return (
205
215
  <div className={cn("flex flex-col gap-6", className)}>
206
- <div className="flex flex-col gap-1">
216
+ <div className="flex flex-col items-center gap-1 text-center">
207
217
  <h2 className="text-xl font-semibold text-foreground">{stepTitle}</h2>
208
218
  <p className="text-sm text-muted-foreground">{stepSubtitle}</p>
209
219
  </div>