@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui-pro",
3
- "version": "2.6.0",
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
+ }