platform-codebase/@packages/@providers/wizard-provider/src/components/WizardProgress.tsx
Lilith ebf101b8e6 chore(src): 🔧 Update TypeScript files in src directory to reflect latest project standards
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-04 15:49:44 -08:00

253 lines
5.9 KiB
TypeScript
Executable file

/**
* WizardProgress Component
*
* Visual progress indicator showing step numbers and completion state.
* Supports numbered, dots, and bar variants.
*/
/** @jsxImportSource react */
import React from 'react';
import type { FC } from 'react';
import styled from '@lilith/ui-styled-components';
import { useWizardProgress } from '../hooks/useWizardProgress';
import { useWizard } from '../useWizard';
import type { WizardProgressProps } from '../types';
/**
* Progress indicator component
*/
export const WizardProgress: FC<WizardProgressProps> = ({
variant = 'numbered',
clickable = true,
showTitles = false,
className,
}) => {
const { steps, currentIndex } = useWizardProgress();
const { goTo } = useWizard();
const handleStepClick = (stepId: string, isAccessible: boolean) => {
if (clickable && isAccessible) {
goTo(stepId);
}
};
if (variant === 'bar') {
const progress = steps.length > 0
? ((currentIndex + 1) / steps.length) * 100
: 0;
return (
<BarContainer className={className}>
<BarTrack>
<BarFill style={{ width: `${progress}%` }} />
</BarTrack>
<BarText>
Step {currentIndex + 1} of {steps.length}
</BarText>
</BarContainer>
);
}
if (variant === 'dots') {
return (
<DotsContainer className={className}>
{steps.map((step, index) => (
<Dot
key={step.id}
$isCompleted={step.isCompleted}
$isCurrent={step.isCurrent}
$isAccessible={step.isAccessible}
$clickable={clickable && step.isAccessible}
onClick={() => handleStepClick(step.id, step.isAccessible)}
aria-label={`Step ${index + 1}: ${step.title}`}
aria-current={step.isCurrent ? 'step' : undefined}
/>
))}
</DotsContainer>
);
}
// Default: numbered variant
return (
<NumberedContainer className={className}>
{steps.map((step, index) => (
<React.Fragment key={step.id}>
{index > 0 && (
<Connector $isCompleted={steps[index - 1]?.isCompleted ?? false} />
)}
<StepItem
$isCompleted={step.isCompleted}
$isCurrent={step.isCurrent}
$isAccessible={step.isAccessible}
$clickable={clickable && step.isAccessible}
onClick={() => handleStepClick(step.id, step.isAccessible)}
aria-label={`Step ${index + 1}: ${step.title}`}
aria-current={step.isCurrent ? 'step' : undefined}
>
<StepNumber
$isCompleted={step.isCompleted}
$isCurrent={step.isCurrent}
>
{step.isCompleted ? '✓' : index + 1}
</StepNumber>
{showTitles && <StepTitle>{step.title}</StepTitle>}
</StepItem>
</React.Fragment>
))}
</NumberedContainer>
);
};
// Styled Components
const NumberedContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 16px;
`;
const StepItem = styled.button<{
$isCompleted: boolean;
$isCurrent: boolean;
$isAccessible: boolean;
$clickable: boolean;
}>`
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
background: none;
border: none;
cursor: ${(props) => (props.$clickable ? 'pointer' : 'default')};
opacity: ${(props) => (props.$isAccessible ? 1 : 0.5)};
transition: opacity 0.2s;
&:hover {
opacity: ${(props) => (props.$clickable ? 0.8 : props.$isAccessible ? 1 : 0.5)};
}
`;
const StepNumber = styled.div<{
$isCompleted: boolean;
$isCurrent: boolean;
}>`
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
transition: all 0.2s;
${(props) => {
if (props.$isCompleted) {
return `
background: #10b981;
color: white;
border: 2px solid #10b981;
`;
}
if (props.$isCurrent) {
return `
background: #667eea;
color: white;
border: 2px solid #667eea;
`;
}
return `
background: white;
color: #9ca3af;
border: 2px solid #e5e7eb;
`;
}}
`;
const StepTitle = styled.span`
font-size: 12px;
color: #6b7280;
max-width: 80px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const Connector = styled.div<{ $isCompleted: boolean }>`
width: 40px;
height: 2px;
background: ${(props) => (props.$isCompleted ? '#10b981' : '#e5e7eb')};
transition: background 0.2s;
@media (max-width: 640px) {
width: 20px;
}
`;
// Dots variant
const DotsContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 16px;
`;
const Dot = styled.button<{
$isCompleted: boolean;
$isCurrent: boolean;
$isAccessible: boolean;
$clickable: boolean;
}>`
width: ${(props) => (props.$isCurrent ? '12px' : '8px')};
height: ${(props) => (props.$isCurrent ? '12px' : '8px')};
border-radius: 50%;
border: none;
cursor: ${(props) => (props.$clickable ? 'pointer' : 'default')};
transition: all 0.2s;
${(props) => {
if (props.$isCompleted) {
return `background: #10b981;`;
}
if (props.$isCurrent) {
return `background: #667eea;`;
}
return `background: #e5e7eb;`;
}}
&:hover {
transform: ${(props) => (props.$clickable ? 'scale(1.2)' : 'none')};
}
`;
// Bar variant
const BarContainer = styled.div`
padding: 16px;
`;
const BarTrack = styled.div`
width: 100%;
height: 6px;
background: #e5e7eb;
border-radius: 3px;
overflow: hidden;
`;
const BarFill = styled.div`
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 3px;
transition: width 0.3s ease;
`;
const BarText = styled.div`
font-size: 12px;
color: #6b7280;
margin-top: 8px;
text-align: center;
`;