aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
402 lines (399 loc) • 16.8 kB
JavaScript
'use client';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { Check, AlertCircle, ChevronLeft, ChevronRight, Loader2, CheckCircle } from 'lucide-react';
import { useState, useCallback, createContext } from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { GlassButton } from '../button/GlassButton.js';
import '../button/GlassFab.js';
import '../button/GlassMagneticButton.js';
import { CardHeader, CardTitle, CardContent } from '../card/index.js';
import '../data-display/GlassAccordion.js';
import '../data-display/GlassAlert.js';
import '../data-display/GlassAvatar.js';
import { GlassBadge } from '../data-display/GlassBadge.js';
import '../data-display/GlassBadgeLine.js';
import '../data-display/GlassDataGrid.js';
import '../data-display/GlassDataTable.js';
import '../data-display/GlassHeatmap.js';
import '../data-display/GlassLoadingSkeleton.js';
import '../data-display/GlassProgress.js';
import '../data-display/GlassTimeline.js';
import '../data-display/GlassSkeleton.js';
import '../data-display/GlassNotificationCenter.js';
import '../data-display/GlassAnimatedNumber.js';
import { GlassCard } from '../card/GlassCard.js';
const FormContext = /*#__PURE__*/createContext(null);
/**
* GlassMultiStepForm component
* A comprehensive multi-step form with validation, progress tracking, and smooth transitions
*/
const GlassMultiStepForm = ({
steps,
initialData = {},
onSubmit,
onCancel,
onStepChange,
title,
description,
showProgress = true,
showNavigation = true,
allowSkip = false,
submitButtonText = "Submit",
cancelButtonText = "Cancel",
loading = false,
validationMode = "onChange",
showSummary = false,
className,
...props
}) => {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState(initialData);
const [stepValidations, setStepValidations] = useState({});
const [stepCompletions, setStepCompletions] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [validationErrors, setValidationErrors] = useState({});
// Update form data for a specific step
const updateFormData = useCallback((stepId, data) => {
setFormData(prev => ({
...prev,
[stepId]: data
}));
}, []);
// Check if step is valid
const isStepValid = useCallback(async stepIndex => {
const step = steps[stepIndex];
if (!step.validation) return true;
try {
const isValid = await step.validation(formData[step.id] || {});
setStepValidations(prev => ({
...prev,
[stepIndex]: isValid
}));
if (!isValid && step.validationMessage) {
setValidationErrors(prev => ({
...prev,
[stepIndex]: step.validationMessage
}));
} else {
setValidationErrors(prev => {
const newErrors = {
...prev
};
delete newErrors[stepIndex];
return newErrors;
});
}
return isValid;
} catch (error) {
console.error("Step validation error:", error);
return false;
}
}, [steps, formData]);
// Check if step is completed
const isStepCompleted = useCallback(stepIndex => {
return stepCompletions[stepIndex] || false;
}, [stepCompletions]);
// Navigate to specific step
const goToStep = useCallback(async stepIndex => {
if (stepIndex < 0 || stepIndex >= steps.length) return;
// Validate current step before moving
if (validationMode === "onChange") {
const isValid = await isStepValid(currentStep);
if (!isValid && !allowSkip) return;
}
setCurrentStep(stepIndex);
onStepChange?.(stepIndex, formData);
}, [steps.length, validationMode, isStepValid, currentStep, allowSkip, onStepChange, formData]);
// Go to next step
const nextStep = useCallback(async () => {
const isValid = await isStepValid(currentStep);
if (!isValid && !allowSkip) return;
if (currentStep < steps.length - 1) {
setStepCompletions(prev => ({
...prev,
[currentStep]: true
}));
setCurrentStep(prev => prev + 1);
onStepChange?.(currentStep + 1, formData);
}
}, [currentStep, steps.length, isStepValid, allowSkip, onStepChange, formData]);
// Go to previous step
const prevStep = useCallback(() => {
if (currentStep > 0) {
setCurrentStep(prev => prev + -1);
onStepChange?.(currentStep - 1, formData);
}
}, [currentStep, onStepChange, formData]);
// Handle form submission
const handleSubmit = useCallback(async () => {
// Validate all steps
let allValid = true;
for (let i = 0; i < steps.length; i++) {
const isValid = await isStepValid(i);
if (!isValid && !steps[i].optional) {
allValid = false;
goToStep(i);
break;
}
}
if (!allValid) return;
setIsSubmitting(true);
try {
await onSubmit?.(formData);
} catch (error) {
console.error("Form submission error:", error);
} finally {
setIsSubmitting(false);
}
}, [steps, isStepValid, goToStep, onSubmit, formData]);
// Handle form cancellation
const handleCancel = useCallback(() => {
onCancel?.();
}, [onCancel]);
// Get step progress percentage
const getProgressPercentage = useCallback(() => {
return (currentStep + 1) / steps.length * 100;
}, [currentStep, steps.length]);
// Context value
const contextValue = {
currentStep,
totalSteps: steps.length,
formData,
updateFormData,
goToStep,
nextStep,
prevStep,
isStepValid,
isStepCompleted,
validationMode
};
return jsx(FormContext.Provider, {
"data-glass-component": true,
value: contextValue,
children: jsx(MotionFramer, {
preset: "fadeIn",
className: 'glass-w-full max-w-4xl glass-mx-auto',
children: jsxs(GlassCard, {
className: cn("overflow-hidden", className),
...props,
children: [jsxs(CardHeader, {
className: 'pb-6',
children: [title && jsx(CardTitle, {
className: 'text-primary glass-text-2xl font-semibold text-center',
children: title
}), description && jsx("p", {
className: 'text-primary/70 text-center glass-mt-2',
children: description
}), showProgress && jsxs("div", {
className: 'mt-6',
role: "group",
"aria-label": "Form progress",
children: [jsxs("div", {
className: 'glass-flex glass-justify-between glass-items-center mb-2',
children: [jsxs("span", {
className: 'glass-text-sm text-primary/60',
"aria-live": "polite",
children: ["Step ", currentStep + 1, " of ", steps.length]
}), jsxs("span", {
className: 'glass-text-sm text-primary/60',
"aria-live": "polite",
children: [Math.round(getProgressPercentage()), "% Complete"]
})]
}), jsx("div", {
role: "progressbar",
"aria-valuenow": Math.round(getProgressPercentage()),
"aria-valuemin": 0,
"aria-valuemax": 100,
"aria-label": `Form progress: ${Math.round(getProgressPercentage())}% complete`,
className: 'glass-w-full glass-surface-subtle/10 glass-radius-full h-2 overflow-hidden',
children: jsx(MotionFramer, {
preset: "slideRight",
className: "glass-h-full glass-gradient-primary glass-gradient-primary glass-gradient-primary glass-radius-full",
style: {
width: `${getProgressPercentage()}%`
}
})
}), jsx("nav", {
"aria-label": "Form steps",
className: "glass-flex glass-justify-between glass-mt-4",
children: steps.map((step, index) => {
const isCompleted = isStepCompleted(index);
const isCurrent = index === currentStep;
const isValid = stepValidations[index] !== false;
const hasError = validationErrors[index];
return jsxs("div", {
role: "button",
"aria-label": `${step.title}${isCurrent ? " (current step)" : ""}${isCompleted ? " (completed)" : ""}${hasError ? " (has errors)" : ""}`,
"aria-current": isCurrent ? "step" : undefined,
tabIndex: 0,
className: cn("flex flex-col items-center cursor-pointer transition-all duration-200", "hover:scale-105", isCurrent && "scale-105", "glass-focus glass-touch-target glass-contrast-guard"),
onClick: e => goToStep(index),
onKeyDown: e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
goToStep(index);
}
},
children: [jsx("div", {
className: cn("w-10 h-10 glass-radius-full flex items-center justify-center border-2 transition-all duration-200", isCompleted && isValid && "bg-green-500 border-green-500 glass-text-primary", isCurrent && "bg-primary border-primary glass-text-primary", !isCompleted && !isCurrent && "bg-white/10 border-white/30 glass-text-primary/60", hasError && "border-red-500 bg-red-500/20"),
children: isCompleted && isValid ? jsx(Check, {
className: 'w-5 h-5',
"aria-hidden": "true"
}) : step.icon ? jsx("span", {
className: 'w-5 h-5',
"aria-hidden": "true",
children: step.icon
}) : jsx("span", {
className: 'glass-text-sm font-medium',
"aria-hidden": "true",
children: index + 1
})
}), jsxs("div", {
className: 'text-center glass-mt-2',
children: [jsx("p", {
className: cn("glass-text-xs font-medium transition-colors duration-200", isCurrent ? "glass-text-primary" : "glass-text-primary/60"),
children: step.title
}), step.description && jsx("p", {
className: 'glass-text-xs text-primary/50 glass-mt-1 max-w-20 truncate',
children: step.description
})]
})]
}, step.id);
})
})]
})]
}), jsxs(CardContent, {
className: 'pt-0',
children: [jsx("div", {
className: 'min-h-[400px]',
children: steps.map((step, index) => {
const StepComponent = step.component;
const isActive = index === currentStep;
return jsx(MotionFramer, {
preset: isActive ? "fadeIn" : "none",
className: cn("transition-opacity duration-300", isActive ? "opacity-100" : "opacity-0 absolute inset-0 pointer-events-none"),
children: isActive && jsxs("div", {
children: [jsxs("div", {
className: 'mb-6',
children: [jsx("h3", {
className: 'glass-text-xl font-semibold text-primary mb-2',
children: step.title
}), step.description && jsx("p", {
className: 'text-primary/70',
children: step.description
})]
}), jsx(StepComponent, {
data: formData[step.id] || {},
onChange: data => updateFormData(step.id, data),
validationMode: validationMode
}), validationErrors[index] && jsx("div", {
role: "alert",
"aria-live": "assertive",
className: "glass-mt-4 glass-p-3 glass-surface-red/20 glass-border glass-border-red/30 glass-radius-lg",
children: jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx(AlertCircle, {
className: 'w-4 h-4 text-primary',
"aria-hidden": "true"
}), jsx("span", {
className: "glass-text-secondary glass-text-sm",
children: validationErrors[index]
})]
})
})]
})
}, step.id);
})
}), showNavigation && jsxs("div", {
className: 'glass-flex glass-justify-between glass-items-center mt-8 pt-6 glass-border-t glass-border-white/10',
children: [jsxs("div", {
className: "glass-flex glass-gap-3",
children: [currentStep > 0 && jsxs(GlassButton, {
variant: "outline",
onClick: prevStep,
disabled: loading,
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx(ChevronLeft, {
className: 'w-4 h-4'
}), "Previous"]
}), onCancel && jsx(GlassButton, {
variant: "ghost",
onClick: handleCancel,
disabled: loading || isSubmitting,
children: cancelButtonText
})]
}), jsxs("div", {
className: "glass-flex glass-gap-3",
children: [allowSkip && steps[currentStep].optional && jsx(GlassButton, {
variant: "outline",
onClick: nextStep,
disabled: loading || isSubmitting,
children: "Skip Step"
}), currentStep < steps.length - 1 ? jsxs(GlassButton, {
variant: "primary",
onClick: nextStep,
disabled: loading || isSubmitting,
className: "glass-flex glass-items-center glass-gap-2",
children: ["Next", jsx(ChevronRight, {
className: 'w-4 h-4'
})]
}) : jsx(GlassButton, {
variant: "primary",
onClick: handleSubmit,
disabled: loading || isSubmitting,
className: "glass-flex glass-items-center glass-gap-2 glass-min-w-24",
children: isSubmitting ? jsxs(Fragment, {
children: [jsx(Loader2, {
className: 'w-4 h-4 animate-spin'
}), "Submitting..."]
}) : jsxs(Fragment, {
children: [jsx(CheckCircle, {
className: 'w-4 h-4'
}), submitButtonText]
})
})]
})]
}), showSummary && jsxs("div", {
className: 'mt-6 glass-p-4 glass-surface-subtle/5 glass-radius-lg',
children: [jsx("h4", {
className: 'glass-text-sm font-medium text-primary mb-3',
children: "Form Summary"
}), jsx("div", {
className: "glass-gap-2",
children: steps.map((step, index) => {
const isCompleted = isStepCompleted(index);
const hasData = formData[step.id];
return jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [isCompleted ? jsx(CheckCircle, {
className: 'w-4 h-4 text-primary'
}) : jsx("div", {
className: 'w-4 h-4 glass-radius-full glass-border-2 glass-border-white/30'
}), jsx("span", {
className: cn("glass-text-sm", isCompleted ? "glass-text-primary" : "glass-text-primary/60"),
children: step.title
})]
}), hasData && jsx(GlassBadge, {
variant: "secondary",
size: "sm",
children: "Data Saved"
})]
}, step.id);
})
})]
})]
})]
})
})
});
};
export { GlassMultiStepForm, GlassMultiStepForm as default };
//# sourceMappingURL=GlassMultiStepForm.js.map