UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

402 lines (399 loc) 16.8 kB
'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