@moontra/moonui-pro 2.6.1 → 2.7.0

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.
@@ -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
+ }
@@ -0,0 +1,105 @@
1
+ "use client"
2
+
3
+ import React from "react"
4
+ import { cn } from "../../lib/utils"
5
+ import { FormWizardProvider } from "./form-wizard-context"
6
+ import { FormWizardProgress } from "./form-wizard-progress"
7
+ import { FormWizardStep } from "./form-wizard-step"
8
+ import { FormWizardNavigation } from "./form-wizard-navigation"
9
+ import { FormWizardProps } from "./types"
10
+ import { Card, CardContent } from "../ui/card"
11
+ import { Separator } from "../ui/separator"
12
+
13
+ export const MoonUIFormWizardPro = React.forwardRef<HTMLDivElement, FormWizardProps>(({
14
+ steps,
15
+ currentStep = 0,
16
+ onStepChange,
17
+ onComplete,
18
+ orientation = 'horizontal',
19
+ progressType = 'linear',
20
+ allowStepSkip = false,
21
+ allowBackNavigation = true,
22
+ validateOnStepChange = true,
23
+ animationType = 'slide',
24
+ animationDuration = 0.3,
25
+ showStepNumbers = true,
26
+ showStepTitles = true,
27
+ showProgressBar = true,
28
+ stepIconPosition = 'top',
29
+ completedStepIcon,
30
+ activeStepIcon,
31
+ errorStepIcon,
32
+ className,
33
+ progressClassName,
34
+ navigationClassName,
35
+ contentClassName,
36
+ stepClassName,
37
+ autoSave = false,
38
+ autoSaveDelay = 2000,
39
+ onAutoSave,
40
+ persistData = false,
41
+ storageKey = "form-wizard-data"
42
+ }, ref) => {
43
+ return (
44
+ <FormWizardProvider
45
+ steps={steps}
46
+ initialStep={currentStep}
47
+ onStepChange={onStepChange}
48
+ onComplete={onComplete}
49
+ validateOnStepChange={validateOnStepChange}
50
+ allowBackNavigation={allowBackNavigation}
51
+ allowStepSkip={allowStepSkip}
52
+ autoSave={autoSave}
53
+ autoSaveDelay={autoSaveDelay}
54
+ onAutoSave={onAutoSave}
55
+ persistData={persistData}
56
+ storageKey={storageKey}
57
+ >
58
+ <div ref={ref} className={cn("w-full", className)}>
59
+ {showProgressBar && (
60
+ <>
61
+ <FormWizardProgress
62
+ className={progressClassName}
63
+ progressType={progressType}
64
+ orientation={orientation}
65
+ showStepNumbers={showStepNumbers}
66
+ showStepTitles={showStepTitles}
67
+ stepIconPosition={stepIconPosition}
68
+ completedStepIcon={completedStepIcon}
69
+ activeStepIcon={activeStepIcon}
70
+ errorStepIcon={errorStepIcon}
71
+ />
72
+ <Separator className="my-8" />
73
+ </>
74
+ )}
75
+
76
+ <Card className={cn("border-0 shadow-none", contentClassName)}>
77
+ <CardContent className="p-0">
78
+ <FormWizardStep
79
+ className={stepClassName}
80
+ animationType={animationType}
81
+ animationDuration={animationDuration}
82
+ />
83
+ </CardContent>
84
+ </Card>
85
+
86
+ <Separator className="my-8" />
87
+
88
+ <FormWizardNavigation
89
+ className={navigationClassName}
90
+ />
91
+ </div>
92
+ </FormWizardProvider>
93
+ )
94
+ })
95
+
96
+ MoonUIFormWizardPro.displayName = "MoonUIFormWizardPro"
97
+
98
+ // Export types and hooks
99
+ export type { FormWizardProps, WizardStep, WizardStepContentProps } from "./types"
100
+ export { useFormWizard } from "./form-wizard-context"
101
+
102
+ // Export sub-components for advanced usage
103
+ export { FormWizardProgress } from "./form-wizard-progress"
104
+ export { FormWizardStep } from "./form-wizard-step"
105
+ export { FormWizardNavigation } from "./form-wizard-navigation"
@@ -0,0 +1,76 @@
1
+ import { ReactNode } from "react"
2
+
3
+ export interface WizardStep {
4
+ id: string
5
+ title: string
6
+ description?: string
7
+ icon?: ReactNode | ((props: { isActive: boolean; isCompleted: boolean }) => ReactNode)
8
+ content: ReactNode | ((props: WizardStepContentProps) => ReactNode)
9
+ validation?: () => boolean | Promise<boolean>
10
+ isOptional?: boolean
11
+ isDisabled?: boolean | ((currentStep: number, steps: WizardStep[]) => boolean)
12
+ onEnter?: () => void | Promise<void>
13
+ onExit?: () => void | Promise<void>
14
+ }
15
+
16
+ export interface WizardStepContentProps {
17
+ currentStep: number
18
+ totalSteps: number
19
+ goToNext: () => void
20
+ goToPrevious: () => void
21
+ goToStep: (step: number) => void
22
+ isFirstStep: boolean
23
+ isLastStep: boolean
24
+ stepData: any
25
+ updateStepData: (data: any) => void
26
+ }
27
+
28
+ export interface FormWizardProps {
29
+ steps: WizardStep[]
30
+ currentStep?: number
31
+ onStepChange?: (step: number, previousStep: number) => void
32
+ onComplete?: (data: Record<string, any>) => void | Promise<void>
33
+ orientation?: 'horizontal' | 'vertical'
34
+ progressType?: 'linear' | 'circular' | 'dots' | 'custom'
35
+ allowStepSkip?: boolean
36
+ allowBackNavigation?: boolean
37
+ validateOnStepChange?: boolean
38
+ animationType?: 'slide' | 'fade' | 'scale' | 'none'
39
+ animationDuration?: number
40
+ showStepNumbers?: boolean
41
+ showStepTitles?: boolean
42
+ showProgressBar?: boolean
43
+ stepIconPosition?: 'top' | 'left' | 'inside'
44
+ completedStepIcon?: ReactNode
45
+ activeStepIcon?: ReactNode
46
+ errorStepIcon?: ReactNode
47
+ className?: string
48
+ progressClassName?: string
49
+ navigationClassName?: string
50
+ contentClassName?: string
51
+ stepClassName?: string
52
+ autoSave?: boolean
53
+ autoSaveDelay?: number
54
+ onAutoSave?: (stepId: string, data: any) => void | Promise<void>
55
+ persistData?: boolean
56
+ storageKey?: string
57
+ }
58
+
59
+ export interface WizardContextValue {
60
+ steps: WizardStep[]
61
+ currentStep: number
62
+ stepData: Record<string, any>
63
+ isLoading: boolean
64
+ error: string | null
65
+ goToNext: () => void
66
+ goToPrevious: () => void
67
+ goToStep: (step: number) => void
68
+ updateStepData: (stepId: string, data: any) => void
69
+ validateCurrentStep: () => Promise<boolean>
70
+ completeWizard: () => void
71
+ resetWizard: () => void
72
+ isStepCompleted: (stepIndex: number) => boolean
73
+ isStepAccessible: (stepIndex: number) => boolean
74
+ canGoNext: boolean
75
+ canGoPrevious: boolean
76
+ }
@@ -84,4 +84,19 @@ export * from "./file-upload"
84
84
  export * from "./data-table"
85
85
 
86
86
  // Sidebar
87
- export * from "./sidebar"
87
+ export * from "./sidebar"
88
+
89
+ // Form Wizard
90
+ export * from "./form-wizard"
91
+
92
+ // Multi-Step Form
93
+ export * from "./multi-step-form"
94
+
95
+ // Quiz Form
96
+ export * from "./quiz-form"
97
+
98
+ // Credit Card Input
99
+ export * from "./credit-card-input"
100
+
101
+ // Phone Number Input
102
+ export * from "./phone-number-input"