diff --git a/packages/auth-ui/src/AuthPageLayout.tsx b/packages/auth-ui/src/AuthPageLayout.tsx new file mode 100644 index 00000000..26a28d2f --- /dev/null +++ b/packages/auth-ui/src/AuthPageLayout.tsx @@ -0,0 +1,101 @@ +import type { AuthPageLayoutProps } from './types.js'; + +/** + * Full-page auth layout — centered card with product branding. + * Wraps any auth form (LoginForm, RegisterForm, etc.). + * Styled via CSS custom properties (inherits --bl-* from host app). + */ +export function AuthPageLayout({ + productName, + logo, + title, + subtitle, + children, + footer, + className, +}: AuthPageLayoutProps) { + return ( +
+
+ {/* Branding */} +
+ {logo && ( +
+ {typeof logo === 'string' ? ( + {productName} + ) : ( + logo + )} +
+ )} +
+ {productName} +
+
+ {title} +
+ {subtitle && ( +
+ {subtitle} +
+ )} +
+ + {/* Form content */} + {children} + + {/* Footer */} + {footer && ( +
+ {footer} +
+ )} +
+
+ ); +} diff --git a/packages/auth-ui/src/ForgotPasswordForm.tsx b/packages/auth-ui/src/ForgotPasswordForm.tsx new file mode 100644 index 00000000..94c7125b --- /dev/null +++ b/packages/auth-ui/src/ForgotPasswordForm.tsx @@ -0,0 +1,111 @@ +import { useState, type FormEvent } from 'react'; +import type { ForgotPasswordFormProps } from './types.js'; + +/** + * Forgot password form — email input to request a reset link. + * Styled via CSS custom properties (inherits --bl-* from host app). + */ +export function ForgotPasswordForm({ + onSubmit, + isLoading = false, + error, + success, + onBack, + className, +}: ForgotPasswordFormProps) { + const [email, setEmail] = useState(''); + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + onSubmit(email); + } + + const inputStyle = { + padding: '10px 12px', + border: '1px solid var(--bl-border, #ccc)', + borderRadius: 'var(--bl-radius, 6px)', + fontSize: '14px', + width: '100%', + boxSizing: 'border-box' as const, + }; + + return ( +
+
+
+ Enter your email address and we'll send you a link to reset your password. +
+ + setEmail(e.target.value)} + required + disabled={isLoading} + data-testid="bl-forgot-email" + style={inputStyle} + /> + + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ {success} +
+ )} + + + + {onBack && ( + + )} +
+
+ ); +} diff --git a/packages/auth-ui/src/OnboardingShell.tsx b/packages/auth-ui/src/OnboardingShell.tsx new file mode 100644 index 00000000..a0d9b5a9 --- /dev/null +++ b/packages/auth-ui/src/OnboardingShell.tsx @@ -0,0 +1,148 @@ +import type { OnboardingShellProps } from './types.js'; + +/** + * Onboarding shell — step indicator, navigation, progress bar. + * Wraps arbitrary step content provided via children. + * Styled via CSS custom properties (inherits --bl-* from host app). + */ +export function OnboardingShell({ + steps, + currentStep, + onNext, + onBack, + onComplete, + children, + className, +}: OnboardingShellProps) { + const isFirst = currentStep === 0; + const isLast = currentStep === steps.length - 1; + const progress = steps.length > 1 ? ((currentStep + 1) / steps.length) * 100 : 100; + + return ( +
+ {/* Progress bar */} +
+
+
+ + {/* Step indicator */} +
+ {steps.map((step, i) => ( +
+ + {i < currentStep ? '✓' : i + 1} + + {step.label} +
+ ))} +
+ + {/* Step content */} +
+ {children} +
+ + {/* Navigation */} +
+ + + +
+
+ ); +} diff --git a/packages/auth-ui/src/PasswordStrengthBar.tsx b/packages/auth-ui/src/PasswordStrengthBar.tsx new file mode 100644 index 00000000..9c3e2b6c --- /dev/null +++ b/packages/auth-ui/src/PasswordStrengthBar.tsx @@ -0,0 +1,67 @@ +import { useMemo } from 'react'; +import type { PasswordStrength } from './types.js'; + +interface PasswordStrengthBarProps { + password: string; + className?: string; +} + +const STRENGTH_CONFIG: Record = { + weak: { color: 'var(--bl-error, #dc3545)', label: 'Weak' }, + fair: { color: 'var(--bl-warning, #f59e0b)', label: 'Fair' }, + good: { color: 'var(--bl-info, #3b82f6)', label: 'Good' }, + strong: { color: 'var(--bl-success, #22c55e)', label: 'Strong' }, +}; + +export function getPasswordStrength(password: string): PasswordStrength { + let score = 0; + if (password.length >= 8) score++; + if (password.length >= 12) score++; + if (/[A-Z]/.test(password)) score++; + if (/[a-z]/.test(password)) score++; + if (/\d/.test(password)) score++; + if (/[^A-Za-z0-9]/.test(password)) score++; + + if (score <= 2) return 'weak'; + if (score <= 3) return 'fair'; + if (score <= 4) return 'good'; + return 'strong'; +} + +export function PasswordStrengthBar({ password, className }: PasswordStrengthBarProps) { + const strength = useMemo(() => getPasswordStrength(password), [password]); + const config = STRENGTH_CONFIG[strength]; + const widthPercent = { weak: 25, fair: 50, good: 75, strong: 100 }[strength]; + + if (!password) return null; + + return ( +
+
+
+
+
+ {config.label} +
+
+ ); +} diff --git a/packages/auth-ui/src/RegisterForm.tsx b/packages/auth-ui/src/RegisterForm.tsx new file mode 100644 index 00000000..0ab1b1b1 --- /dev/null +++ b/packages/auth-ui/src/RegisterForm.tsx @@ -0,0 +1,226 @@ +import { useState, type FormEvent } from 'react'; +import { PasswordStrengthBar } from './PasswordStrengthBar.js'; +import type { RegisterFormProps } from './types.js'; + +/** + * Registration form with name, email, password, confirm password, + * password strength indicator, and optional terms checkbox. + * Styled via CSS custom properties (inherits --bl-* from host app). + */ +export function RegisterForm({ + onSubmit, + isLoading = false, + error, + termsUrl, + privacyUrl, + onSwitchToLogin, + className, +}: RegisterFormProps) { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + const [termsAccepted, setTermsAccepted] = useState(!termsUrl); + + const passwordMismatch = confirm.length > 0 && password !== confirm; + const canSubmit = + name.trim().length > 0 && + email.length > 0 && + password.length >= 8 && + !passwordMismatch && + termsAccepted && + !isLoading; + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + if (!canSubmit) return; + onSubmit({ name: name.trim(), email, password }); + } + + const inputStyle = { + padding: '10px 12px', + border: '1px solid var(--bl-border, #ccc)', + borderRadius: 'var(--bl-radius, 6px)', + fontSize: '14px', + width: '100%', + boxSizing: 'border-box' as const, + }; + + return ( +
+
+ + + + + + + + + + + {passwordMismatch && ( +
+ Passwords do not match +
+ )} + + {termsUrl && ( + + )} + + {error && ( +
+ {error} +
+ )} + + + + {onSwitchToLogin && ( +
+ Already have an account?{' '} + +
+ )} + +
+ ); +} diff --git a/packages/auth-ui/src/ResetPasswordForm.tsx b/packages/auth-ui/src/ResetPasswordForm.tsx new file mode 100644 index 00000000..bd6454e0 --- /dev/null +++ b/packages/auth-ui/src/ResetPasswordForm.tsx @@ -0,0 +1,131 @@ +import { useState, type FormEvent } from 'react'; +import { PasswordStrengthBar } from './PasswordStrengthBar.js'; +import type { ResetPasswordFormProps } from './types.js'; + +/** + * Reset password form — new password + confirm, with strength indicator. + * Styled via CSS custom properties (inherits --bl-* from host app). + */ +export function ResetPasswordForm({ + onSubmit, + isLoading = false, + error, + success, + className, +}: ResetPasswordFormProps) { + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + + const passwordMismatch = confirm.length > 0 && password !== confirm; + const canSubmit = password.length >= 8 && !passwordMismatch && !isLoading; + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + if (!canSubmit) return; + onSubmit(password); + } + + const inputStyle = { + padding: '10px 12px', + border: '1px solid var(--bl-border, #ccc)', + borderRadius: 'var(--bl-radius, 6px)', + fontSize: '14px', + width: '100%', + boxSizing: 'border-box' as const, + }; + + return ( +
+
+
+ Enter your new password. +
+ + + + + + + + {passwordMismatch && ( +
+ Passwords do not match +
+ )} + + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ {success} +
+ )} + + + +
+ ); +} diff --git a/packages/auth-ui/src/VerifyEmailForm.tsx b/packages/auth-ui/src/VerifyEmailForm.tsx new file mode 100644 index 00000000..fce07463 --- /dev/null +++ b/packages/auth-ui/src/VerifyEmailForm.tsx @@ -0,0 +1,117 @@ +import { useState, type FormEvent } from 'react'; +import type { VerifyEmailFormProps } from './types.js'; + +/** + * Email verification form — 6-digit code input with resend option. + * Styled via CSS custom properties (inherits --bl-* from host app). + */ +export function VerifyEmailForm({ + onSubmit, + onResend, + isLoading = false, + error, + success, + email, + className, +}: VerifyEmailFormProps) { + const [code, setCode] = useState(''); + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + onSubmit(code); + } + + return ( +
+
+
+ Enter the 6-digit code sent to {email ? {email} : 'your email'}. +
+ + setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + required + disabled={isLoading} + maxLength={6} + data-testid="bl-verify-code" + style={{ + padding: '12px', + border: '1px solid var(--bl-border, #ccc)', + borderRadius: 'var(--bl-radius, 6px)', + fontSize: '24px', + textAlign: 'center', + letterSpacing: '6px', + fontFamily: 'monospace', + width: '100%', + boxSizing: 'border-box', + }} + /> + + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ {success} +
+ )} + + + + {onResend && ( + + )} +
+
+ ); +} diff --git a/packages/auth-ui/src/__tests__/new-components.test.tsx b/packages/auth-ui/src/__tests__/new-components.test.tsx new file mode 100644 index 00000000..76526695 --- /dev/null +++ b/packages/auth-ui/src/__tests__/new-components.test.tsx @@ -0,0 +1,401 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, cleanup } from '@testing-library/react'; +import { RegisterForm } from '../RegisterForm.js'; +import { ForgotPasswordForm } from '../ForgotPasswordForm.js'; +import { ResetPasswordForm } from '../ResetPasswordForm.js'; +import { VerifyEmailForm } from '../VerifyEmailForm.js'; +import { OnboardingShell } from '../OnboardingShell.js'; +import { AuthPageLayout } from '../AuthPageLayout.js'; +import { PasswordStrengthBar, getPasswordStrength } from '../PasswordStrengthBar.js'; + +describe('RegisterForm', () => { + beforeEach(() => cleanup()); + + it('renders all fields', () => { + render(); + expect(screen.getByTestId('bl-register-name')).toBeDefined(); + expect(screen.getByTestId('bl-register-email')).toBeDefined(); + expect(screen.getByTestId('bl-register-password')).toBeDefined(); + expect(screen.getByTestId('bl-register-confirm')).toBeDefined(); + expect(screen.getByTestId('bl-register-submit')).toBeDefined(); + }); + + it('calls onSubmit with name, email, password', () => { + const onSubmit = vi.fn(); + render(); + + fireEvent.change(screen.getByTestId('bl-register-name'), { target: { value: 'Alice' } }); + fireEvent.change(screen.getByTestId('bl-register-email'), { + target: { value: 'alice@example.com' }, + }); + fireEvent.change(screen.getByTestId('bl-register-password'), { + target: { value: 'Password1!' }, + }); + fireEvent.change(screen.getByTestId('bl-register-confirm'), { + target: { value: 'Password1!' }, + }); + fireEvent.submit(screen.getByTestId('bl-register-submit').closest('form')!); + + expect(onSubmit).toHaveBeenCalledWith({ + name: 'Alice', + email: 'alice@example.com', + password: 'Password1!', + }); + }); + + it('shows password mismatch error', () => { + render(); + fireEvent.change(screen.getByTestId('bl-register-password'), { + target: { value: 'Password1!' }, + }); + fireEvent.change(screen.getByTestId('bl-register-confirm'), { + target: { value: 'Different1!' }, + }); + + expect(screen.getByTestId('bl-register-mismatch')).toBeDefined(); + expect(screen.getByText('Passwords do not match')).toBeDefined(); + }); + + it('displays error message', () => { + render(); + expect(screen.getByTestId('bl-register-error')).toBeDefined(); + expect(screen.getByText('Email already taken')).toBeDefined(); + }); + + it('shows terms checkbox when termsUrl provided', () => { + render(); + expect(screen.getByTestId('bl-register-terms')).toBeDefined(); + expect(screen.getByText('Terms of Service')).toBeDefined(); + }); + + it('renders switch to login link', () => { + const onSwitch = vi.fn(); + render(); + const link = screen.getByTestId('bl-register-switch-login'); + fireEvent.click(link); + expect(onSwitch).toHaveBeenCalledOnce(); + }); + + it('shows loading state', () => { + render(); + expect(screen.getByText('Creating account...')).toBeDefined(); + }); + + it('shows password strength bar when typing', () => { + render(); + fireEvent.change(screen.getByTestId('bl-register-password'), { target: { value: 'ab' } }); + expect(screen.getByTestId('bl-password-strength')).toBeDefined(); + }); +}); + +describe('ForgotPasswordForm', () => { + beforeEach(() => cleanup()); + + it('renders email input and submit', () => { + render(); + expect(screen.getByTestId('bl-forgot-email')).toBeDefined(); + expect(screen.getByTestId('bl-forgot-submit')).toBeDefined(); + }); + + it('calls onSubmit with email', () => { + const onSubmit = vi.fn(); + render(); + fireEvent.change(screen.getByTestId('bl-forgot-email'), { + target: { value: 'test@example.com' }, + }); + fireEvent.submit(screen.getByTestId('bl-forgot-submit').closest('form')!); + expect(onSubmit).toHaveBeenCalledWith('test@example.com'); + }); + + it('displays error message', () => { + render(); + expect(screen.getByTestId('bl-forgot-error')).toBeDefined(); + }); + + it('displays success message', () => { + render(); + expect(screen.getByTestId('bl-forgot-success')).toBeDefined(); + expect(screen.getByText('Check your email')).toBeDefined(); + }); + + it('renders back button and calls onBack', () => { + const onBack = vi.fn(); + render(); + fireEvent.click(screen.getByTestId('bl-forgot-back')); + expect(onBack).toHaveBeenCalledOnce(); + }); + + it('shows loading state', () => { + render(); + expect(screen.getByText('Sending...')).toBeDefined(); + }); +}); + +describe('ResetPasswordForm', () => { + beforeEach(() => cleanup()); + + it('renders password fields and submit', () => { + render(); + expect(screen.getByTestId('bl-reset-password')).toBeDefined(); + expect(screen.getByTestId('bl-reset-confirm')).toBeDefined(); + expect(screen.getByTestId('bl-reset-submit')).toBeDefined(); + }); + + it('calls onSubmit with password', () => { + const onSubmit = vi.fn(); + render(); + fireEvent.change(screen.getByTestId('bl-reset-password'), { target: { value: 'NewPass1!' } }); + fireEvent.change(screen.getByTestId('bl-reset-confirm'), { target: { value: 'NewPass1!' } }); + fireEvent.submit(screen.getByTestId('bl-reset-submit').closest('form')!); + expect(onSubmit).toHaveBeenCalledWith('NewPass1!'); + }); + + it('shows mismatch error', () => { + render(); + fireEvent.change(screen.getByTestId('bl-reset-password'), { target: { value: 'NewPass1!' } }); + fireEvent.change(screen.getByTestId('bl-reset-confirm'), { target: { value: 'Different!' } }); + expect(screen.getByTestId('bl-reset-mismatch')).toBeDefined(); + }); + + it('displays error and success messages', () => { + const { rerender } = render(); + expect(screen.getByTestId('bl-reset-error')).toBeDefined(); + + rerender(); + expect(screen.getByTestId('bl-reset-success')).toBeDefined(); + }); + + it('shows password strength bar', () => { + render(); + fireEvent.change(screen.getByTestId('bl-reset-password'), { target: { value: 'abc' } }); + expect(screen.getByTestId('bl-password-strength')).toBeDefined(); + }); +}); + +describe('VerifyEmailForm', () => { + beforeEach(() => cleanup()); + + it('renders code input and submit', () => { + render(); + expect(screen.getByTestId('bl-verify-code')).toBeDefined(); + expect(screen.getByTestId('bl-verify-submit')).toBeDefined(); + }); + + it('calls onSubmit with code', () => { + const onSubmit = vi.fn(); + render(); + fireEvent.change(screen.getByTestId('bl-verify-code'), { target: { value: '123456' } }); + fireEvent.submit(screen.getByTestId('bl-verify-submit').closest('form')!); + expect(onSubmit).toHaveBeenCalledWith('123456'); + }); + + it('displays email address', () => { + render(); + expect(screen.getByText('test@example.com')).toBeDefined(); + }); + + it('renders resend button', () => { + const onResend = vi.fn(); + render(); + fireEvent.click(screen.getByTestId('bl-verify-resend')); + expect(onResend).toHaveBeenCalledOnce(); + }); + + it('displays error and success messages', () => { + const { rerender } = render(); + expect(screen.getByTestId('bl-verify-error')).toBeDefined(); + + rerender(); + expect(screen.getByTestId('bl-verify-success')).toBeDefined(); + }); + + it('strips non-numeric characters', () => { + render(); + const input = screen.getByTestId('bl-verify-code'); + fireEvent.change(input, { target: { value: 'abc123def456' } }); + expect((input as unknown as { value: string }).value).toBe('123456'); + }); +}); + +describe('OnboardingShell', () => { + beforeEach(() => cleanup()); + + const steps = [ + { key: 'welcome', label: 'Welcome' }, + { key: 'profile', label: 'Profile' }, + { key: 'preferences', label: 'Preferences' }, + ]; + + it('renders steps and content', () => { + render( + +
Step 1 content
+
+ ); + expect(screen.getByTestId('bl-onboarding-shell')).toBeDefined(); + expect(screen.getByTestId('bl-onboarding-steps')).toBeDefined(); + expect(screen.getByTestId('bl-onboarding-progress')).toBeDefined(); + expect(screen.getByText('Step 1 content')).toBeDefined(); + expect(screen.getByText('Welcome')).toBeDefined(); + expect(screen.getByText('Profile')).toBeDefined(); + }); + + it('disables Back on first step', () => { + render( + +
+ + ); + const back = screen.getByTestId('bl-onboarding-back'); + expect(back.getAttribute('disabled')).toBe(''); + }); + + it('calls onNext on middle step', () => { + const onNext = vi.fn(); + render( + +
+ + ); + fireEvent.click(screen.getByTestId('bl-onboarding-next')); + expect(onNext).toHaveBeenCalledOnce(); + }); + + it('shows Complete on last step and calls onComplete', () => { + const onComplete = vi.fn(); + render( + +
+ + ); + const btn = screen.getByTestId('bl-onboarding-complete'); + expect(btn.textContent).toBe('Complete'); + fireEvent.click(btn); + expect(onComplete).toHaveBeenCalledOnce(); + }); + + it('calls onBack on non-first step', () => { + const onBack = vi.fn(); + render( + +
+ + ); + fireEvent.click(screen.getByTestId('bl-onboarding-back')); + expect(onBack).toHaveBeenCalledOnce(); + }); +}); + +describe('AuthPageLayout', () => { + beforeEach(() => cleanup()); + + it('renders product name and title', () => { + render( + +
Form content
+
+ ); + expect(screen.getByTestId('bl-auth-product-name').textContent).toBe('TestApp'); + expect(screen.getByTestId('bl-auth-title').textContent).toBe('Sign In'); + expect(screen.getByText('Form content')).toBeDefined(); + }); + + it('renders subtitle when provided', () => { + render( + +
+ + ); + expect(screen.getByTestId('bl-auth-subtitle').textContent).toBe('Welcome back'); + }); + + it('renders logo as element', () => { + render( + Logo} + > +
+ + ); + expect(screen.getByTestId('custom-logo')).toBeDefined(); + }); + + it('renders footer', () => { + render( + Footer text}> +
+ + ); + expect(screen.getByTestId('bl-auth-footer')).toBeDefined(); + expect(screen.getByText('Footer text')).toBeDefined(); + }); +}); + +describe('PasswordStrengthBar', () => { + beforeEach(() => cleanup()); + + it('returns null for empty password', () => { + const { container } = render(); + expect(container.querySelector('[data-testid="bl-password-strength"]')).toBeNull(); + }); + + it('shows Weak for short password', () => { + render(); + expect(screen.getByTestId('bl-password-strength-label').textContent).toBe('Weak'); + }); + + it('shows Strong for complex password', () => { + render(); + expect(screen.getByTestId('bl-password-strength-label').textContent).toBe('Strong'); + }); +}); + +describe('getPasswordStrength', () => { + it('returns weak for very short passwords', () => { + expect(getPasswordStrength('ab')).toBe('weak'); + }); + + it('returns fair for medium passwords', () => { + expect(getPasswordStrength('abcdefgh1')).toBe('fair'); + }); + + it('returns good for decent passwords', () => { + expect(getPasswordStrength('Abcdefgh1')).toBe('good'); + }); + + it('returns strong for complex passwords', () => { + expect(getPasswordStrength('MyStr0ng!Pass')).toBe('strong'); + }); +}); diff --git a/packages/auth-ui/src/index.ts b/packages/auth-ui/src/index.ts index 9803e793..e24f8748 100644 --- a/packages/auth-ui/src/index.ts +++ b/packages/auth-ui/src/index.ts @@ -1,9 +1,24 @@ export { LoginForm } from './LoginForm.js'; +export { RegisterForm } from './RegisterForm.js'; +export { ForgotPasswordForm } from './ForgotPasswordForm.js'; +export { ResetPasswordForm } from './ResetPasswordForm.js'; +export { VerifyEmailForm } from './VerifyEmailForm.js'; export { MfaChallenge } from './MfaChallenge.js'; export { SocialButtons } from './SocialButtons.js'; +export { OnboardingShell } from './OnboardingShell.js'; +export { AuthPageLayout } from './AuthPageLayout.js'; +export { PasswordStrengthBar, getPasswordStrength } from './PasswordStrengthBar.js'; export type { LoginFormProps, + RegisterFormProps, + ForgotPasswordFormProps, + ResetPasswordFormProps, + VerifyEmailFormProps, MfaChallengeProps, SocialButtonsProps, SocialProvider, + OnboardingShellProps, + OnboardingStep, + AuthPageLayoutProps, + PasswordStrength, } from './types.js'; diff --git a/packages/auth-ui/src/types.ts b/packages/auth-ui/src/types.ts index db3f9bd5..706f44fd 100644 --- a/packages/auth-ui/src/types.ts +++ b/packages/auth-ui/src/types.ts @@ -40,3 +40,108 @@ export interface SocialButtonsProps { /** Additional CSS class for the root element. */ className?: string; } + +export interface RegisterFormProps { + /** Called when user submits registration. */ + onSubmit: (data: { name: string; email: string; password: string }) => void; + /** Whether the form is currently loading. */ + isLoading?: boolean; + /** Error message to display. */ + error?: string | null; + /** Terms of service URL (renders checkbox if provided). */ + termsUrl?: string; + /** Privacy policy URL. */ + privacyUrl?: string; + /** Called when user clicks "Already have an account?" */ + onSwitchToLogin?: () => void; + /** Additional CSS class for the root element. */ + className?: string; +} + +export interface ForgotPasswordFormProps { + /** Called when user submits email for password reset. */ + onSubmit: (email: string) => void; + /** Whether the form is currently loading. */ + isLoading?: boolean; + /** Error message to display. */ + error?: string | null; + /** Success message (e.g., "Check your email"). */ + success?: string | null; + /** Called when user clicks "Back to login". */ + onBack?: () => void; + /** Additional CSS class for the root element. */ + className?: string; +} + +export interface ResetPasswordFormProps { + /** Called when user submits new password. */ + onSubmit: (password: string) => void; + /** Whether the form is currently loading. */ + isLoading?: boolean; + /** Error message to display. */ + error?: string | null; + /** Success message (e.g., "Password updated"). */ + success?: string | null; + /** Additional CSS class for the root element. */ + className?: string; +} + +export interface VerifyEmailFormProps { + /** Called when user submits the verification code. */ + onSubmit: (code: string) => void; + /** Called when user clicks "Resend code". */ + onResend?: () => void; + /** Whether the form is currently loading. */ + isLoading?: boolean; + /** Error message to display. */ + error?: string | null; + /** Success message (e.g., "Code resent"). */ + success?: string | null; + /** Email address being verified (for display). */ + email?: string; + /** Additional CSS class for the root element. */ + className?: string; +} + +export interface OnboardingStep { + /** Unique key for the step. */ + key: string; + /** Display label for the step indicator. */ + label: string; +} + +export interface OnboardingShellProps { + /** Ordered list of steps. */ + steps: OnboardingStep[]; + /** Index of the current step (0-based). */ + currentStep: number; + /** Called when user clicks Next. */ + onNext: () => void; + /** Called when user clicks Back. */ + onBack: () => void; + /** Called when the final step completes. */ + onComplete: () => void; + /** Content to render for the current step. */ + children: React.ReactNode; + /** Additional CSS class for the root element. */ + className?: string; +} + +export interface AuthPageLayoutProps { + /** Product name displayed at the top. */ + productName: string; + /** Optional logo element or URL. */ + logo?: React.ReactNode; + /** Page title (e.g., "Sign In", "Create Account"). */ + title: string; + /** Subtitle or description. */ + subtitle?: string; + /** Form content. */ + children: React.ReactNode; + /** Footer content (links, etc.). */ + footer?: React.ReactNode; + /** Additional CSS class for the root element. */ + className?: string; +} + +export type PasswordStrength = 'weak' | 'fair' | 'good' | 'strong';