@moontra/moonui-pro 2.8.1 → 2.8.3

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.8.1",
3
+ "version": "2.8.3",
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",
@@ -95,17 +95,76 @@ export const FormWizardProvider: React.FC<FormWizardProviderProps> = ({
95
95
 
96
96
  const validateCurrentStep = useCallback(async (): Promise<boolean> => {
97
97
  const step = steps[currentStep]
98
+
99
+ // First check HTML5 validation for required fields
100
+ if (typeof window !== 'undefined') {
101
+ const stepElement = document.querySelector('[data-wizard-step-content]')
102
+ if (stepElement) {
103
+ const requiredInputs = stepElement.querySelectorAll('input[required], select[required], textarea[required]')
104
+ const emptyFields: string[] = []
105
+
106
+ requiredInputs.forEach((input: Element) => {
107
+ const htmlInput = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
108
+ if (!htmlInput.value || htmlInput.value.trim() === '') {
109
+ const label = document.querySelector(`label[for="${htmlInput.id}"]`)
110
+ const fieldName = label ? (label.textContent?.replace(' *', '') || 'Field') : htmlInput.name || htmlInput.id || 'Field'
111
+ emptyFields.push(fieldName)
112
+ }
113
+ })
114
+
115
+ // Check for required checkboxes
116
+ const requiredCheckboxes = stepElement.querySelectorAll('input[type="checkbox"][required]')
117
+ requiredCheckboxes.forEach((checkbox: Element) => {
118
+ const htmlCheckbox = checkbox as HTMLInputElement
119
+ if (!htmlCheckbox.checked) {
120
+ const label = document.querySelector(`label[for="${htmlCheckbox.id}"]`)
121
+ const fieldName = label ? (label.textContent?.replace(' *', '') || 'Checkbox') : htmlCheckbox.name || htmlCheckbox.id || 'Checkbox'
122
+ emptyFields.push(fieldName)
123
+ }
124
+ })
125
+
126
+ if (emptyFields.length > 0) {
127
+ setError(`Please fill in the following required fields: ${emptyFields.join(', ')}`)
128
+ return false
129
+ }
130
+ }
131
+ }
132
+
133
+ // Then run custom validation if provided
98
134
  if (!step.validation) return true
99
135
 
100
136
  setIsLoading(true)
101
137
  setError(null)
102
138
 
103
139
  try {
104
- const isValid = await step.validation()
105
- if (!isValid) {
106
- setError("Please complete all required fields before proceeding")
140
+ const result = await step.validation()
141
+
142
+ // Handle boolean return
143
+ if (typeof result === 'boolean') {
144
+ if (!result) {
145
+ setError("Please complete all required fields before proceeding")
146
+ }
147
+ return result
107
148
  }
108
- return isValid
149
+
150
+ // Handle object return {isValid: boolean, error?: string, errors?: string[]}
151
+ if (typeof result === 'object' && result !== null && 'isValid' in result) {
152
+ const validationResult = result as { isValid: boolean; error?: string; errors?: string[] }
153
+ if (!validationResult.isValid) {
154
+ if (validationResult.error) {
155
+ setError(validationResult.error)
156
+ } else if (validationResult.errors && Array.isArray(validationResult.errors)) {
157
+ setError(validationResult.errors.join(', '))
158
+ } else {
159
+ setError("Please complete all required fields before proceeding")
160
+ }
161
+ }
162
+ return validationResult.isValid
163
+ }
164
+
165
+ // Invalid return type
166
+ setError("Invalid validation response")
167
+ return false
109
168
  } catch (err) {
110
169
  setError(err instanceof Error ? err.message : "Validation failed")
111
170
  return false
@@ -41,31 +41,7 @@ export const FormWizardProgress: React.FC<FormWizardProgressProps> = ({
41
41
  )}>
42
42
  {orientation === 'horizontal' ? (
43
43
  <div className="relative w-full">
44
- {/* Progress Line Background - Positioned absolutely */}
45
- <div
46
- className="absolute left-6 right-6 h-[2px] bg-gray-200 dark:bg-gray-700"
47
- style={{
48
- top: showStepTitles ? '80px' : '24px'
49
- }}
50
- />
51
-
52
- {/* Progress Line Fill */}
53
- <motion.div
54
- className="absolute left-6 h-[2px] bg-primary"
55
- style={{
56
- top: showStepTitles ? '80px' : '24px',
57
- right: `${100 - ((currentStep / (steps.length - 1)) * 100)}%`
58
- }}
59
- initial={{ right: '100%' }}
60
- animate={{
61
- right: steps.length > 1
62
- ? `${100 - ((currentStep / (steps.length - 1)) * 100)}%`
63
- : '100%'
64
- }}
65
- transition={{ duration: 0.5, ease: "easeInOut" }}
66
- />
67
-
68
- {/* Steps Container */}
44
+ {/* Steps Container with connecting lines */}
69
45
  <div className="relative flex items-start justify-between w-full">
70
46
  {steps.map((step, index) => {
71
47
  const isActive = index === currentStep
@@ -78,7 +54,7 @@ export const FormWizardProgress: React.FC<FormWizardProgressProps> = ({
78
54
  : step.icon
79
55
 
80
56
  return (
81
- <div key={step.id} className="relative flex flex-col items-center">
57
+ <div key={step.id} className="relative flex-1 flex flex-col items-center">
82
58
  {/* Step Title & Description - Above the circle */}
83
59
  {showStepTitles && (
84
60
  <div className="text-center mb-3 min-h-[40px] px-2">
@@ -98,35 +74,60 @@ export const FormWizardProgress: React.FC<FormWizardProgressProps> = ({
98
74
  </div>
99
75
  )}
100
76
 
101
- {/* Step Circle */}
102
- <motion.div
103
- initial={{ scale: 0.8 }}
104
- animate={{ scale: isActive ? 1.05 : 1 }}
105
- transition={{ duration: 0.2 }}
106
- className={cn(
107
- "relative z-10 flex items-center justify-center rounded-full transition-all duration-300",
108
- "w-12 h-12 border-2",
109
- isActive && "border-primary bg-primary text-primary-foreground shadow-lg shadow-primary/20",
110
- isCompleted && !isActive && "border-primary bg-primary text-primary-foreground",
111
- !isActive && !isCompleted && isAccessible && "border-gray-300 bg-background text-gray-500 hover:border-gray-400 dark:border-gray-600 dark:text-gray-400",
112
- !isAccessible && "border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed dark:border-gray-700 dark:bg-gray-900 dark:text-gray-600",
113
- hasError && "border-destructive bg-destructive text-destructive-foreground"
114
- )}
115
- >
116
- {hasError ? (
117
- errorStepIcon
118
- ) : isCompleted && !isActive ? (
119
- completedStepIcon
120
- ) : isActive && activeStepIcon ? (
121
- activeStepIcon
122
- ) : StepIcon ? (
123
- <span className="w-5 h-5 flex items-center justify-center">{StepIcon}</span>
124
- ) : showStepNumbers ? (
125
- <span className="text-sm font-semibold">{index + 1}</span>
126
- ) : (
127
- <Circle className="w-4 h-4" />
77
+ {/* Step Circle Container with line */}
78
+ <div className="relative flex items-center w-full">
79
+ {/* Connecting Line - Positioned after each circle except the last */}
80
+ {index < steps.length - 1 && (
81
+ <div
82
+ className="absolute left-1/2 w-full h-[2px]"
83
+ style={{
84
+ width: 'calc(100% + 24px)',
85
+ marginLeft: '24px'
86
+ }}
87
+ >
88
+ <div className="h-full bg-gray-200 dark:bg-gray-700" />
89
+ {index < currentStep && (
90
+ <motion.div
91
+ className="absolute inset-0 bg-primary"
92
+ initial={{ scaleX: 0 }}
93
+ animate={{ scaleX: 1 }}
94
+ transition={{ duration: 0.5, delay: index * 0.1 }}
95
+ style={{ transformOrigin: 'left' }}
96
+ />
97
+ )}
98
+ </div>
128
99
  )}
129
- </motion.div>
100
+
101
+ {/* Step Circle */}
102
+ <motion.div
103
+ initial={{ scale: 0.8 }}
104
+ animate={{ scale: isActive ? 1.05 : 1 }}
105
+ transition={{ duration: 0.2 }}
106
+ className={cn(
107
+ "relative z-10 flex items-center justify-center rounded-full transition-all duration-300 mx-auto",
108
+ "w-12 h-12 border-2",
109
+ isActive && "border-primary bg-primary text-primary-foreground shadow-lg shadow-primary/20",
110
+ isCompleted && !isActive && "border-primary bg-primary text-primary-foreground",
111
+ !isActive && !isCompleted && isAccessible && "border-gray-300 bg-background text-gray-500 hover:border-gray-400 dark:border-gray-600 dark:text-gray-400",
112
+ !isAccessible && "border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed dark:border-gray-700 dark:bg-gray-900 dark:text-gray-600",
113
+ hasError && "border-destructive bg-destructive text-destructive-foreground"
114
+ )}
115
+ >
116
+ {hasError ? (
117
+ errorStepIcon
118
+ ) : isCompleted && !isActive ? (
119
+ completedStepIcon
120
+ ) : isActive && activeStepIcon ? (
121
+ activeStepIcon
122
+ ) : StepIcon ? (
123
+ <span className="w-5 h-5 flex items-center justify-center">{StepIcon}</span>
124
+ ) : showStepNumbers ? (
125
+ <span className="text-sm font-semibold">{index + 1}</span>
126
+ ) : (
127
+ <Circle className="w-4 h-4" />
128
+ )}
129
+ </motion.div>
130
+ </div>
130
131
 
131
132
  {/* Step Label Below Circle */}
132
133
  <div className="mt-3">
@@ -5,6 +5,8 @@ import { motion, AnimatePresence } from "framer-motion"
5
5
  import { cn } from "../../lib/utils"
6
6
  import { useFormWizard } from "./form-wizard-context"
7
7
  import { FormWizardProps } from "./types"
8
+ import { Alert, AlertDescription } from "../ui/alert"
9
+ import { AlertCircle } from "lucide-react"
8
10
 
9
11
  interface FormWizardStepProps {
10
12
  className?: string
@@ -49,7 +51,7 @@ export const FormWizardStep: React.FC<FormWizardStepProps> = ({
49
51
  animationType = 'slide',
50
52
  animationDuration = 0.3
51
53
  }) => {
52
- const { steps, currentStep, goToNext, goToPrevious, goToStep, updateStepData, stepData } = useFormWizard()
54
+ const { steps, currentStep, goToNext, goToPrevious, goToStep, updateStepData, stepData, error } = useFormWizard()
53
55
  const [direction, setDirection] = React.useState(0)
54
56
  const previousStep = React.useRef(currentStep)
55
57
 
@@ -92,8 +94,17 @@ export const FormWizardStep: React.FC<FormWizardStepProps> = ({
92
94
  ease: "easeInOut"
93
95
  }}
94
96
  className={cn("w-full", className)}
97
+ data-wizard-step-content
95
98
  >
96
- {content}
99
+ <div className="space-y-4">
100
+ {error && (
101
+ <Alert variant="error">
102
+ <AlertCircle className="h-4 w-4" />
103
+ <AlertDescription>{error}</AlertDescription>
104
+ </Alert>
105
+ )}
106
+ {content}
107
+ </div>
97
108
  </motion.div>
98
109
  </AnimatePresence>
99
110
  )
@@ -89,8 +89,6 @@ export * from "./sidebar"
89
89
  // Form Wizard
90
90
  export * from "./form-wizard"
91
91
 
92
- // Multi-Step Form
93
- export * from "./multi-step-form"
94
92
 
95
93
  // Quiz Form
96
94
  export * from "./quiz-form"
@@ -1,223 +0,0 @@
1
- "use client"
2
-
3
- import React from "react"
4
- import { useForm, FormProvider, UseFormReturn, FieldValues, DefaultValues } from "react-hook-form"
5
- import { zodResolver } from "@hookform/resolvers/zod"
6
- import { z } from "zod"
7
- import { MoonUIFormWizardPro, FormWizardProps, WizardStep } from "../form-wizard"
8
- import { Alert, AlertDescription } from "../ui/alert"
9
- import { AlertCircle } from "lucide-react"
10
-
11
- export interface MultiStepFormField {
12
- name: string
13
- label: string
14
- type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'textarea' | 'select' | 'checkbox' | 'radio' | 'switch' | 'file' | 'date' | 'time' | 'datetime'
15
- placeholder?: string
16
- description?: string
17
- required?: boolean
18
- validation?: z.ZodSchema<any>
19
- options?: Array<{ value: string; label: string }>
20
- defaultValue?: any
21
- disabled?: boolean
22
- autoComplete?: string
23
- min?: number | string
24
- max?: number | string
25
- step?: number | string
26
- pattern?: string
27
- multiple?: boolean
28
- accept?: string
29
- }
30
-
31
- export interface MultiStepFormStep extends Omit<WizardStep, 'content' | 'validation'> {
32
- fields: MultiStepFormField[]
33
- schema?: z.ZodSchema<any>
34
- submitLabel?: string
35
- }
36
-
37
- export interface MultiStepFormProps<TFormData extends FieldValues = FieldValues> extends Omit<FormWizardProps, 'steps'> {
38
- steps: MultiStepFormStep[]
39
- onSubmit: (data: TFormData) => void | Promise<void>
40
- defaultValues?: DefaultValues<TFormData>
41
- showErrorSummary?: boolean
42
- fieldClassName?: string
43
- labelClassName?: string
44
- errorClassName?: string
45
- descriptionClassName?: string
46
- renderField?: (field: MultiStepFormField, form: UseFormReturn<TFormData>) => React.ReactNode
47
- }
48
-
49
- function createStepSchema(fields: MultiStepFormField[]): z.ZodSchema<any> {
50
- const shape: Record<string, z.ZodSchema<any>> = {}
51
-
52
- fields.forEach(field => {
53
- if (field.validation) {
54
- shape[field.name] = field.validation
55
- } else {
56
- let schema: z.ZodSchema<any> = z.any()
57
-
58
- switch (field.type) {
59
- case 'email':
60
- schema = z.string().email('Invalid email address')
61
- break
62
- case 'number':
63
- schema = z.number()
64
- if (field.min !== undefined) schema = (schema as z.ZodNumber).min(Number(field.min))
65
- if (field.max !== undefined) schema = (schema as z.ZodNumber).max(Number(field.max))
66
- break
67
- case 'url':
68
- schema = z.string().url('Invalid URL')
69
- break
70
- case 'tel':
71
- schema = z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number')
72
- break
73
- case 'checkbox':
74
- case 'switch':
75
- schema = z.boolean()
76
- break
77
- default:
78
- schema = z.string()
79
- if (field.min !== undefined) schema = (schema as z.ZodString).min(Number(field.min))
80
- if (field.max !== undefined) schema = (schema as z.ZodString).max(Number(field.max))
81
- if (field.pattern) schema = (schema as z.ZodString).regex(new RegExp(field.pattern))
82
- }
83
-
84
- if (!field.required) {
85
- schema = schema.optional()
86
- }
87
-
88
- shape[field.name] = schema
89
- }
90
- })
91
-
92
- return z.object(shape)
93
- }
94
-
95
- export function MoonUIMultiStepFormPro<TFormData extends FieldValues = FieldValues>({
96
- steps,
97
- onSubmit,
98
- defaultValues,
99
- showErrorSummary = true,
100
- fieldClassName,
101
- labelClassName,
102
- errorClassName,
103
- descriptionClassName,
104
- renderField,
105
- ...wizardProps
106
- }: MultiStepFormProps<TFormData>) {
107
- // Create combined schema from all steps
108
- const combinedSchema = React.useMemo(() => {
109
- const shape: Record<string, z.ZodSchema<any>> = {}
110
-
111
- steps.forEach(step => {
112
- const stepSchema = step.schema || createStepSchema(step.fields)
113
- if (stepSchema instanceof z.ZodObject) {
114
- Object.assign(shape, stepSchema.shape)
115
- }
116
- })
117
-
118
- return z.object(shape)
119
- }, [steps])
120
-
121
- const form = useForm<TFormData>({
122
- resolver: zodResolver(combinedSchema) as any,
123
- defaultValues,
124
- mode: 'onChange'
125
- })
126
-
127
- const wizardSteps: WizardStep[] = steps.map((step, stepIndex) => ({
128
- ...step,
129
- validation: async () => {
130
- // Validate only the fields in the current step
131
- const currentStepFields = step.fields.map(f => f.name)
132
- const result = await form.trigger(currentStepFields as any)
133
- return result
134
- },
135
- content: ({ updateStepData }) => {
136
- const errors = form.formState.errors
137
- const stepErrors = step.fields
138
- .map(field => errors[field.name])
139
- .filter(Boolean)
140
-
141
- return (
142
- <FormProvider {...form}>
143
- <form className="space-y-6">
144
- {showErrorSummary && stepErrors.length > 0 && (
145
- <Alert variant="error">
146
- <AlertCircle className="h-4 w-4" />
147
- <AlertDescription>
148
- Please fix the errors below before proceeding.
149
- </AlertDescription>
150
- </Alert>
151
- )}
152
-
153
- {step.fields.map(field => {
154
- if (renderField) {
155
- return <div key={field.name}>{renderField(field, form)}</div>
156
- }
157
-
158
- // Default field rendering
159
- return (
160
- <div key={field.name} className={fieldClassName}>
161
- <label
162
- htmlFor={field.name}
163
- className={cn(
164
- "block text-sm font-medium",
165
- labelClassName
166
- )}
167
- >
168
- {field.label}
169
- {field.required && <span className="text-destructive ml-1">*</span>}
170
- </label>
171
-
172
- {field.description && (
173
- <p className={cn("text-sm text-muted-foreground mt-1", descriptionClassName)}>
174
- {field.description}
175
- </p>
176
- )}
177
-
178
- <div className="mt-2">
179
- {/* Field rendering would go here - simplified for brevity */}
180
- <input
181
- {...form.register(field.name as any)}
182
- id={field.name}
183
- type={field.type}
184
- placeholder={field.placeholder}
185
- disabled={field.disabled}
186
- className="w-full px-3 py-2 border rounded-md"
187
- />
188
- </div>
189
-
190
- {errors[field.name] && (
191
- <p className={cn("text-sm text-destructive mt-1", errorClassName)}>
192
- {errors[field.name]?.message as string}
193
- </p>
194
- )}
195
- </div>
196
- )
197
- })}
198
- </form>
199
- </FormProvider>
200
- )
201
- }
202
- }))
203
-
204
- const handleComplete = async (stepData: Record<string, any>) => {
205
- const isValid = await form.trigger()
206
- if (isValid) {
207
- await onSubmit(form.getValues())
208
- }
209
- }
210
-
211
- return (
212
- <MoonUIFormWizardPro
213
- {...wizardProps}
214
- steps={wizardSteps}
215
- onComplete={handleComplete}
216
- />
217
- )
218
- }
219
-
220
- // Utility function for better imports
221
- function cn(...classes: (string | undefined | false)[]) {
222
- return classes.filter(Boolean).join(' ')
223
- }