@moontra/moonui-pro 2.0.22 → 2.1.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.
- package/dist/index.mjs +215 -214
- package/package.json +4 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +557 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +14 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-guard.tsx +177 -0
- package/src/utils/license-validator.tsx +183 -0
- package/src/utils/package-guard.ts +60 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react"
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
+
import { useForm } from "react-hook-form"
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
|
7
|
+
import { Button } from "../ui/button"
|
|
8
|
+
import { Input } from "../ui/input"
|
|
9
|
+
import { Label } from "../ui/label"
|
|
10
|
+
import { Textarea } from "../ui/textarea"
|
|
11
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
|
12
|
+
import { Checkbox } from "../ui/checkbox"
|
|
13
|
+
import { Switch } from "../ui/switch"
|
|
14
|
+
import { Badge } from "../ui/badge"
|
|
15
|
+
import { cn } from "../../lib/utils"
|
|
16
|
+
import { Lock, Sparkles, CheckCircle, AlertCircle, Eye, EyeOff, Upload, X } from "lucide-react"
|
|
17
|
+
import { useSubscription } from "../../hooks/use-subscription"
|
|
18
|
+
|
|
19
|
+
export interface AdvancedFormField {
|
|
20
|
+
name: string
|
|
21
|
+
label: string
|
|
22
|
+
type: "text" | "email" | "password" | "textarea" | "select" | "checkbox" | "switch" | "file" | "number" | "url" | "tel"
|
|
23
|
+
placeholder?: string
|
|
24
|
+
required?: boolean
|
|
25
|
+
options?: Array<{ value: string; label: string }>
|
|
26
|
+
validation?: {
|
|
27
|
+
minLength?: number
|
|
28
|
+
maxLength?: number
|
|
29
|
+
pattern?: RegExp
|
|
30
|
+
custom?: (value: any) => string | true
|
|
31
|
+
}
|
|
32
|
+
description?: string
|
|
33
|
+
defaultValue?: any
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AdvancedFormsProps {
|
|
37
|
+
fields: AdvancedFormField[]
|
|
38
|
+
onSubmit: (data: any) => void | Promise<void>
|
|
39
|
+
title?: string
|
|
40
|
+
description?: string
|
|
41
|
+
submitText?: string
|
|
42
|
+
enableAutoSave?: boolean
|
|
43
|
+
showProgress?: boolean
|
|
44
|
+
className?: string
|
|
45
|
+
layout?: "vertical" | "horizontal" | "grid"
|
|
46
|
+
columns?: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
50
|
+
fields,
|
|
51
|
+
onSubmit,
|
|
52
|
+
title = "Advanced Form",
|
|
53
|
+
description,
|
|
54
|
+
submitText = "Submit",
|
|
55
|
+
enableAutoSave = false,
|
|
56
|
+
showProgress = true,
|
|
57
|
+
className,
|
|
58
|
+
layout = "vertical",
|
|
59
|
+
columns = 2
|
|
60
|
+
}) => {
|
|
61
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
62
|
+
const [showPasswords, setShowPasswords] = useState<Record<string, boolean>>({})
|
|
63
|
+
const [uploadedFiles, setUploadedFiles] = useState<Record<string, File[]>>({})
|
|
64
|
+
|
|
65
|
+
const {
|
|
66
|
+
register,
|
|
67
|
+
handleSubmit,
|
|
68
|
+
watch,
|
|
69
|
+
formState: { errors, isValid, touchedFields },
|
|
70
|
+
setValue,
|
|
71
|
+
getValues
|
|
72
|
+
} = useForm<any>({
|
|
73
|
+
mode: "onChange",
|
|
74
|
+
defaultValues: fields.reduce((acc, field) => ({
|
|
75
|
+
...acc,
|
|
76
|
+
[field.name]: field.defaultValue || (field.type === "checkbox" ? false : "")
|
|
77
|
+
}), {})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const watchedValues = watch()
|
|
81
|
+
const completedFields = Object.keys(touchedFields).length
|
|
82
|
+
const progress = fields.length > 0 ? (completedFields / fields.length) * 100 : 0
|
|
83
|
+
|
|
84
|
+
const handleFormSubmit = async (data: any) => {
|
|
85
|
+
setIsSubmitting(true)
|
|
86
|
+
try {
|
|
87
|
+
await onSubmit(data)
|
|
88
|
+
} finally {
|
|
89
|
+
setIsSubmitting(false)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const togglePasswordVisibility = (fieldName: string) => {
|
|
94
|
+
setShowPasswords(prev => ({
|
|
95
|
+
...prev,
|
|
96
|
+
[fieldName]: !prev[fieldName]
|
|
97
|
+
}))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handleFileUpload = (fieldName: string, files: FileList | null) => {
|
|
101
|
+
if (files) {
|
|
102
|
+
const fileArray = Array.from(files)
|
|
103
|
+
setUploadedFiles(prev => ({
|
|
104
|
+
...prev,
|
|
105
|
+
[fieldName]: fileArray
|
|
106
|
+
}))
|
|
107
|
+
setValue(fieldName as any, fileArray as any)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const removeFile = (fieldName: string, index: number) => {
|
|
112
|
+
setUploadedFiles(prev => {
|
|
113
|
+
const newFiles = [...(prev[fieldName] || [])]
|
|
114
|
+
newFiles.splice(index, 1)
|
|
115
|
+
return {
|
|
116
|
+
...prev,
|
|
117
|
+
[fieldName]: newFiles
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
setValue(fieldName as any, uploadedFiles[fieldName]?.filter((_, i) => i !== index) || [] as any)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const renderField = (field: AdvancedFormField, index: number) => {
|
|
124
|
+
const error = errors[field.name]
|
|
125
|
+
const isTouched = touchedFields[field.name]
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<motion.div
|
|
129
|
+
key={field.name}
|
|
130
|
+
initial={{ opacity: 0, y: 20 }}
|
|
131
|
+
animate={{ opacity: 1, y: 0 }}
|
|
132
|
+
transition={{ delay: index * 0.1 }}
|
|
133
|
+
className="space-y-2"
|
|
134
|
+
>
|
|
135
|
+
<div className="flex items-center justify-between">
|
|
136
|
+
<Label htmlFor={field.name} className="flex items-center gap-2">
|
|
137
|
+
{field.label}
|
|
138
|
+
{field.required && <span className="text-destructive">*</span>}
|
|
139
|
+
{isTouched && !error && (
|
|
140
|
+
<CheckCircle className="h-4 w-4 text-green-500" />
|
|
141
|
+
)}
|
|
142
|
+
{error && <AlertCircle className="h-4 w-4 text-destructive" />}
|
|
143
|
+
</Label>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className="relative">
|
|
147
|
+
{field.type === "text" || field.type === "email" || field.type === "number" || field.type === "url" || field.type === "tel" && (
|
|
148
|
+
<Input
|
|
149
|
+
id={field.name}
|
|
150
|
+
type={field.type}
|
|
151
|
+
placeholder={field.placeholder}
|
|
152
|
+
className={cn(error && "border-destructive")}
|
|
153
|
+
{...register(field.name, {
|
|
154
|
+
required: field.required ? `${field.label} is required` : false,
|
|
155
|
+
minLength: field.validation?.minLength ? {
|
|
156
|
+
value: field.validation.minLength,
|
|
157
|
+
message: `Minimum ${field.validation.minLength} characters required`
|
|
158
|
+
} : undefined,
|
|
159
|
+
maxLength: field.validation?.maxLength ? {
|
|
160
|
+
value: field.validation.maxLength,
|
|
161
|
+
message: `Maximum ${field.validation.maxLength} characters allowed`
|
|
162
|
+
} : undefined,
|
|
163
|
+
pattern: field.validation?.pattern ? {
|
|
164
|
+
value: field.validation.pattern,
|
|
165
|
+
message: "Invalid format"
|
|
166
|
+
} : undefined,
|
|
167
|
+
validate: field.validation?.custom
|
|
168
|
+
})}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{field.type === "password" && (
|
|
173
|
+
<div className="relative">
|
|
174
|
+
<Input
|
|
175
|
+
id={field.name}
|
|
176
|
+
type={showPasswords[field.name] ? "text" : "password"}
|
|
177
|
+
placeholder={field.placeholder}
|
|
178
|
+
className={cn("pr-10", error && "border-destructive")}
|
|
179
|
+
{...register(field.name, {
|
|
180
|
+
required: field.required ? `${field.label} is required` : false,
|
|
181
|
+
minLength: field.validation?.minLength ? {
|
|
182
|
+
value: field.validation.minLength,
|
|
183
|
+
message: `Minimum ${field.validation.minLength} characters required`
|
|
184
|
+
} : undefined
|
|
185
|
+
})}
|
|
186
|
+
/>
|
|
187
|
+
<Button
|
|
188
|
+
type="button"
|
|
189
|
+
variant="ghost"
|
|
190
|
+
size="sm"
|
|
191
|
+
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0"
|
|
192
|
+
onClick={() => togglePasswordVisibility(field.name)}
|
|
193
|
+
>
|
|
194
|
+
{showPasswords[field.name] ? (
|
|
195
|
+
<EyeOff className="h-4 w-4" />
|
|
196
|
+
) : (
|
|
197
|
+
<Eye className="h-4 w-4" />
|
|
198
|
+
)}
|
|
199
|
+
</Button>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{field.type === "textarea" && (
|
|
204
|
+
<Textarea
|
|
205
|
+
id={field.name}
|
|
206
|
+
placeholder={field.placeholder}
|
|
207
|
+
className={cn(error && "border-destructive")}
|
|
208
|
+
rows={3}
|
|
209
|
+
{...register(field.name, {
|
|
210
|
+
required: field.required ? `${field.label} is required` : false,
|
|
211
|
+
maxLength: field.validation?.maxLength ? {
|
|
212
|
+
value: field.validation.maxLength,
|
|
213
|
+
message: `Maximum ${field.validation.maxLength} characters allowed`
|
|
214
|
+
} : undefined
|
|
215
|
+
})}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{field.type === "select" && (
|
|
220
|
+
<Select
|
|
221
|
+
onValueChange={(value) => setValue(field.name, value)}
|
|
222
|
+
defaultValue={field.defaultValue}
|
|
223
|
+
>
|
|
224
|
+
<SelectTrigger className={cn(error && "border-destructive")}>
|
|
225
|
+
<SelectValue placeholder={field.placeholder} />
|
|
226
|
+
</SelectTrigger>
|
|
227
|
+
<SelectContent>
|
|
228
|
+
{field.options?.map((option) => (
|
|
229
|
+
<SelectItem key={option.value} value={option.value}>
|
|
230
|
+
{option.label}
|
|
231
|
+
</SelectItem>
|
|
232
|
+
))}
|
|
233
|
+
</SelectContent>
|
|
234
|
+
</Select>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{field.type === "checkbox" && (
|
|
238
|
+
<div className="flex items-center space-x-2">
|
|
239
|
+
<Checkbox
|
|
240
|
+
id={field.name}
|
|
241
|
+
{...register(field.name)}
|
|
242
|
+
onCheckedChange={(checked) => setValue(field.name, checked)}
|
|
243
|
+
/>
|
|
244
|
+
<Label htmlFor={field.name} className="text-sm">
|
|
245
|
+
{field.placeholder || "Check this option"}
|
|
246
|
+
</Label>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
{field.type === "switch" && (
|
|
251
|
+
<div className="flex items-center space-x-2">
|
|
252
|
+
<Switch
|
|
253
|
+
id={field.name}
|
|
254
|
+
{...register(field.name)}
|
|
255
|
+
onCheckedChange={(checked) => setValue(field.name, checked)}
|
|
256
|
+
/>
|
|
257
|
+
<Label htmlFor={field.name} className="text-sm">
|
|
258
|
+
{field.placeholder || "Toggle this option"}
|
|
259
|
+
</Label>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{field.type === "file" && (
|
|
264
|
+
<div className="space-y-2">
|
|
265
|
+
<div className="flex items-center justify-center w-full">
|
|
266
|
+
<label
|
|
267
|
+
htmlFor={field.name}
|
|
268
|
+
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-muted/50 hover:bg-muted/80 transition-colors"
|
|
269
|
+
>
|
|
270
|
+
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
271
|
+
<Upload className="w-8 h-8 mb-4 text-muted-foreground" />
|
|
272
|
+
<p className="mb-2 text-sm text-muted-foreground">
|
|
273
|
+
<span className="font-semibold">Click to upload</span> or drag and drop
|
|
274
|
+
</p>
|
|
275
|
+
</div>
|
|
276
|
+
<input
|
|
277
|
+
id={field.name}
|
|
278
|
+
type="file"
|
|
279
|
+
multiple
|
|
280
|
+
className="hidden"
|
|
281
|
+
onChange={(e) => handleFileUpload(field.name, e.target.files)}
|
|
282
|
+
/>
|
|
283
|
+
</label>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{uploadedFiles[field.name] && uploadedFiles[field.name].length > 0 && (
|
|
287
|
+
<div className="space-y-2">
|
|
288
|
+
{uploadedFiles[field.name].map((file, index) => (
|
|
289
|
+
<div key={index} className="flex items-center justify-between p-2 bg-muted rounded-md">
|
|
290
|
+
<span className="text-sm truncate">{file.name}</span>
|
|
291
|
+
<Button
|
|
292
|
+
type="button"
|
|
293
|
+
variant="ghost"
|
|
294
|
+
size="sm"
|
|
295
|
+
onClick={() => removeFile(field.name, index)}
|
|
296
|
+
>
|
|
297
|
+
<X className="h-4 w-4" />
|
|
298
|
+
</Button>
|
|
299
|
+
</div>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{field.description && (
|
|
308
|
+
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
309
|
+
)}
|
|
310
|
+
|
|
311
|
+
<AnimatePresence>
|
|
312
|
+
{error && (
|
|
313
|
+
<motion.p
|
|
314
|
+
initial={{ opacity: 0, height: 0 }}
|
|
315
|
+
animate={{ opacity: 1, height: "auto" }}
|
|
316
|
+
exit={{ opacity: 0, height: 0 }}
|
|
317
|
+
className="text-sm text-destructive"
|
|
318
|
+
>
|
|
319
|
+
{error.message as string}
|
|
320
|
+
</motion.p>
|
|
321
|
+
)}
|
|
322
|
+
</AnimatePresence>
|
|
323
|
+
</motion.div>
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const gridClassName = layout === "grid" ? `grid grid-cols-1 md:grid-cols-${columns} gap-6` : "space-y-6"
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<Card className={cn("w-full max-w-4xl", className)}>
|
|
331
|
+
<CardHeader>
|
|
332
|
+
<div className="flex items-center justify-between">
|
|
333
|
+
<div>
|
|
334
|
+
<CardTitle>{title}</CardTitle>
|
|
335
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
336
|
+
</div>
|
|
337
|
+
<Badge variant="pro" className="ml-2">
|
|
338
|
+
PRO
|
|
339
|
+
</Badge>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
{showProgress && (
|
|
343
|
+
<div className="space-y-2">
|
|
344
|
+
<div className="flex justify-between text-sm">
|
|
345
|
+
<span>Progress</span>
|
|
346
|
+
<span>{Math.round(progress)}%</span>
|
|
347
|
+
</div>
|
|
348
|
+
<div className="w-full bg-muted rounded-full h-2">
|
|
349
|
+
<motion.div
|
|
350
|
+
className="bg-primary h-2 rounded-full"
|
|
351
|
+
initial={{ width: 0 }}
|
|
352
|
+
animate={{ width: `${progress}%` }}
|
|
353
|
+
transition={{ duration: 0.3 }}
|
|
354
|
+
/>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</CardHeader>
|
|
359
|
+
|
|
360
|
+
<CardContent>
|
|
361
|
+
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
|
362
|
+
<div className={gridClassName}>
|
|
363
|
+
{fields.map((field, index) => renderField(field, index))}
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<div className="flex items-center justify-between pt-6">
|
|
367
|
+
<div className="text-sm text-muted-foreground">
|
|
368
|
+
{completedFields}/{fields.length} fields completed
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<Button
|
|
372
|
+
type="submit"
|
|
373
|
+
disabled={isSubmitting || !isValid}
|
|
374
|
+
className="min-w-32"
|
|
375
|
+
>
|
|
376
|
+
{isSubmitting ? (
|
|
377
|
+
<motion.div
|
|
378
|
+
animate={{ rotate: 360 }}
|
|
379
|
+
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
|
380
|
+
className="w-4 h-4 border-2 border-white border-t-transparent rounded-full"
|
|
381
|
+
/>
|
|
382
|
+
) : (
|
|
383
|
+
submitText
|
|
384
|
+
)}
|
|
385
|
+
</Button>
|
|
386
|
+
</div>
|
|
387
|
+
</form>
|
|
388
|
+
</CardContent>
|
|
389
|
+
</Card>
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export const AdvancedForms: React.FC<AdvancedFormsProps> = ({ className, ...props }) => {
|
|
394
|
+
// Check if we're in docs mode or have pro access
|
|
395
|
+
const docsProAccess = { hasAccess: true } // Pro access assumed in package
|
|
396
|
+
const { hasProAccess, isLoading } = useSubscription()
|
|
397
|
+
|
|
398
|
+
// In docs mode, always show the component
|
|
399
|
+
const canShowComponent = docsProAccess.isDocsMode || hasProAccess
|
|
400
|
+
|
|
401
|
+
// If not in docs mode and no pro access, show upgrade prompt
|
|
402
|
+
if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
|
|
403
|
+
return (
|
|
404
|
+
<Card className={cn("w-fit", className)}>
|
|
405
|
+
<CardContent className="py-6 text-center">
|
|
406
|
+
<div className="space-y-4">
|
|
407
|
+
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
408
|
+
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
409
|
+
</div>
|
|
410
|
+
<div>
|
|
411
|
+
<h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
|
|
412
|
+
<p className="text-muted-foreground text-xs mb-4">
|
|
413
|
+
Advanced Forms is available exclusively to MoonUI Pro subscribers.
|
|
414
|
+
</p>
|
|
415
|
+
<a href="/pricing">
|
|
416
|
+
<Button size="sm">
|
|
417
|
+
<Sparkles className="mr-2 h-4 w-4" />
|
|
418
|
+
Upgrade to Pro
|
|
419
|
+
</Button>
|
|
420
|
+
</a>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</CardContent>
|
|
424
|
+
</Card>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return <AdvancedFormsInternal className={className} {...props} />
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export type { AdvancedFormField, AdvancedFormsProps }
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react"
|
|
4
|
+
import { motion } from "framer-motion"
|
|
5
|
+
import { Loader2, Check, X, Lock, Sparkles } from "lucide-react"
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
8
|
+
import { Card, CardContent } from "../ui/card"
|
|
9
|
+
import { Button } from "../ui/button"
|
|
10
|
+
import { useSubscription } from "../../hooks/use-subscription"
|
|
11
|
+
|
|
12
|
+
const animatedButtonVariants = cva(
|
|
13
|
+
"relative inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 overflow-hidden",
|
|
14
|
+
{
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
18
|
+
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
19
|
+
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
20
|
+
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
21
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
default: "h-9 px-4 py-2",
|
|
26
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
27
|
+
lg: "h-10 rounded-md px-8",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: "default",
|
|
32
|
+
size: "default",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export interface AnimatedButtonProps
|
|
38
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
39
|
+
VariantProps<typeof animatedButtonVariants> {
|
|
40
|
+
state?: "idle" | "loading" | "success" | "error"
|
|
41
|
+
onStateChange?: (state: "idle" | "loading" | "success" | "error") => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const AnimatedButtonInternal = React.forwardRef<HTMLButtonElement, AnimatedButtonProps>(
|
|
45
|
+
({ className, variant, size, state = "idle", onStateChange, children, onClick, ...props }, ref) => {
|
|
46
|
+
const [internalState, setInternalState] = useState<"idle" | "loading" | "success" | "error">("idle")
|
|
47
|
+
const currentState = state !== "idle" ? state : internalState
|
|
48
|
+
|
|
49
|
+
const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
50
|
+
if (currentState === "loading") return
|
|
51
|
+
|
|
52
|
+
setInternalState("loading")
|
|
53
|
+
onStateChange?.("loading")
|
|
54
|
+
|
|
55
|
+
if (onClick) {
|
|
56
|
+
try {
|
|
57
|
+
await onClick(e)
|
|
58
|
+
setInternalState("success")
|
|
59
|
+
onStateChange?.("success")
|
|
60
|
+
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
setInternalState("idle")
|
|
63
|
+
onStateChange?.("idle")
|
|
64
|
+
}, 2000)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
setInternalState("error")
|
|
67
|
+
onStateChange?.("error")
|
|
68
|
+
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
setInternalState("idle")
|
|
71
|
+
onStateChange?.("idle")
|
|
72
|
+
}, 2000)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getIcon = () => {
|
|
78
|
+
switch (currentState) {
|
|
79
|
+
case "loading":
|
|
80
|
+
return <Loader2 className="h-4 w-4 animate-spin" />
|
|
81
|
+
case "success":
|
|
82
|
+
return <Check className="h-4 w-4" />
|
|
83
|
+
case "error":
|
|
84
|
+
return <X className="h-4 w-4" />
|
|
85
|
+
default:
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const getBackgroundColor = () => {
|
|
91
|
+
switch (currentState) {
|
|
92
|
+
case "success":
|
|
93
|
+
return "bg-green-600"
|
|
94
|
+
case "error":
|
|
95
|
+
return "bg-red-600"
|
|
96
|
+
default:
|
|
97
|
+
return ""
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<button
|
|
103
|
+
className={cn(animatedButtonVariants({ variant, size }), className)}
|
|
104
|
+
ref={ref}
|
|
105
|
+
onClick={handleClick}
|
|
106
|
+
disabled={currentState === "loading" || props.disabled}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
<motion.div
|
|
110
|
+
className="flex items-center gap-2"
|
|
111
|
+
animate={{
|
|
112
|
+
width: currentState !== "idle" ? "auto" : "100%"
|
|
113
|
+
}}
|
|
114
|
+
transition={{ duration: 0.2 }}
|
|
115
|
+
>
|
|
116
|
+
<motion.div
|
|
117
|
+
animate={{
|
|
118
|
+
scale: currentState !== "idle" ? 1 : 0,
|
|
119
|
+
width: currentState !== "idle" ? "auto" : 0
|
|
120
|
+
}}
|
|
121
|
+
transition={{ duration: 0.2 }}
|
|
122
|
+
className="flex items-center"
|
|
123
|
+
>
|
|
124
|
+
{getIcon()}
|
|
125
|
+
</motion.div>
|
|
126
|
+
|
|
127
|
+
<motion.span
|
|
128
|
+
animate={{
|
|
129
|
+
opacity: currentState === "idle" ? 1 : 0,
|
|
130
|
+
x: currentState !== "idle" ? -20 : 0
|
|
131
|
+
}}
|
|
132
|
+
transition={{ duration: 0.2 }}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</motion.span>
|
|
136
|
+
</motion.div>
|
|
137
|
+
|
|
138
|
+
{/* Background animation */}
|
|
139
|
+
<motion.div
|
|
140
|
+
className={cn(
|
|
141
|
+
"absolute inset-0 rounded-md",
|
|
142
|
+
getBackgroundColor()
|
|
143
|
+
)}
|
|
144
|
+
initial={{ scale: 0, opacity: 0 }}
|
|
145
|
+
animate={{
|
|
146
|
+
scale: currentState === "success" || currentState === "error" ? 1 : 0,
|
|
147
|
+
opacity: currentState === "success" || currentState === "error" ? 1 : 0
|
|
148
|
+
}}
|
|
149
|
+
transition={{ duration: 0.3 }}
|
|
150
|
+
style={{ zIndex: -1 }}
|
|
151
|
+
/>
|
|
152
|
+
</button>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
AnimatedButtonInternal.displayName = "AnimatedButtonInternal"
|
|
158
|
+
|
|
159
|
+
export const AnimatedButton = React.forwardRef<HTMLButtonElement, AnimatedButtonProps>(
|
|
160
|
+
({ className, ...props }, ref) => {
|
|
161
|
+
// Check if we're in docs mode or have pro access
|
|
162
|
+
const docsProAccess = { hasAccess: true } // Pro access assumed in package
|
|
163
|
+
const { hasProAccess, isLoading } = useSubscription()
|
|
164
|
+
|
|
165
|
+
// In docs mode, always show the component
|
|
166
|
+
const canShowComponent = docsProAccess.isDocsMode || hasProAccess
|
|
167
|
+
|
|
168
|
+
// If not in docs mode and no pro access, show upgrade prompt
|
|
169
|
+
if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
|
|
170
|
+
return (
|
|
171
|
+
<Card className={cn("w-fit", className)}>
|
|
172
|
+
<CardContent className="py-6 text-center">
|
|
173
|
+
<div className="space-y-4">
|
|
174
|
+
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
175
|
+
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
176
|
+
</div>
|
|
177
|
+
<div>
|
|
178
|
+
<h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
|
|
179
|
+
<p className="text-muted-foreground text-xs mb-4">
|
|
180
|
+
Animated Button is available exclusively to MoonUI Pro subscribers.
|
|
181
|
+
</p>
|
|
182
|
+
<a href="/pricing">
|
|
183
|
+
<Button size="sm">
|
|
184
|
+
<Sparkles className="mr-2 h-4 w-4" />
|
|
185
|
+
Upgrade to Pro
|
|
186
|
+
</Button>
|
|
187
|
+
</a>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</CardContent>
|
|
191
|
+
</Card>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return <AnimatedButtonInternal className={className} ref={ref} {...props} />
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
AnimatedButton.displayName = "AnimatedButton"
|
|
200
|
+
|
|
201
|
+
export { animatedButtonVariants }
|
|
202
|
+
export type { AnimatedButtonProps }
|