@moontra/moonui-pro 2.20.2 → 2.20.4
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 +8 -3
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postinstall.js +191 -23
- package/src/components/advanced-chart/index.tsx +0 -1246
- package/src/components/advanced-forms/index.tsx +0 -585
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -377
- package/src/components/calendar/index.tsx +0 -1220
- package/src/components/calendar-pro/index.tsx +0 -1697
- package/src/components/color-picker/index.tsx +0 -432
- package/src/components/credit-card-input/index.tsx +0 -406
- package/src/components/dashboard/dashboard-grid.tsx +0 -480
- package/src/components/dashboard/demo.tsx +0 -425
- package/src/components/dashboard/index.tsx +0 -1046
- package/src/components/dashboard/time-range-picker.tsx +0 -336
- package/src/components/dashboard/types.ts +0 -225
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
- package/src/components/dashboard/widgets/index.ts +0 -5
- package/src/components/dashboard/widgets/metric-card.tsx +0 -363
- package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
- package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
- package/src/components/data-table/data-table-column-toggle.tsx +0 -169
- package/src/components/data-table/data-table-export.ts +0 -156
- package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
- package/src/components/data-table/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/error-boundary/index.tsx +0 -232
- package/src/components/file-upload/index.tsx +0 -1660
- package/src/components/floating-action-button/index.tsx +0 -206
- package/src/components/form-wizard/form-wizard-context.tsx +0 -335
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
- package/src/components/form-wizard/form-wizard-step.tsx +0 -111
- package/src/components/form-wizard/index.tsx +0 -102
- package/src/components/form-wizard/types.ts +0 -77
- package/src/components/gesture-drawer/index.tsx +0 -551
- package/src/components/github-stars/github-api.ts +0 -426
- package/src/components/github-stars/hooks.ts +0 -517
- package/src/components/github-stars/index.tsx +0 -375
- package/src/components/github-stars/types.ts +0 -148
- package/src/components/github-stars/variants.tsx +0 -515
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -529
- package/src/components/index.ts +0 -130
- package/src/components/internal/index.ts +0 -78
- package/src/components/kanban/add-card-modal.tsx +0 -502
- package/src/components/kanban/card-detail-modal.tsx +0 -761
- package/src/components/kanban/index.ts +0 -13
- package/src/components/kanban/kanban.tsx +0 -1689
- package/src/components/kanban/types.ts +0 -168
- package/src/components/lazy-component/index.tsx +0 -823
- package/src/components/license-error/index.tsx +0 -31
- package/src/components/magnetic-button/index.tsx +0 -216
- package/src/components/memory-efficient-data/index.tsx +0 -1018
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/navbar/index.tsx +0 -781
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -613
- package/src/components/performance-monitor/index.tsx +0 -808
- package/src/components/phone-number-input/index.tsx +0 -343
- package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
- package/src/components/pinch-zoom/index.tsx +0 -566
- package/src/components/quiz-form/index.tsx +0 -479
- package/src/components/rich-text-editor/index.tsx +0 -2322
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
- package/src/components/rich-text-editor/slash-commands.css +0 -35
- package/src/components/rich-text-editor/table-styles.css +0 -65
- package/src/components/sidebar/index.tsx +0 -884
- package/src/components/spotlight-card/index.tsx +0 -191
- package/src/components/swipeable-card/index.tsx +0 -100
- package/src/components/timeline/index.tsx +0 -1183
- package/src/components/ui/accordion.tsx +0 -581
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/alert.tsx +0 -141
- package/src/components/ui/aspect-ratio.tsx +0 -245
- package/src/components/ui/avatar.tsx +0 -155
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/breadcrumb.tsx +0 -216
- package/src/components/ui/button.tsx +0 -228
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -216
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -631
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -948
- package/src/components/ui/dialog.tsx +0 -752
- package/src/components/ui/dropdown-menu.tsx +0 -706
- package/src/components/ui/gesture-drawer.tsx +0 -11
- package/src/components/ui/hover-card.tsx +0 -29
- package/src/components/ui/index.ts +0 -222
- package/src/components/ui/input.tsx +0 -224
- package/src/components/ui/label.tsx +0 -29
- package/src/components/ui/lightbox.tsx +0 -606
- package/src/components/ui/magnetic-button.tsx +0 -129
- package/src/components/ui/media-gallery.tsx +0 -611
- package/src/components/ui/navigation-menu.tsx +0 -130
- package/src/components/ui/pagination.tsx +0 -125
- package/src/components/ui/popover.tsx +0 -185
- package/src/components/ui/progress.tsx +0 -30
- package/src/components/ui/radio-group.tsx +0 -257
- package/src/components/ui/scroll-area.tsx +0 -47
- package/src/components/ui/select.tsx +0 -378
- package/src/components/ui/separator.tsx +0 -145
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/skeleton.tsx +0 -20
- package/src/components/ui/slider.tsx +0 -354
- package/src/components/ui/spotlight-card.tsx +0 -119
- package/src/components/ui/switch.tsx +0 -86
- package/src/components/ui/table.tsx +0 -331
- package/src/components/ui/tabs-pro.tsx +0 -542
- package/src/components/ui/tabs.tsx +0 -54
- package/src/components/ui/textarea.tsx +0 -28
- package/src/components/ui/toast.tsx +0 -317
- package/src/components/ui/toggle.tsx +0 -119
- package/src/components/ui/tooltip.tsx +0 -151
- package/src/components/virtual-list/index.tsx +0 -668
- package/src/hooks/use-chart.ts +0 -205
- package/src/hooks/use-data-table.ts +0 -182
- package/src/hooks/use-docs-pro-access.ts +0 -13
- package/src/hooks/use-license-check.ts +0 -65
- package/src/hooks/use-subscription.ts +0 -19
- package/src/hooks/use-toast.ts +0 -15
- package/src/index.ts +0 -22
- package/src/lib/ai-providers.ts +0 -377
- package/src/lib/component-metadata.ts +0 -18
- package/src/lib/micro-interactions.ts +0 -255
- package/src/lib/paddle.ts +0 -17
- package/src/lib/utils.ts +0 -6
- package/src/patterns/login-form/index.tsx +0 -276
- package/src/patterns/login-form/types.ts +0 -67
- package/src/setupTests.ts +0 -41
- package/src/styles/advanced-chart.css +0 -239
- package/src/styles/calendar.css +0 -35
- package/src/styles/design-system.css +0 -363
- package/src/styles/index.css +0 -681
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- package/src/types/next-auth.d.ts +0 -21
- package/src/use-intersection-observer.tsx +0 -154
- package/src/use-local-storage.tsx +0 -71
- package/src/use-paddle.ts +0 -138
- package/src/use-performance-optimizer.ts +0 -389
- package/src/use-pro-access.ts +0 -141
- package/src/use-scroll-animation.ts +0 -219
- package/src/use-subscription.ts +0 -37
- package/src/use-toast.ts +0 -32
- package/src/utils/chart-helpers.ts +0 -357
- package/src/utils/cn.ts +0 -6
- package/src/utils/data-processing.ts +0 -151
- package/src/utils/license-validator.tsx +0 -183
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React, { useState } from "react"
|
|
4
|
-
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
-
import { Plus, X, Lock, Sparkles } from "lucide-react"
|
|
6
|
-
import { cn } from "../../lib/utils"
|
|
7
|
-
import { Card, CardContent } from "../ui/card"
|
|
8
|
-
import { Button } from "../ui/button"
|
|
9
|
-
import { useSubscription } from "../../hooks/use-subscription"
|
|
10
|
-
|
|
11
|
-
export interface FloatingActionButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
12
|
-
actions?: Array<{
|
|
13
|
-
icon: React.ReactNode
|
|
14
|
-
label: string
|
|
15
|
-
onClick: () => void
|
|
16
|
-
}>
|
|
17
|
-
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left"
|
|
18
|
-
size?: "sm" | "default" | "lg"
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const FloatingActionButtonInternal = React.forwardRef<HTMLButtonElement, FloatingActionButtonProps>(
|
|
22
|
-
({
|
|
23
|
-
actions = [],
|
|
24
|
-
position = "bottom-right",
|
|
25
|
-
size = "default",
|
|
26
|
-
className,
|
|
27
|
-
children,
|
|
28
|
-
onClick,
|
|
29
|
-
...props
|
|
30
|
-
}, ref) => {
|
|
31
|
-
const [isOpen, setIsOpen] = useState(false)
|
|
32
|
-
|
|
33
|
-
const sizeClasses = {
|
|
34
|
-
sm: "h-10 w-10",
|
|
35
|
-
default: "h-12 w-12",
|
|
36
|
-
lg: "h-14 w-14"
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const positionClasses = {
|
|
40
|
-
"bottom-right": "bottom-4 right-4",
|
|
41
|
-
"bottom-left": "bottom-4 left-4",
|
|
42
|
-
"top-right": "top-4 right-4",
|
|
43
|
-
"top-left": "top-4 left-4"
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const actionPositions = {
|
|
47
|
-
"bottom-right": { x: -60, y: 0 },
|
|
48
|
-
"bottom-left": { x: 60, y: 0 },
|
|
49
|
-
"top-right": { x: -60, y: 0 },
|
|
50
|
-
"top-left": { x: 60, y: 0 }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const handleMainClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
54
|
-
if (actions.length > 0) {
|
|
55
|
-
setIsOpen(!isOpen)
|
|
56
|
-
} else if (onClick) {
|
|
57
|
-
onClick(e)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<div className={cn("fixed z-50", positionClasses[position])}>
|
|
63
|
-
{/* Action buttons */}
|
|
64
|
-
<AnimatePresence>
|
|
65
|
-
{isOpen && actions.length > 0 && (
|
|
66
|
-
<div className="absolute">
|
|
67
|
-
{actions.map((action, index) => (
|
|
68
|
-
<motion.div
|
|
69
|
-
key={index}
|
|
70
|
-
initial={{
|
|
71
|
-
scale: 0,
|
|
72
|
-
opacity: 0,
|
|
73
|
-
x: 0,
|
|
74
|
-
y: 0
|
|
75
|
-
}}
|
|
76
|
-
animate={{
|
|
77
|
-
scale: 1,
|
|
78
|
-
opacity: 1,
|
|
79
|
-
x: actionPositions[position].x,
|
|
80
|
-
y: actionPositions[position].y * (index + 1) - 10
|
|
81
|
-
}}
|
|
82
|
-
exit={{
|
|
83
|
-
scale: 0,
|
|
84
|
-
opacity: 0,
|
|
85
|
-
x: 0,
|
|
86
|
-
y: 0
|
|
87
|
-
}}
|
|
88
|
-
transition={{
|
|
89
|
-
delay: index * 0.05,
|
|
90
|
-
type: "spring",
|
|
91
|
-
stiffness: 200,
|
|
92
|
-
damping: 15
|
|
93
|
-
}}
|
|
94
|
-
className="absolute flex items-center gap-2"
|
|
95
|
-
style={{
|
|
96
|
-
transform: `translate(${position.includes('right') ? '0' : '0'}, ${position.includes('bottom') ? '0' : '0'})`
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
{/* Label */}
|
|
100
|
-
<motion.div
|
|
101
|
-
initial={{ opacity: 0, scale: 0.8 }}
|
|
102
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
103
|
-
exit={{ opacity: 0, scale: 0.8 }}
|
|
104
|
-
transition={{ delay: (index + 1) * 0.05 }}
|
|
105
|
-
className={cn(
|
|
106
|
-
"px-2 py-1 bg-background/90 backdrop-blur-sm text-sm font-medium rounded-md shadow-sm border whitespace-nowrap",
|
|
107
|
-
position.includes('right') ? 'mr-2' : 'ml-2'
|
|
108
|
-
)}
|
|
109
|
-
>
|
|
110
|
-
{action.label}
|
|
111
|
-
</motion.div>
|
|
112
|
-
|
|
113
|
-
{/* Action button */}
|
|
114
|
-
<button
|
|
115
|
-
onClick={() => {
|
|
116
|
-
action.onClick()
|
|
117
|
-
setIsOpen(false)
|
|
118
|
-
}}
|
|
119
|
-
className={cn(
|
|
120
|
-
"inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
121
|
-
sizeClasses[size]
|
|
122
|
-
)}
|
|
123
|
-
>
|
|
124
|
-
{action.icon}
|
|
125
|
-
</button>
|
|
126
|
-
</motion.div>
|
|
127
|
-
))}
|
|
128
|
-
</div>
|
|
129
|
-
)}
|
|
130
|
-
</AnimatePresence>
|
|
131
|
-
|
|
132
|
-
{/* Main FAB */}
|
|
133
|
-
<motion.button
|
|
134
|
-
ref={ref}
|
|
135
|
-
className={cn(
|
|
136
|
-
"inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
137
|
-
sizeClasses[size],
|
|
138
|
-
className
|
|
139
|
-
)}
|
|
140
|
-
onClick={handleMainClick}
|
|
141
|
-
whileHover={{ scale: 1.1 }}
|
|
142
|
-
whileTap={{ scale: 0.9 }}
|
|
143
|
-
animate={{ rotate: isOpen ? 45 : 0 }}
|
|
144
|
-
transition={{ duration: 0.2 }}
|
|
145
|
-
>
|
|
146
|
-
{children || (actions.length > 0 ? <Plus className="h-5 w-5" /> : <Plus className="h-5 w-5" />)}
|
|
147
|
-
</motion.button>
|
|
148
|
-
|
|
149
|
-
{/* Backdrop */}
|
|
150
|
-
<AnimatePresence>
|
|
151
|
-
{isOpen && (
|
|
152
|
-
<motion.div
|
|
153
|
-
initial={{ opacity: 0 }}
|
|
154
|
-
animate={{ opacity: 1 }}
|
|
155
|
-
exit={{ opacity: 0 }}
|
|
156
|
-
className="fixed inset-0 z-[-1]"
|
|
157
|
-
onClick={() => setIsOpen(false)}
|
|
158
|
-
/>
|
|
159
|
-
)}
|
|
160
|
-
</AnimatePresence>
|
|
161
|
-
</div>
|
|
162
|
-
)
|
|
163
|
-
}
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
FloatingActionButtonInternal.displayName = "FloatingActionButtonInternal"
|
|
167
|
-
|
|
168
|
-
export const FloatingActionButton = React.forwardRef<HTMLButtonElement, FloatingActionButtonProps>(
|
|
169
|
-
({ className, ...props }, ref) => {
|
|
170
|
-
// Check if we're in docs mode or have pro access
|
|
171
|
-
const { hasProAccess, isLoading } = useSubscription()
|
|
172
|
-
|
|
173
|
-
// In docs mode, always show the component
|
|
174
|
-
|
|
175
|
-
// If not in docs mode and no pro access, show upgrade prompt
|
|
176
|
-
if (!isLoading && !hasProAccess) {
|
|
177
|
-
return (
|
|
178
|
-
<Card className={cn("w-fit", className)}>
|
|
179
|
-
<CardContent className="py-6 text-center">
|
|
180
|
-
<div className="space-y-4">
|
|
181
|
-
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
182
|
-
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
183
|
-
</div>
|
|
184
|
-
<div>
|
|
185
|
-
<h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
|
|
186
|
-
<p className="text-muted-foreground text-xs mb-4">
|
|
187
|
-
Floating Action Button is available exclusively to MoonUI Pro subscribers.
|
|
188
|
-
</p>
|
|
189
|
-
<a href="/pricing">
|
|
190
|
-
<Button size="sm">
|
|
191
|
-
<Sparkles className="mr-2 h-4 w-4" />
|
|
192
|
-
Upgrade to Pro
|
|
193
|
-
</Button>
|
|
194
|
-
</a>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
</CardContent>
|
|
198
|
-
</Card>
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return <FloatingActionButtonInternal className={className} ref={ref} {...props} />
|
|
203
|
-
}
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
FloatingActionButton.displayName = "FloatingActionButton"
|
|
@@ -1,335 +0,0 @@
|
|
|
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
|
-
|
|
99
|
-
// Check if automatic validation should be performed
|
|
100
|
-
const shouldValidateFields = step.requiredFields !== false
|
|
101
|
-
|
|
102
|
-
// First check HTML5 validation for required fields if enabled
|
|
103
|
-
if (shouldValidateFields && typeof window !== 'undefined') {
|
|
104
|
-
const stepElement = document.querySelector('[data-wizard-step-content]')
|
|
105
|
-
if (stepElement) {
|
|
106
|
-
// If requiredFields is an array, only check those specific fields
|
|
107
|
-
if (Array.isArray(step.requiredFields)) {
|
|
108
|
-
const emptyFields: string[] = []
|
|
109
|
-
|
|
110
|
-
step.requiredFields.forEach((fieldName) => {
|
|
111
|
-
const field = stepElement.querySelector(`[name="${fieldName}"], [id="${fieldName}"]`)
|
|
112
|
-
if (field) {
|
|
113
|
-
const htmlField = field as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
114
|
-
if (field.tagName === 'INPUT' && (field as HTMLInputElement).type === 'checkbox') {
|
|
115
|
-
if (!(field as HTMLInputElement).checked) {
|
|
116
|
-
emptyFields.push(fieldName)
|
|
117
|
-
}
|
|
118
|
-
} else if (!htmlField.value || htmlField.value.trim() === '') {
|
|
119
|
-
emptyFields.push(fieldName)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
if (emptyFields.length > 0) {
|
|
125
|
-
setError(`Please fill in the following required fields: ${emptyFields.join(', ')}`)
|
|
126
|
-
return false
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
// Default behavior: check all required fields
|
|
130
|
-
const requiredInputs = stepElement.querySelectorAll('input[required], select[required], textarea[required]')
|
|
131
|
-
const emptyFields: string[] = []
|
|
132
|
-
|
|
133
|
-
requiredInputs.forEach((input: Element) => {
|
|
134
|
-
const htmlInput = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
135
|
-
if (!htmlInput.value || htmlInput.value.trim() === '') {
|
|
136
|
-
const label = document.querySelector(`label[for="${htmlInput.id}"]`)
|
|
137
|
-
const fieldName = label ? (label.textContent?.replace(' *', '') || 'Field') : htmlInput.name || htmlInput.id || 'Field'
|
|
138
|
-
emptyFields.push(fieldName)
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
// Check for required checkboxes
|
|
143
|
-
const requiredCheckboxes = stepElement.querySelectorAll('input[type="checkbox"][required]')
|
|
144
|
-
requiredCheckboxes.forEach((checkbox: Element) => {
|
|
145
|
-
const htmlCheckbox = checkbox as HTMLInputElement
|
|
146
|
-
if (!htmlCheckbox.checked) {
|
|
147
|
-
const label = document.querySelector(`label[for="${htmlCheckbox.id}"]`)
|
|
148
|
-
const fieldName = label ? (label.textContent?.replace(' *', '') || 'Checkbox') : htmlCheckbox.name || htmlCheckbox.id || 'Checkbox'
|
|
149
|
-
emptyFields.push(fieldName)
|
|
150
|
-
}
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
if (emptyFields.length > 0) {
|
|
154
|
-
setError(`Please fill in the following required fields: ${emptyFields.join(', ')}`)
|
|
155
|
-
return false
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Then run custom validation if provided
|
|
162
|
-
if (!step.validation) return true
|
|
163
|
-
|
|
164
|
-
setIsLoading(true)
|
|
165
|
-
setError(null)
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const result = await step.validation()
|
|
169
|
-
|
|
170
|
-
// Handle boolean return
|
|
171
|
-
if (typeof result === 'boolean') {
|
|
172
|
-
if (!result) {
|
|
173
|
-
setError("Please complete all required fields before proceeding")
|
|
174
|
-
}
|
|
175
|
-
return result
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Handle object return {isValid: boolean, error?: string, errors?: string[]}
|
|
179
|
-
if (typeof result === 'object' && result !== null && 'isValid' in result) {
|
|
180
|
-
const validationResult = result as { isValid: boolean; error?: string; errors?: string[] }
|
|
181
|
-
if (!validationResult.isValid) {
|
|
182
|
-
if (validationResult.error) {
|
|
183
|
-
setError(validationResult.error)
|
|
184
|
-
} else if (validationResult.errors && Array.isArray(validationResult.errors)) {
|
|
185
|
-
setError(validationResult.errors.join(', '))
|
|
186
|
-
} else {
|
|
187
|
-
setError("Please complete all required fields before proceeding")
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return validationResult.isValid
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Invalid return type
|
|
194
|
-
setError("Invalid validation response")
|
|
195
|
-
return false
|
|
196
|
-
} catch (err) {
|
|
197
|
-
setError(err instanceof Error ? err.message : "Validation failed")
|
|
198
|
-
return false
|
|
199
|
-
} finally {
|
|
200
|
-
setIsLoading(false)
|
|
201
|
-
}
|
|
202
|
-
}, [currentStep, steps])
|
|
203
|
-
|
|
204
|
-
const updateStepData = useCallback((stepId: string, data: any) => {
|
|
205
|
-
setStepData(prev => ({
|
|
206
|
-
...prev,
|
|
207
|
-
[stepId]: { ...prev[stepId], ...data }
|
|
208
|
-
}))
|
|
209
|
-
setError(null)
|
|
210
|
-
}, [])
|
|
211
|
-
|
|
212
|
-
const goToStep = useCallback(async (stepIndex: number) => {
|
|
213
|
-
if (stepIndex < 0 || stepIndex >= steps.length) return
|
|
214
|
-
if (stepIndex === currentStep) return
|
|
215
|
-
|
|
216
|
-
const targetStep = steps[stepIndex]
|
|
217
|
-
const isDisabled = typeof targetStep.isDisabled === 'function'
|
|
218
|
-
? targetStep.isDisabled(currentStep, steps)
|
|
219
|
-
: targetStep.isDisabled
|
|
220
|
-
|
|
221
|
-
if (isDisabled) return
|
|
222
|
-
|
|
223
|
-
// Validate current step if moving forward
|
|
224
|
-
if (validateOnStepChange && stepIndex > currentStep) {
|
|
225
|
-
const isValid = await validateCurrentStep()
|
|
226
|
-
if (!isValid) return
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Call exit handler for current step
|
|
230
|
-
const currentStepObj = steps[currentStep]
|
|
231
|
-
if (currentStepObj.onExit) {
|
|
232
|
-
await currentStepObj.onExit()
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Update completed steps
|
|
236
|
-
if (stepIndex > currentStep) {
|
|
237
|
-
setCompletedSteps(prev => new Set([...prev, currentStep]))
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Call enter handler for new step
|
|
241
|
-
if (targetStep.onEnter) {
|
|
242
|
-
await targetStep.onEnter()
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const previousStep = currentStep
|
|
246
|
-
setCurrentStep(stepIndex)
|
|
247
|
-
onStepChange?.(stepIndex, previousStep)
|
|
248
|
-
}, [currentStep, steps, validateOnStepChange, validateCurrentStep, onStepChange])
|
|
249
|
-
|
|
250
|
-
const goToNext = useCallback(() => {
|
|
251
|
-
goToStep(currentStep + 1)
|
|
252
|
-
}, [currentStep, goToStep])
|
|
253
|
-
|
|
254
|
-
const goToPrevious = useCallback(() => {
|
|
255
|
-
if (!allowBackNavigation) return
|
|
256
|
-
goToStep(currentStep - 1)
|
|
257
|
-
}, [currentStep, goToStep, allowBackNavigation])
|
|
258
|
-
|
|
259
|
-
const completeWizard = useCallback(async () => {
|
|
260
|
-
const isValid = await validateCurrentStep()
|
|
261
|
-
if (!isValid) return
|
|
262
|
-
|
|
263
|
-
setCompletedSteps(prev => new Set([...prev, currentStep]))
|
|
264
|
-
|
|
265
|
-
if (onComplete) {
|
|
266
|
-
setIsLoading(true)
|
|
267
|
-
try {
|
|
268
|
-
await onComplete(stepData)
|
|
269
|
-
|
|
270
|
-
// Clear persisted data after successful completion
|
|
271
|
-
if (persistData && typeof window !== 'undefined') {
|
|
272
|
-
localStorage.removeItem(storageKey)
|
|
273
|
-
}
|
|
274
|
-
} catch (err) {
|
|
275
|
-
setError(err instanceof Error ? err.message : "Failed to complete wizard")
|
|
276
|
-
} finally {
|
|
277
|
-
setIsLoading(false)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}, [currentStep, stepData, validateCurrentStep, onComplete, persistData, storageKey])
|
|
281
|
-
|
|
282
|
-
const resetWizard = useCallback(() => {
|
|
283
|
-
setCurrentStep(0)
|
|
284
|
-
setStepData({})
|
|
285
|
-
setCompletedSteps(new Set())
|
|
286
|
-
setError(null)
|
|
287
|
-
|
|
288
|
-
if (persistData && typeof window !== 'undefined') {
|
|
289
|
-
localStorage.removeItem(storageKey)
|
|
290
|
-
}
|
|
291
|
-
}, [persistData, storageKey])
|
|
292
|
-
|
|
293
|
-
const isStepCompleted = useCallback((stepIndex: number) => {
|
|
294
|
-
return completedSteps.has(stepIndex)
|
|
295
|
-
}, [completedSteps])
|
|
296
|
-
|
|
297
|
-
const isStepAccessible = useCallback((stepIndex: number) => {
|
|
298
|
-
if (stepIndex === 0) return true
|
|
299
|
-
if (allowStepSkip) return true
|
|
300
|
-
|
|
301
|
-
// Check if all previous steps are completed
|
|
302
|
-
for (let i = 0; i < stepIndex; i++) {
|
|
303
|
-
if (!completedSteps.has(i) && !steps[i].isOptional) {
|
|
304
|
-
return false
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return true
|
|
309
|
-
}, [completedSteps, allowStepSkip, steps])
|
|
310
|
-
|
|
311
|
-
const value: WizardContextValue = {
|
|
312
|
-
steps,
|
|
313
|
-
currentStep,
|
|
314
|
-
stepData,
|
|
315
|
-
isLoading,
|
|
316
|
-
error,
|
|
317
|
-
goToNext,
|
|
318
|
-
goToPrevious,
|
|
319
|
-
goToStep,
|
|
320
|
-
updateStepData: (data: any) => updateStepData(steps[currentStep].id, data),
|
|
321
|
-
validateCurrentStep,
|
|
322
|
-
completeWizard,
|
|
323
|
-
resetWizard,
|
|
324
|
-
isStepCompleted,
|
|
325
|
-
isStepAccessible,
|
|
326
|
-
canGoNext: currentStep < steps.length - 1,
|
|
327
|
-
canGoPrevious: allowBackNavigation && currentStep > 0
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return (
|
|
331
|
-
<FormWizardContext.Provider value={value}>
|
|
332
|
-
{children}
|
|
333
|
-
</FormWizardContext.Provider>
|
|
334
|
-
)
|
|
335
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
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
|
-
}
|