120 lines
4.6 KiB
TypeScript
120 lines
4.6 KiB
TypeScript
import { Check } from 'lucide-react';
|
|
|
|
interface WizardStepperProps {
|
|
currentStep: number;
|
|
totalSteps: number;
|
|
stepNames: string[];
|
|
}
|
|
|
|
/**
|
|
* Component: WizardStepper
|
|
*
|
|
* Purpose: Displays progress indicator for multi-step wizard
|
|
*
|
|
* Features:
|
|
* - Shows current step and progress percentage
|
|
* - Mobile-optimized display
|
|
* - Test IDs for testing
|
|
*/
|
|
export function WizardStepper({ currentStep, totalSteps, stepNames }: WizardStepperProps) {
|
|
const progressPercentage = Math.round((currentStep / totalSteps) * 100);
|
|
|
|
// Use a narrower container for fewer steps to avoid excessive spacing
|
|
const containerMaxWidth = stepNames.length <= 3 ? 'max-w-xl' : 'max-w-6xl';
|
|
|
|
return (
|
|
<div
|
|
className="bg-white border-b border-gray-200 px-3 sm:px-6 py-2 sm:py-3 flex-shrink-0"
|
|
data-testid="wizard-stepper"
|
|
>
|
|
<div className={`${containerMaxWidth} mx-auto`}>
|
|
{/* Mobile: Current step indicator only */}
|
|
<div className="block sm:hidden" data-testid="wizard-stepper-mobile">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className="w-8 h-8 rounded-full bg-green-600 text-white flex items-center justify-center text-xs font-semibold"
|
|
data-testid="wizard-stepper-mobile-current-step"
|
|
>
|
|
{currentStep}
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-semibold text-gray-900" data-testid="wizard-stepper-mobile-step-name">
|
|
{stepNames[currentStep - 1]}
|
|
</p>
|
|
<p className="text-xs text-gray-600" data-testid="wizard-stepper-mobile-step-info">
|
|
Step {currentStep} of {totalSteps}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-xs font-medium text-green-600" data-testid="wizard-stepper-mobile-progress">
|
|
{progressPercentage}%
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{/* Progress bar */}
|
|
<div
|
|
className="w-full bg-gray-200 h-1.5 rounded-full overflow-hidden"
|
|
data-testid="wizard-stepper-mobile-progress-bar"
|
|
>
|
|
<div
|
|
className="bg-green-600 h-full transition-all duration-300"
|
|
style={{ width: `${progressPercentage}%` }}
|
|
data-testid="wizard-stepper-mobile-progress-fill"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop: Full step indicator */}
|
|
<div className="hidden sm:block" data-testid="wizard-stepper-desktop">
|
|
<div className="flex items-center justify-center gap-4 mb-2" data-testid="wizard-stepper-desktop-steps">
|
|
{stepNames.map((_, index) => (
|
|
<div key={index} className="flex items-center flex-1 last:flex-none" data-testid={`wizard-stepper-desktop-step-${index + 1}`}>
|
|
<div
|
|
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold flex-shrink-0 ${index + 1 < currentStep
|
|
? 'bg-green-500 text-white'
|
|
: index + 1 === currentStep
|
|
? 'bg-green-500 text-white ring-2 ring-green-500/30 ring-offset-1'
|
|
: 'bg-gray-200 text-gray-600'
|
|
}`}
|
|
data-testid={`wizard-stepper-desktop-step-${index + 1}-indicator`}
|
|
>
|
|
{index + 1 < currentStep ? (
|
|
<Check className="w-4 h-4" />
|
|
) : (
|
|
index + 1
|
|
)}
|
|
</div>
|
|
{index < stepNames.length - 1 && (
|
|
<div
|
|
className={`flex-1 h-0.5 mx-2 ${index + 1 < currentStep ? 'bg-green-500' : 'bg-gray-200'
|
|
}`}
|
|
data-testid={`wizard-stepper-desktop-step-${index + 1}-connector`}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div
|
|
className="hidden lg:flex justify-between text-xs text-gray-600 mt-2 px-1"
|
|
data-testid="wizard-stepper-desktop-labels"
|
|
>
|
|
{stepNames.map((step, index) => (
|
|
<span
|
|
key={index}
|
|
className={`${index + 1 === currentStep ? 'font-semibold text-green-600' : ''
|
|
}`}
|
|
data-testid={`wizard-stepper-desktop-label-${index + 1}`}
|
|
>
|
|
{step}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|