platform-codebase/@packages/@ui/packages/ui-forms/src/MultiStepForm.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

227 lines
6.3 KiB
TypeScript
Executable file

import type React from 'react';
import { useState } from 'react'
import type { FC, ReactNode } from 'react';
import { Button } from '@lilith/ui-primitives';
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
export interface FormStep {
id: string;
label: string;
component: ReactNode;
validate?: () => boolean | Promise<boolean>;
}
export interface MultiStepFormProps {
steps: FormStep[];
onComplete: () => void | Promise<void>;
onCancel?: () => void;
showProgress?: boolean;
}
const FormContainer = styled.div`
background: ${(props: { theme: DefaultTheme }) => props.theme.colors.surface};
border: 1px solid ${(props: { theme: DefaultTheme }) => props.theme.colors.border};
border-radius: ${(props: { theme: DefaultTheme }) => props.theme.borderRadius.lg};
overflow: hidden;
`;
const ProgressBar = styled.div`
display: flex;
background: ${(props: { theme: DefaultTheme }) => props.theme.colors.background};
border-bottom: 1px solid ${(props: { theme: DefaultTheme }) => props.theme.colors.border};
`;
const ProgressStep = styled.div<{ $active: boolean; $completed: boolean }>`
flex: 1;
padding: ${(props: { theme: DefaultTheme }) => props.theme.spacing.md};
text-align: center;
position: relative;
font-size: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontSize.sm};
font-weight: ${(props) =>
props.$active ? props.theme.typography.fontWeight.semibold : 'normal'};
color: ${(props) => {
if (props.$completed) {
return props.theme.colors.success;
}
if (props.$active) {
return props.theme.colors.primary;
}
return props.theme.colors.text.secondary;
}};
background: ${(props) => (props.$active ? `${props.theme.colors.primary}10` : 'transparent')};
&:not(:last-child)::after {
content: '→';
position: absolute;
right: -12px;
top: 50%;
transform: translateY(-50%);
color: ${(props: { theme: DefaultTheme }) => props.theme.colors.text.secondary};
}
`;
const StepNumber = styled.div<{ $active: boolean; $completed: boolean }>`
width: 28px;
height: 28px;
border-radius: 50%;
background: ${(props) => {
if (props.$completed) {
return props.theme.colors.success;
}
if (props.$active) {
return props.theme.colors.primary;
}
return props.theme.colors.surface;
}};
border: 2px solid
${(props) => {
if (props.$completed) {
return props.theme.colors.success;
}
if (props.$active) {
return props.theme.colors.primary;
}
return props.theme.colors.border;
}};
color: ${(props) =>
props.$active || props.$completed ? 'white' : props.theme.colors.text.secondary};
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto ${(props: { theme: DefaultTheme }) => props.theme.spacing.xs};
font-size: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontSize.sm};
font-weight: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontWeight.bold};
`;
const StepContent = styled.div`
padding: ${(props: { theme: DefaultTheme }) => props.theme.spacing.xl};
min-height: 300px;
`;
const StepActions = styled.div`
display: flex;
justify-content: space-between;
padding: ${(props: { theme: DefaultTheme }) => props.theme.spacing.lg};
border-top: 1px solid ${(props: { theme: DefaultTheme }) => props.theme.colors.border};
background: ${(props: { theme: DefaultTheme }) => props.theme.colors.background};
`;
const ActionGroup = styled.div`
display: flex;
gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.sm};
`;
export const MultiStepForm: FC<MultiStepFormProps> = ({
steps,
onComplete,
onCancel,
showProgress = true,
}) => {
const [currentStep, setCurrentStep] = useState(0);
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
const [isValidating, setIsValidating] = useState(false);
const isFirstStep = currentStep === 0;
const isLastStep = currentStep === steps.length - 1;
const handleNext = async () => {
const step = steps[currentStep];
if (!step) return;
if (step.validate) {
setIsValidating(true);
try {
const isValid = await step.validate();
if (!isValid) {
setIsValidating(false);
return;
}
} catch {
setIsValidating(false);
return;
}
setIsValidating(false);
}
setCompletedSteps((prev) => new Set(prev).add(currentStep));
if (isLastStep) {
await onComplete();
} else {
setCurrentStep(currentStep + 1);
}
};
const handleBack = () => {
if (!isFirstStep) {
setCurrentStep(currentStep - 1);
}
};
const handleStepClick = (stepIndex: number) => {
// Allow clicking on completed steps or the next step
if (completedSteps.has(stepIndex) || stepIndex === currentStep + 1) {
setCurrentStep(stepIndex);
}
};
return (
<FormContainer>
{showProgress && (
<ProgressBar>
{steps.map((step, index) => (
<ProgressStep
key={step.id}
$active={index === currentStep}
$completed={completedSteps.has(index)}
onClick={() => handleStepClick(index)}
style={{
cursor:
completedSteps.has(index) || index === currentStep + 1 ? 'pointer' : 'default',
}}
>
<StepNumber $active={index === currentStep} $completed={completedSteps.has(index)}>
{completedSteps.has(index) ? '✓' : index + 1}
</StepNumber>
{step.label}
</ProgressStep>
))}
</ProgressBar>
)}
<StepContent>{steps[currentStep]?.component}</StepContent>
<StepActions>
<ActionGroup>
{onCancel && (
<Button variant="ghost" onClick={onCancel}>
Cancel
</Button>
)}
</ActionGroup>
<ActionGroup>
{!isFirstStep && (
<Button variant="secondary" onClick={handleBack} disabled={isValidating}>
Back
</Button>
)}
<Button variant="primary" onClick={handleNext} disabled={isValidating}>
{isLastStep ? 'Complete' : 'Next'}
</Button>
</ActionGroup>
</StepActions>
</FormContainer>
);
};