New components: - RegisterForm — name, email, password, confirm, terms, password strength - ForgotPasswordForm — email input with success/error states, back link - ResetPasswordForm — new password + confirm with strength indicator - VerifyEmailForm — 6-digit code input with resend, numeric-only filter - OnboardingShell — step indicator, progress bar, back/next/complete nav - AuthPageLayout — full-page centered card with product branding - PasswordStrengthBar — visual bar + label (weak/fair/good/strong) Existing components preserved: LoginForm, MfaChallenge, SocialButtons All styled via --bl-* CSS custom properties for product theming 54 tests (13 existing + 41 new) — all passing
149 lines
4.1 KiB
TypeScript
149 lines
4.1 KiB
TypeScript
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 (
|
|
<div className={className} data-testid="bl-onboarding-shell">
|
|
{/* Progress bar */}
|
|
<div
|
|
style={{
|
|
height: '4px',
|
|
borderRadius: '2px',
|
|
background: 'var(--bl-border, #e5e7eb)',
|
|
marginBottom: '24px',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<div
|
|
data-testid="bl-onboarding-progress"
|
|
style={{
|
|
height: '100%',
|
|
width: `${progress}%`,
|
|
background: 'var(--bl-primary, #0066ff)',
|
|
transition: 'width 0.3s ease',
|
|
borderRadius: '2px',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Step indicator */}
|
|
<div
|
|
data-testid="bl-onboarding-steps"
|
|
style={{
|
|
display: 'flex',
|
|
gap: '8px',
|
|
marginBottom: '24px',
|
|
justifyContent: 'center',
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
{steps.map((step, i) => (
|
|
<div
|
|
key={step.key}
|
|
data-testid={`bl-onboarding-step-${step.key}`}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '6px',
|
|
fontSize: '13px',
|
|
color:
|
|
i === currentStep
|
|
? 'var(--bl-primary, #0066ff)'
|
|
: i < currentStep
|
|
? 'var(--bl-success, #22c55e)'
|
|
: 'var(--bl-muted, #999)',
|
|
fontWeight: i === currentStep ? 600 : 400,
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
width: '24px',
|
|
height: '24px',
|
|
borderRadius: '50%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
background:
|
|
i <= currentStep ? 'var(--bl-primary, #0066ff)' : 'var(--bl-border, #e5e7eb)',
|
|
color: i <= currentStep ? '#fff' : 'var(--bl-muted, #999)',
|
|
}}
|
|
>
|
|
{i < currentStep ? '✓' : i + 1}
|
|
</span>
|
|
{step.label}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Step content */}
|
|
<div data-testid="bl-onboarding-content" style={{ marginBottom: '24px' }}>
|
|
{children}
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
gap: '12px',
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={onBack}
|
|
disabled={isFirst}
|
|
data-testid="bl-onboarding-back"
|
|
style={{
|
|
padding: '10px 20px',
|
|
border: '1px solid var(--bl-border, #ccc)',
|
|
borderRadius: 'var(--bl-radius, 6px)',
|
|
background: 'var(--bl-surface, #fff)',
|
|
color: 'var(--bl-text, #333)',
|
|
cursor: isFirst ? 'not-allowed' : 'pointer',
|
|
fontSize: '14px',
|
|
opacity: isFirst ? 0.4 : 1,
|
|
}}
|
|
>
|
|
Back
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={isLast ? onComplete : onNext}
|
|
data-testid={isLast ? 'bl-onboarding-complete' : 'bl-onboarding-next'}
|
|
style={{
|
|
padding: '10px 20px',
|
|
border: 'none',
|
|
borderRadius: 'var(--bl-radius, 6px)',
|
|
background: 'var(--bl-primary, #0066ff)',
|
|
color: '#fff',
|
|
cursor: 'pointer',
|
|
fontSize: '14px',
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{isLast ? 'Complete' : 'Next'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|