@moontra/moonui-pro 2.6.0 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +231 -21
- package/dist/index.mjs +14948 -2136
- package/package.json +1 -1
- package/src/components/form-wizard/form-wizard-context.tsx +248 -0
- package/src/components/form-wizard/form-wizard-navigation.tsx +118 -0
- package/src/components/form-wizard/form-wizard-progress.tsx +193 -0
- package/src/components/form-wizard/form-wizard-step.tsx +100 -0
- package/src/components/form-wizard/index.tsx +105 -0
- package/src/components/form-wizard/types.ts +76 -0
- package/src/components/index.ts +10 -1
- package/src/components/multi-step-form/index.tsx +223 -0
- package/src/components/quiz-form/index.tsx +479 -0
- package/src/components/sidebar/index.tsx +19 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moontra/moonui-pro",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Premium React components for MoonUI - Advanced UI library with 50+ pro components including performance, interactive, and gesture components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, useCallback, useEffect } from "react"
|
|
4
|
+
import { WizardContextValue, WizardStep, FormWizardProps } from "./types"
|
|
5
|
+
|
|
6
|
+
const FormWizardContext = createContext<WizardContextValue | null>(null)
|
|
7
|
+
|
|
8
|
+
export const useFormWizard = () => {
|
|
9
|
+
const context = useContext(FormWizardContext)
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error("useFormWizard must be used within a FormWizardProvider")
|
|
12
|
+
}
|
|
13
|
+
return context
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface FormWizardProviderProps {
|
|
17
|
+
children: React.ReactNode
|
|
18
|
+
steps: WizardStep[]
|
|
19
|
+
initialStep?: number
|
|
20
|
+
onStepChange?: FormWizardProps['onStepChange']
|
|
21
|
+
onComplete?: FormWizardProps['onComplete']
|
|
22
|
+
validateOnStepChange?: boolean
|
|
23
|
+
allowBackNavigation?: boolean
|
|
24
|
+
allowStepSkip?: boolean
|
|
25
|
+
autoSave?: boolean
|
|
26
|
+
autoSaveDelay?: number
|
|
27
|
+
onAutoSave?: FormWizardProps['onAutoSave']
|
|
28
|
+
persistData?: boolean
|
|
29
|
+
storageKey?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const FormWizardProvider: React.FC<FormWizardProviderProps> = ({
|
|
33
|
+
children,
|
|
34
|
+
steps,
|
|
35
|
+
initialStep = 0,
|
|
36
|
+
onStepChange,
|
|
37
|
+
onComplete,
|
|
38
|
+
validateOnStepChange = true,
|
|
39
|
+
allowBackNavigation = true,
|
|
40
|
+
allowStepSkip = false,
|
|
41
|
+
autoSave = false,
|
|
42
|
+
autoSaveDelay = 2000,
|
|
43
|
+
onAutoSave,
|
|
44
|
+
persistData = false,
|
|
45
|
+
storageKey = "form-wizard-data"
|
|
46
|
+
}) => {
|
|
47
|
+
const [currentStep, setCurrentStep] = useState(initialStep)
|
|
48
|
+
const [stepData, setStepData] = useState<Record<string, any>>({})
|
|
49
|
+
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set())
|
|
50
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
51
|
+
const [error, setError] = useState<string | null>(null)
|
|
52
|
+
|
|
53
|
+
// Load persisted data on mount
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (persistData && typeof window !== 'undefined') {
|
|
56
|
+
const savedData = localStorage.getItem(storageKey)
|
|
57
|
+
if (savedData) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(savedData)
|
|
60
|
+
setStepData(parsed.stepData || {})
|
|
61
|
+
setCurrentStep(parsed.currentStep || 0)
|
|
62
|
+
setCompletedSteps(new Set(parsed.completedSteps || []))
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error("Failed to load persisted wizard data:", e)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}, [persistData, storageKey])
|
|
69
|
+
|
|
70
|
+
// Auto-save functionality
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!autoSave || !onAutoSave) return
|
|
73
|
+
|
|
74
|
+
const timeoutId = setTimeout(() => {
|
|
75
|
+
const currentStepData = stepData[steps[currentStep].id]
|
|
76
|
+
if (currentStepData) {
|
|
77
|
+
onAutoSave(steps[currentStep].id, currentStepData)
|
|
78
|
+
}
|
|
79
|
+
}, autoSaveDelay)
|
|
80
|
+
|
|
81
|
+
return () => clearTimeout(timeoutId)
|
|
82
|
+
}, [stepData, currentStep, autoSave, autoSaveDelay, onAutoSave, steps])
|
|
83
|
+
|
|
84
|
+
// Persist data when it changes
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (persistData && typeof window !== 'undefined') {
|
|
87
|
+
const dataToSave = {
|
|
88
|
+
stepData,
|
|
89
|
+
currentStep,
|
|
90
|
+
completedSteps: Array.from(completedSteps)
|
|
91
|
+
}
|
|
92
|
+
localStorage.setItem(storageKey, JSON.stringify(dataToSave))
|
|
93
|
+
}
|
|
94
|
+
}, [stepData, currentStep, completedSteps, persistData, storageKey])
|
|
95
|
+
|
|
96
|
+
const validateCurrentStep = useCallback(async (): Promise<boolean> => {
|
|
97
|
+
const step = steps[currentStep]
|
|
98
|
+
if (!step.validation) return true
|
|
99
|
+
|
|
100
|
+
setIsLoading(true)
|
|
101
|
+
setError(null)
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const isValid = await step.validation()
|
|
105
|
+
if (!isValid) {
|
|
106
|
+
setError("Please complete all required fields before proceeding")
|
|
107
|
+
}
|
|
108
|
+
return isValid
|
|
109
|
+
} catch (err) {
|
|
110
|
+
setError(err instanceof Error ? err.message : "Validation failed")
|
|
111
|
+
return false
|
|
112
|
+
} finally {
|
|
113
|
+
setIsLoading(false)
|
|
114
|
+
}
|
|
115
|
+
}, [currentStep, steps])
|
|
116
|
+
|
|
117
|
+
const updateStepData = useCallback((stepId: string, data: any) => {
|
|
118
|
+
setStepData(prev => ({
|
|
119
|
+
...prev,
|
|
120
|
+
[stepId]: { ...prev[stepId], ...data }
|
|
121
|
+
}))
|
|
122
|
+
setError(null)
|
|
123
|
+
}, [])
|
|
124
|
+
|
|
125
|
+
const goToStep = useCallback(async (stepIndex: number) => {
|
|
126
|
+
if (stepIndex < 0 || stepIndex >= steps.length) return
|
|
127
|
+
if (stepIndex === currentStep) return
|
|
128
|
+
|
|
129
|
+
const targetStep = steps[stepIndex]
|
|
130
|
+
const isDisabled = typeof targetStep.isDisabled === 'function'
|
|
131
|
+
? targetStep.isDisabled(currentStep, steps)
|
|
132
|
+
: targetStep.isDisabled
|
|
133
|
+
|
|
134
|
+
if (isDisabled) return
|
|
135
|
+
|
|
136
|
+
// Validate current step if moving forward
|
|
137
|
+
if (validateOnStepChange && stepIndex > currentStep) {
|
|
138
|
+
const isValid = await validateCurrentStep()
|
|
139
|
+
if (!isValid) return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Call exit handler for current step
|
|
143
|
+
const currentStepObj = steps[currentStep]
|
|
144
|
+
if (currentStepObj.onExit) {
|
|
145
|
+
await currentStepObj.onExit()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Update completed steps
|
|
149
|
+
if (stepIndex > currentStep) {
|
|
150
|
+
setCompletedSteps(prev => new Set([...prev, currentStep]))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Call enter handler for new step
|
|
154
|
+
if (targetStep.onEnter) {
|
|
155
|
+
await targetStep.onEnter()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const previousStep = currentStep
|
|
159
|
+
setCurrentStep(stepIndex)
|
|
160
|
+
onStepChange?.(stepIndex, previousStep)
|
|
161
|
+
}, [currentStep, steps, validateOnStepChange, validateCurrentStep, onStepChange])
|
|
162
|
+
|
|
163
|
+
const goToNext = useCallback(() => {
|
|
164
|
+
goToStep(currentStep + 1)
|
|
165
|
+
}, [currentStep, goToStep])
|
|
166
|
+
|
|
167
|
+
const goToPrevious = useCallback(() => {
|
|
168
|
+
if (!allowBackNavigation) return
|
|
169
|
+
goToStep(currentStep - 1)
|
|
170
|
+
}, [currentStep, goToStep, allowBackNavigation])
|
|
171
|
+
|
|
172
|
+
const completeWizard = useCallback(async () => {
|
|
173
|
+
const isValid = await validateCurrentStep()
|
|
174
|
+
if (!isValid) return
|
|
175
|
+
|
|
176
|
+
setCompletedSteps(prev => new Set([...prev, currentStep]))
|
|
177
|
+
|
|
178
|
+
if (onComplete) {
|
|
179
|
+
setIsLoading(true)
|
|
180
|
+
try {
|
|
181
|
+
await onComplete(stepData)
|
|
182
|
+
|
|
183
|
+
// Clear persisted data after successful completion
|
|
184
|
+
if (persistData && typeof window !== 'undefined') {
|
|
185
|
+
localStorage.removeItem(storageKey)
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
setError(err instanceof Error ? err.message : "Failed to complete wizard")
|
|
189
|
+
} finally {
|
|
190
|
+
setIsLoading(false)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}, [currentStep, stepData, validateCurrentStep, onComplete, persistData, storageKey])
|
|
194
|
+
|
|
195
|
+
const resetWizard = useCallback(() => {
|
|
196
|
+
setCurrentStep(0)
|
|
197
|
+
setStepData({})
|
|
198
|
+
setCompletedSteps(new Set())
|
|
199
|
+
setError(null)
|
|
200
|
+
|
|
201
|
+
if (persistData && typeof window !== 'undefined') {
|
|
202
|
+
localStorage.removeItem(storageKey)
|
|
203
|
+
}
|
|
204
|
+
}, [persistData, storageKey])
|
|
205
|
+
|
|
206
|
+
const isStepCompleted = useCallback((stepIndex: number) => {
|
|
207
|
+
return completedSteps.has(stepIndex)
|
|
208
|
+
}, [completedSteps])
|
|
209
|
+
|
|
210
|
+
const isStepAccessible = useCallback((stepIndex: number) => {
|
|
211
|
+
if (stepIndex === 0) return true
|
|
212
|
+
if (allowStepSkip) return true
|
|
213
|
+
|
|
214
|
+
// Check if all previous steps are completed
|
|
215
|
+
for (let i = 0; i < stepIndex; i++) {
|
|
216
|
+
if (!completedSteps.has(i) && !steps[i].isOptional) {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return true
|
|
222
|
+
}, [completedSteps, allowStepSkip, steps])
|
|
223
|
+
|
|
224
|
+
const value: WizardContextValue = {
|
|
225
|
+
steps,
|
|
226
|
+
currentStep,
|
|
227
|
+
stepData,
|
|
228
|
+
isLoading,
|
|
229
|
+
error,
|
|
230
|
+
goToNext,
|
|
231
|
+
goToPrevious,
|
|
232
|
+
goToStep,
|
|
233
|
+
updateStepData: (data: any) => updateStepData(steps[currentStep].id, data),
|
|
234
|
+
validateCurrentStep,
|
|
235
|
+
completeWizard,
|
|
236
|
+
resetWizard,
|
|
237
|
+
isStepCompleted,
|
|
238
|
+
isStepAccessible,
|
|
239
|
+
canGoNext: currentStep < steps.length - 1,
|
|
240
|
+
canGoPrevious: allowBackNavigation && currentStep > 0
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<FormWizardContext.Provider value={value}>
|
|
245
|
+
{children}
|
|
246
|
+
</FormWizardContext.Provider>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { motion } from "framer-motion"
|
|
5
|
+
import { ChevronLeft, ChevronRight, Check, Loader2 } from "lucide-react"
|
|
6
|
+
import { Button } from "../ui/button"
|
|
7
|
+
import { cn } from "../../lib/utils"
|
|
8
|
+
import { useFormWizard } from "./form-wizard-context"
|
|
9
|
+
|
|
10
|
+
interface FormWizardNavigationProps {
|
|
11
|
+
className?: string
|
|
12
|
+
showStepIndicator?: boolean
|
|
13
|
+
previousText?: string
|
|
14
|
+
nextText?: string
|
|
15
|
+
completeText?: string
|
|
16
|
+
loadingText?: string
|
|
17
|
+
onPreviousClick?: () => void
|
|
18
|
+
onNextClick?: () => void
|
|
19
|
+
onCompleteClick?: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const FormWizardNavigation: React.FC<FormWizardNavigationProps> = ({
|
|
23
|
+
className,
|
|
24
|
+
showStepIndicator = true,
|
|
25
|
+
previousText = "Previous",
|
|
26
|
+
nextText = "Next",
|
|
27
|
+
completeText = "Complete",
|
|
28
|
+
loadingText = "Processing...",
|
|
29
|
+
onPreviousClick,
|
|
30
|
+
onNextClick,
|
|
31
|
+
onCompleteClick
|
|
32
|
+
}) => {
|
|
33
|
+
const {
|
|
34
|
+
steps,
|
|
35
|
+
currentStep,
|
|
36
|
+
isLoading,
|
|
37
|
+
canGoNext,
|
|
38
|
+
canGoPrevious,
|
|
39
|
+
goToNext,
|
|
40
|
+
goToPrevious,
|
|
41
|
+
completeWizard
|
|
42
|
+
} = useFormWizard()
|
|
43
|
+
|
|
44
|
+
const isLastStep = currentStep === steps.length - 1
|
|
45
|
+
const currentStepObj = steps[currentStep]
|
|
46
|
+
|
|
47
|
+
const handlePrevious = () => {
|
|
48
|
+
onPreviousClick?.()
|
|
49
|
+
goToPrevious()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleNext = () => {
|
|
53
|
+
if (isLastStep) {
|
|
54
|
+
onCompleteClick?.()
|
|
55
|
+
completeWizard()
|
|
56
|
+
} else {
|
|
57
|
+
onNextClick?.()
|
|
58
|
+
goToNext()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={cn("flex items-center justify-between", className)}>
|
|
64
|
+
<div className="flex items-center gap-2">
|
|
65
|
+
<Button
|
|
66
|
+
type="button"
|
|
67
|
+
variant="outline"
|
|
68
|
+
onClick={handlePrevious}
|
|
69
|
+
disabled={!canGoPrevious || isLoading}
|
|
70
|
+
className={cn(
|
|
71
|
+
"transition-all duration-200",
|
|
72
|
+
!canGoPrevious && "invisible"
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
<ChevronLeft className="w-4 h-4 mr-1" />
|
|
76
|
+
{previousText}
|
|
77
|
+
</Button>
|
|
78
|
+
|
|
79
|
+
{showStepIndicator && (
|
|
80
|
+
<motion.div
|
|
81
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
82
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
83
|
+
className="px-3 py-1 text-sm text-muted-foreground"
|
|
84
|
+
>
|
|
85
|
+
Step {currentStep + 1} of {steps.length}
|
|
86
|
+
{currentStepObj.isOptional && (
|
|
87
|
+
<span className="ml-1 text-xs">(Optional)</span>
|
|
88
|
+
)}
|
|
89
|
+
</motion.div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<Button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={handleNext}
|
|
96
|
+
disabled={isLoading || (!canGoNext && !isLastStep)}
|
|
97
|
+
className="min-w-[120px]"
|
|
98
|
+
>
|
|
99
|
+
{isLoading ? (
|
|
100
|
+
<>
|
|
101
|
+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
102
|
+
{loadingText}
|
|
103
|
+
</>
|
|
104
|
+
) : isLastStep ? (
|
|
105
|
+
<>
|
|
106
|
+
<Check className="w-4 h-4 mr-2" />
|
|
107
|
+
{completeText}
|
|
108
|
+
</>
|
|
109
|
+
) : (
|
|
110
|
+
<>
|
|
111
|
+
{nextText}
|
|
112
|
+
<ChevronRight className="w-4 h-4 ml-1" />
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</Button>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { motion } from "framer-motion"
|
|
5
|
+
import { CheckCircle, Circle, XCircle } from "lucide-react"
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
import { useFormWizard } from "./form-wizard-context"
|
|
8
|
+
import { FormWizardProps } from "./types"
|
|
9
|
+
|
|
10
|
+
interface FormWizardProgressProps {
|
|
11
|
+
className?: string
|
|
12
|
+
progressType?: FormWizardProps['progressType']
|
|
13
|
+
orientation?: FormWizardProps['orientation']
|
|
14
|
+
showStepNumbers?: boolean
|
|
15
|
+
showStepTitles?: boolean
|
|
16
|
+
stepIconPosition?: FormWizardProps['stepIconPosition']
|
|
17
|
+
completedStepIcon?: React.ReactNode
|
|
18
|
+
activeStepIcon?: React.ReactNode
|
|
19
|
+
errorStepIcon?: React.ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const FormWizardProgress: React.FC<FormWizardProgressProps> = ({
|
|
23
|
+
className,
|
|
24
|
+
progressType = 'linear',
|
|
25
|
+
orientation = 'horizontal',
|
|
26
|
+
showStepNumbers = true,
|
|
27
|
+
showStepTitles = true,
|
|
28
|
+
stepIconPosition = 'top',
|
|
29
|
+
completedStepIcon = <CheckCircle className="w-5 h-5" />,
|
|
30
|
+
activeStepIcon,
|
|
31
|
+
errorStepIcon = <XCircle className="w-5 h-5" />
|
|
32
|
+
}) => {
|
|
33
|
+
const { steps, currentStep, isStepCompleted, isStepAccessible, error } = useFormWizard()
|
|
34
|
+
|
|
35
|
+
if (progressType === 'linear') {
|
|
36
|
+
return (
|
|
37
|
+
<div className={cn(
|
|
38
|
+
"relative",
|
|
39
|
+
orientation === 'vertical' ? "flex flex-col space-y-8" : "flex items-center justify-between",
|
|
40
|
+
className
|
|
41
|
+
)}>
|
|
42
|
+
{steps.map((step, index) => {
|
|
43
|
+
const isActive = index === currentStep
|
|
44
|
+
const isCompleted = isStepCompleted(index)
|
|
45
|
+
const isAccessible = isStepAccessible(index)
|
|
46
|
+
const hasError = isActive && error
|
|
47
|
+
|
|
48
|
+
const StepIcon = typeof step.icon === 'function'
|
|
49
|
+
? step.icon({ isActive, isCompleted })
|
|
50
|
+
: step.icon
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<React.Fragment key={step.id}>
|
|
54
|
+
<div className={cn(
|
|
55
|
+
"relative flex items-center",
|
|
56
|
+
orientation === 'vertical' ? "w-full" : "flex-1"
|
|
57
|
+
)}>
|
|
58
|
+
<motion.div
|
|
59
|
+
initial={{ scale: 0.8 }}
|
|
60
|
+
animate={{ scale: isActive ? 1.1 : 1 }}
|
|
61
|
+
className={cn(
|
|
62
|
+
"relative z-20 flex items-center justify-center rounded-full border-2 transition-all duration-300",
|
|
63
|
+
stepIconPosition === 'inside' ? "w-12 h-12" : "w-10 h-10",
|
|
64
|
+
isActive && "border-primary bg-primary text-primary-foreground shadow-lg",
|
|
65
|
+
isCompleted && !isActive && "border-primary bg-primary text-primary-foreground",
|
|
66
|
+
!isActive && !isCompleted && isAccessible && "border-muted-foreground/50 bg-background text-muted-foreground hover:border-muted-foreground",
|
|
67
|
+
!isAccessible && "border-muted bg-muted text-muted-foreground cursor-not-allowed opacity-50",
|
|
68
|
+
hasError && "border-destructive bg-destructive text-destructive-foreground"
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{hasError ? (
|
|
72
|
+
errorStepIcon
|
|
73
|
+
) : isCompleted && !isActive ? (
|
|
74
|
+
completedStepIcon
|
|
75
|
+
) : isActive && activeStepIcon ? (
|
|
76
|
+
activeStepIcon
|
|
77
|
+
) : StepIcon ? (
|
|
78
|
+
<span className="w-5 h-5 flex items-center justify-center">{StepIcon}</span>
|
|
79
|
+
) : showStepNumbers ? (
|
|
80
|
+
<span className="text-sm font-medium">{index + 1}</span>
|
|
81
|
+
) : (
|
|
82
|
+
<Circle className="w-4 h-4" />
|
|
83
|
+
)}
|
|
84
|
+
</motion.div>
|
|
85
|
+
|
|
86
|
+
{showStepTitles && (
|
|
87
|
+
<div className={cn(
|
|
88
|
+
"absolute whitespace-nowrap",
|
|
89
|
+
orientation === 'vertical'
|
|
90
|
+
? "left-16 top-1/2 -translate-y-1/2"
|
|
91
|
+
: "top-full mt-2 left-1/2 -translate-x-1/2 text-center"
|
|
92
|
+
)}>
|
|
93
|
+
<p className={cn(
|
|
94
|
+
"text-sm font-medium transition-colors",
|
|
95
|
+
isActive && "text-primary",
|
|
96
|
+
isCompleted && !isActive && "text-primary",
|
|
97
|
+
!isActive && !isCompleted && "text-muted-foreground"
|
|
98
|
+
)}>
|
|
99
|
+
{step.title}
|
|
100
|
+
</p>
|
|
101
|
+
{step.description && (
|
|
102
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
103
|
+
{step.description}
|
|
104
|
+
</p>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{index < steps.length - 1 && (
|
|
110
|
+
<div className={cn(
|
|
111
|
+
"absolute transition-all duration-500",
|
|
112
|
+
orientation === 'vertical'
|
|
113
|
+
? "top-12 left-5 w-0.5 h-8"
|
|
114
|
+
: "left-12 right-0 top-1/2 -translate-y-1/2 h-0.5",
|
|
115
|
+
isCompleted ? "bg-primary" : "bg-muted"
|
|
116
|
+
)} />
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</React.Fragment>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (progressType === 'dots') {
|
|
127
|
+
return (
|
|
128
|
+
<div className={cn("flex items-center justify-center space-x-2", className)}>
|
|
129
|
+
{steps.map((_, index) => {
|
|
130
|
+
const isActive = index === currentStep
|
|
131
|
+
const isCompleted = isStepCompleted(index)
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<motion.div
|
|
135
|
+
key={index}
|
|
136
|
+
initial={{ scale: 0.8 }}
|
|
137
|
+
animate={{ scale: isActive ? 1.2 : 1 }}
|
|
138
|
+
className={cn(
|
|
139
|
+
"rounded-full transition-all duration-300",
|
|
140
|
+
isActive ? "w-3 h-3 bg-primary" : "w-2 h-2",
|
|
141
|
+
isCompleted && !isActive && "bg-primary/60",
|
|
142
|
+
!isActive && !isCompleted && "bg-muted"
|
|
143
|
+
)}
|
|
144
|
+
/>
|
|
145
|
+
)
|
|
146
|
+
})}
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (progressType === 'circular') {
|
|
152
|
+
const progress = ((currentStep + 1) / steps.length) * 100
|
|
153
|
+
const circumference = 2 * Math.PI * 40 // radius = 40
|
|
154
|
+
const strokeDashoffset = circumference - (progress / 100) * circumference
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className={cn("relative w-32 h-32", className)}>
|
|
158
|
+
<svg className="w-full h-full -rotate-90">
|
|
159
|
+
<circle
|
|
160
|
+
cx="64"
|
|
161
|
+
cy="64"
|
|
162
|
+
r="40"
|
|
163
|
+
fill="none"
|
|
164
|
+
stroke="currentColor"
|
|
165
|
+
strokeWidth="8"
|
|
166
|
+
className="text-muted"
|
|
167
|
+
/>
|
|
168
|
+
<motion.circle
|
|
169
|
+
cx="64"
|
|
170
|
+
cy="64"
|
|
171
|
+
r="40"
|
|
172
|
+
fill="none"
|
|
173
|
+
stroke="currentColor"
|
|
174
|
+
strokeWidth="8"
|
|
175
|
+
strokeDasharray={circumference}
|
|
176
|
+
initial={{ strokeDashoffset: circumference }}
|
|
177
|
+
animate={{ strokeDashoffset }}
|
|
178
|
+
className="text-primary"
|
|
179
|
+
transition={{ duration: 0.5 }}
|
|
180
|
+
/>
|
|
181
|
+
</svg>
|
|
182
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
183
|
+
<div className="text-center">
|
|
184
|
+
<p className="text-2xl font-bold">{currentStep + 1}</p>
|
|
185
|
+
<p className="text-sm text-muted-foreground">of {steps.length}</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
import { useFormWizard } from "./form-wizard-context"
|
|
7
|
+
import { FormWizardProps } from "./types"
|
|
8
|
+
|
|
9
|
+
interface FormWizardStepProps {
|
|
10
|
+
className?: string
|
|
11
|
+
animationType?: FormWizardProps['animationType']
|
|
12
|
+
animationDuration?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const animations = {
|
|
16
|
+
slide: {
|
|
17
|
+
initial: (direction: number) => ({
|
|
18
|
+
x: direction > 0 ? 1000 : -1000,
|
|
19
|
+
opacity: 0
|
|
20
|
+
}),
|
|
21
|
+
animate: {
|
|
22
|
+
x: 0,
|
|
23
|
+
opacity: 1
|
|
24
|
+
},
|
|
25
|
+
exit: (direction: number) => ({
|
|
26
|
+
x: direction < 0 ? 1000 : -1000,
|
|
27
|
+
opacity: 0
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
fade: {
|
|
31
|
+
initial: { opacity: 0 },
|
|
32
|
+
animate: { opacity: 1 },
|
|
33
|
+
exit: { opacity: 0 }
|
|
34
|
+
},
|
|
35
|
+
scale: {
|
|
36
|
+
initial: { opacity: 0, scale: 0.8 },
|
|
37
|
+
animate: { opacity: 1, scale: 1 },
|
|
38
|
+
exit: { opacity: 0, scale: 0.8 }
|
|
39
|
+
},
|
|
40
|
+
none: {
|
|
41
|
+
initial: {},
|
|
42
|
+
animate: {},
|
|
43
|
+
exit: {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const FormWizardStep: React.FC<FormWizardStepProps> = ({
|
|
48
|
+
className,
|
|
49
|
+
animationType = 'slide',
|
|
50
|
+
animationDuration = 0.3
|
|
51
|
+
}) => {
|
|
52
|
+
const { steps, currentStep, goToNext, goToPrevious, goToStep, updateStepData, stepData } = useFormWizard()
|
|
53
|
+
const [direction, setDirection] = React.useState(0)
|
|
54
|
+
const previousStep = React.useRef(currentStep)
|
|
55
|
+
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
setDirection(currentStep > previousStep.current ? 1 : -1)
|
|
58
|
+
previousStep.current = currentStep
|
|
59
|
+
}, [currentStep])
|
|
60
|
+
|
|
61
|
+
const currentStepObj = steps[currentStep]
|
|
62
|
+
const isFirstStep = currentStep === 0
|
|
63
|
+
const isLastStep = currentStep === steps.length - 1
|
|
64
|
+
|
|
65
|
+
const stepContentProps = {
|
|
66
|
+
currentStep,
|
|
67
|
+
totalSteps: steps.length,
|
|
68
|
+
goToNext,
|
|
69
|
+
goToPrevious,
|
|
70
|
+
goToStep,
|
|
71
|
+
isFirstStep,
|
|
72
|
+
isLastStep,
|
|
73
|
+
stepData: stepData[currentStepObj.id] || {},
|
|
74
|
+
updateStepData: (data: any) => updateStepData(currentStepObj.id, data)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const content = typeof currentStepObj.content === 'function'
|
|
78
|
+
? currentStepObj.content(stepContentProps)
|
|
79
|
+
: currentStepObj.content
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<AnimatePresence mode="wait" custom={direction}>
|
|
83
|
+
<motion.div
|
|
84
|
+
key={currentStep}
|
|
85
|
+
custom={direction}
|
|
86
|
+
variants={animations[animationType]}
|
|
87
|
+
initial="initial"
|
|
88
|
+
animate="animate"
|
|
89
|
+
exit="exit"
|
|
90
|
+
transition={{
|
|
91
|
+
duration: animationDuration,
|
|
92
|
+
ease: "easeInOut"
|
|
93
|
+
}}
|
|
94
|
+
className={cn("w-full", className)}
|
|
95
|
+
>
|
|
96
|
+
{content}
|
|
97
|
+
</motion.div>
|
|
98
|
+
</AnimatePresence>
|
|
99
|
+
)
|
|
100
|
+
}
|