@moontra/moonui-pro 2.20.0 → 2.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +691 -261
- package/dist/index.mjs +7419 -4935
- package/package.json +4 -3
- package/scripts/postbuild.js +27 -0
- package/src/components/advanced-chart/index.tsx +5 -1
- package/src/components/advanced-forms/index.tsx +175 -16
- package/src/components/calendar/event-dialog.tsx +18 -13
- package/src/components/calendar/index.tsx +197 -50
- package/src/components/dashboard/dashboard-grid.tsx +21 -3
- package/src/components/dashboard/types.ts +3 -0
- package/src/components/dashboard/widgets/activity-feed.tsx +6 -1
- package/src/components/dashboard/widgets/comparison-widget.tsx +177 -0
- package/src/components/dashboard/widgets/index.ts +5 -0
- package/src/components/dashboard/widgets/metric-card.tsx +21 -1
- package/src/components/dashboard/widgets/progress-widget.tsx +113 -0
- package/src/components/error-boundary/index.tsx +160 -37
- package/src/components/form-wizard/form-wizard-context.tsx +54 -26
- package/src/components/form-wizard/form-wizard-progress.tsx +33 -2
- package/src/components/form-wizard/types.ts +2 -1
- package/src/components/github-stars/hooks.ts +1 -0
- package/src/components/github-stars/variants.tsx +3 -1
- package/src/components/health-check/index.tsx +14 -14
- package/src/components/hover-card-3d/index.tsx +2 -3
- package/src/components/index.ts +5 -3
- package/src/components/kanban/kanban.tsx +23 -18
- package/src/components/license-error/index.tsx +2 -0
- package/src/components/magnetic-button/index.tsx +56 -7
- package/src/components/memory-efficient-data/index.tsx +117 -115
- package/src/components/navbar/index.tsx +781 -0
- package/src/components/performance-debugger/index.tsx +62 -38
- package/src/components/performance-monitor/index.tsx +47 -33
- package/src/components/phone-number-input/index.tsx +32 -27
- package/src/components/phone-number-input/phone-number-input-simple.tsx +167 -0
- package/src/components/rich-text-editor/index.tsx +26 -28
- package/src/components/rich-text-editor/slash-commands-extension.ts +15 -5
- package/src/components/sidebar/index.tsx +32 -13
- package/src/components/timeline/index.tsx +84 -49
- package/src/components/ui/accordion.tsx +550 -42
- package/src/components/ui/avatar.tsx +2 -0
- package/src/components/ui/badge.tsx +2 -0
- package/src/components/ui/breadcrumb.tsx +2 -0
- package/src/components/ui/button.tsx +39 -33
- package/src/components/ui/card.tsx +2 -0
- package/src/components/ui/collapsible.tsx +546 -50
- package/src/components/ui/command.tsx +790 -67
- package/src/components/ui/dialog.tsx +510 -92
- package/src/components/ui/dropdown-menu.tsx +540 -52
- package/src/components/ui/index.ts +37 -5
- package/src/components/ui/input.tsx +2 -0
- package/src/components/ui/magnetic-button.tsx +1 -1
- package/src/components/ui/media-gallery.tsx +1 -2
- package/src/components/ui/navigation-menu.tsx +130 -0
- package/src/components/ui/pagination.tsx +2 -0
- package/src/components/ui/select.tsx +6 -2
- package/src/components/ui/spotlight-card.tsx +1 -1
- package/src/components/ui/table.tsx +2 -0
- package/src/components/ui/tabs-pro.tsx +542 -0
- package/src/components/ui/tabs.tsx +23 -167
- package/src/components/ui/toggle.tsx +13 -13
- package/src/index.ts +11 -3
- package/src/styles/index.css +596 -0
- package/src/use-performance-optimizer.ts +1 -1
- package/src/utils/chart-helpers.ts +1 -1
- package/src/__tests__/use-intersection-observer.test.tsx +0 -216
- package/src/__tests__/use-local-storage.test.tsx +0 -174
- package/src/__tests__/use-pro-access.test.tsx +0 -183
- package/src/components/advanced-chart/advanced-chart.test.tsx +0 -281
- package/src/components/data-table/data-table.test.tsx +0 -187
- package/src/components/enhanced/badge.tsx +0 -191
- package/src/components/enhanced/button.tsx +0 -362
- package/src/components/enhanced/card.tsx +0 -266
- package/src/components/enhanced/dialog.tsx +0 -246
- package/src/components/enhanced/index.ts +0 -4
- package/src/components/file-upload/file-upload.test.tsx +0 -243
- package/src/components/rich-text-editor/index-old-backup.tsx +0 -437
- package/src/types/moonui.d.ts +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moontra/moonui-pro",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.2",
|
|
4
4
|
"description": "Premium React components for MoonUI - Advanced UI library with 50+ pro components including performance, interactive, and gesture components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"postinstall": "node scripts/postinstall.js",
|
|
33
|
-
"build": "tsup",
|
|
33
|
+
"build": "tsup && node scripts/postbuild.js",
|
|
34
34
|
"build:dts": "tsup --dts",
|
|
35
35
|
"dev": "tsup --watch --sourcemap",
|
|
36
36
|
"clean": "rm -rf dist",
|
|
@@ -75,7 +75,6 @@
|
|
|
75
75
|
"react-dom": ">=18.0.0 || ^19.0.0"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@moontra/moonui": "^2.1.9",
|
|
79
78
|
"@radix-ui/react-accordion": "^1.2.11",
|
|
80
79
|
"@radix-ui/react-avatar": "^1.1.10",
|
|
81
80
|
"@radix-ui/react-checkbox": "^1.3.2",
|
|
@@ -95,8 +94,10 @@
|
|
|
95
94
|
"canvas-confetti": "^1.9.3",
|
|
96
95
|
"class-variance-authority": "^0.7.0",
|
|
97
96
|
"clsx": "^2.1.1",
|
|
97
|
+
"cmdk": "^1.0.4",
|
|
98
98
|
"date-fns": "^3.6.0",
|
|
99
99
|
"framer-motion": "^11.11.17",
|
|
100
|
+
"fuse.js": "^7.0.0",
|
|
100
101
|
"lucide-react": "^0.525.0",
|
|
101
102
|
"react-beautiful-dnd": "^13.1.1",
|
|
102
103
|
"react-colorful": "^5.6.1",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Add 'use client' to the beginning of dist files
|
|
11
|
+
const files = ['dist/index.mjs', 'dist/index.js'];
|
|
12
|
+
|
|
13
|
+
files.forEach(file => {
|
|
14
|
+
const filePath = path.join(__dirname, '..', file);
|
|
15
|
+
|
|
16
|
+
if (fs.existsSync(filePath)) {
|
|
17
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
18
|
+
|
|
19
|
+
if (!content.startsWith('"use client"') && !content.startsWith("'use client'")) {
|
|
20
|
+
content = '"use client";\n' + content;
|
|
21
|
+
fs.writeFileSync(filePath, content);
|
|
22
|
+
console.log(`✅ Added 'use client' to ${file}`);
|
|
23
|
+
} else {
|
|
24
|
+
console.log(`✔️ 'use client' already exists in ${file}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
@@ -775,9 +775,13 @@ export function AdvancedChart({
|
|
|
775
775
|
animationBegin={index * 100}
|
|
776
776
|
shape={(props: any) => {
|
|
777
777
|
const isHovered = hoveredDataPoint === props.payload
|
|
778
|
+
// Extract only the necessary props for the circle element
|
|
779
|
+
const { cx, cy, fill } = props
|
|
778
780
|
return (
|
|
779
781
|
<circle
|
|
780
|
-
{
|
|
782
|
+
cx={cx}
|
|
783
|
+
cy={cy}
|
|
784
|
+
fill={fill}
|
|
781
785
|
r={isHovered ? 6 : 4}
|
|
782
786
|
className="transition-all duration-200 hover:r-6"
|
|
783
787
|
onMouseEnter={() => setHoveredDataPoint(props.payload)}
|
|
@@ -15,6 +15,7 @@ import { MoonUIBadgePro as Badge } from "../ui/badge"
|
|
|
15
15
|
import { cn } from "../../lib/utils"
|
|
16
16
|
import { Lock, Sparkles, CheckCircle, AlertCircle, Eye, EyeOff, Upload, X } from "lucide-react"
|
|
17
17
|
import { useSubscription } from "../../hooks/use-subscription"
|
|
18
|
+
import { MoonUIPhoneNumberInputPro as PhoneNumberInput } from "../phone-number-input"
|
|
18
19
|
|
|
19
20
|
interface AdvancedFormField {
|
|
20
21
|
name: string
|
|
@@ -31,6 +32,7 @@ interface AdvancedFormField {
|
|
|
31
32
|
}
|
|
32
33
|
description?: string
|
|
33
34
|
defaultValue?: any
|
|
35
|
+
defaultCountry?: string // For phone number fields
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
interface AdvancedFormsProps {
|
|
@@ -61,6 +63,19 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
61
63
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
62
64
|
const [showPasswords, setShowPasswords] = useState<Record<string, boolean>>({})
|
|
63
65
|
const [uploadedFiles, setUploadedFiles] = useState<Record<string, File[]>>({})
|
|
66
|
+
const [phoneNumbers, setPhoneNumbers] = useState<Record<string, { country: string; number: string }>>(() => {
|
|
67
|
+
// Initialize phone numbers from default values
|
|
68
|
+
const initial: Record<string, { country: string; number: string }> = {}
|
|
69
|
+
fields.forEach(field => {
|
|
70
|
+
if (field.type === "tel" && field.defaultValue) {
|
|
71
|
+
initial[field.name] = {
|
|
72
|
+
country: field.defaultCountry || "US",
|
|
73
|
+
number: field.defaultValue
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
return initial
|
|
78
|
+
})
|
|
64
79
|
|
|
65
80
|
const {
|
|
66
81
|
register,
|
|
@@ -68,18 +83,72 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
68
83
|
watch,
|
|
69
84
|
formState: { errors, isValid, touchedFields },
|
|
70
85
|
setValue,
|
|
71
|
-
getValues
|
|
86
|
+
getValues,
|
|
87
|
+
setError,
|
|
88
|
+
clearErrors
|
|
72
89
|
} = useForm<any>({
|
|
73
90
|
mode: "onChange",
|
|
74
91
|
defaultValues: fields.reduce((acc, field) => ({
|
|
75
92
|
...acc,
|
|
76
|
-
[field.name]: field.defaultValue || (field.type === "checkbox" ? false : "")
|
|
93
|
+
[field.name]: field.defaultValue || (field.type === "checkbox" || field.type === "switch" ? false : "")
|
|
77
94
|
}), {})
|
|
78
95
|
})
|
|
79
96
|
|
|
80
97
|
const watchedValues = watch()
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
|
|
99
|
+
// Calculate progress based on valid and filled fields
|
|
100
|
+
const calculateProgress = () => {
|
|
101
|
+
let validFields = 0
|
|
102
|
+
let totalFields = 0
|
|
103
|
+
|
|
104
|
+
fields.forEach(field => {
|
|
105
|
+
const value = watchedValues[field.name]
|
|
106
|
+
const hasError = errors[field.name]
|
|
107
|
+
|
|
108
|
+
// Only count required fields or optional fields that have been touched
|
|
109
|
+
if (!field.required && !touchedFields[field.name] && !value) {
|
|
110
|
+
return // Skip untouched optional fields
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
totalFields++
|
|
114
|
+
|
|
115
|
+
// Check if field has value and no errors
|
|
116
|
+
if (field.type === "checkbox" || field.type === "switch") {
|
|
117
|
+
if (!field.required || value === true) {
|
|
118
|
+
if (!hasError) validFields++
|
|
119
|
+
}
|
|
120
|
+
} else if (field.type === "file") {
|
|
121
|
+
if (!field.required || uploadedFiles[field.name]?.length > 0) {
|
|
122
|
+
if (!hasError) validFields++
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
if (value && !hasError && value.toString().trim() !== "") {
|
|
126
|
+
validFields++
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return totalFields > 0 ? (validFields / totalFields) * 100 : 0
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const progress = calculateProgress()
|
|
135
|
+
|
|
136
|
+
// Check if all required fields are valid
|
|
137
|
+
const isFormValid = () => {
|
|
138
|
+
for (const field of fields) {
|
|
139
|
+
if (field.required) {
|
|
140
|
+
const value = watchedValues[field.name]
|
|
141
|
+
const hasError = errors[field.name]
|
|
142
|
+
|
|
143
|
+
if (hasError) return false
|
|
144
|
+
|
|
145
|
+
if (field.type === "checkbox" && value !== true) return false
|
|
146
|
+
if (field.type === "file" && (!uploadedFiles[field.name] || uploadedFiles[field.name].length === 0)) return false
|
|
147
|
+
if (!value || (typeof value === 'string' && value.trim() === '')) return false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
83
152
|
|
|
84
153
|
const handleFormSubmit = async (data: any) => {
|
|
85
154
|
setIsSubmitting(true)
|
|
@@ -144,7 +213,7 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
144
213
|
</div>
|
|
145
214
|
|
|
146
215
|
<div className="relative">
|
|
147
|
-
{field.type === "text" || field.type === "email" || field.type === "number" || field.type === "url"
|
|
216
|
+
{(field.type === "text" || field.type === "email" || field.type === "number" || field.type === "url") && (
|
|
148
217
|
<Input
|
|
149
218
|
id={field.name}
|
|
150
219
|
type={field.type}
|
|
@@ -163,12 +232,58 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
163
232
|
pattern: field.validation?.pattern ? {
|
|
164
233
|
value: field.validation.pattern,
|
|
165
234
|
message: "Invalid format"
|
|
235
|
+
} : field.type === "email" ? {
|
|
236
|
+
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|
237
|
+
message: "Please enter a valid email address"
|
|
238
|
+
} : field.type === "url" ? {
|
|
239
|
+
value: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
|
|
240
|
+
message: "Please enter a valid URL"
|
|
166
241
|
} : undefined,
|
|
167
242
|
validate: field.validation?.custom
|
|
168
243
|
})}
|
|
169
244
|
/>
|
|
170
245
|
)}
|
|
171
246
|
|
|
247
|
+
{field.type === "tel" && (
|
|
248
|
+
<PhoneNumberInput
|
|
249
|
+
id={field.name}
|
|
250
|
+
placeholder={field.placeholder}
|
|
251
|
+
className={cn(error && "border-destructive")}
|
|
252
|
+
value={phoneNumbers[field.name] || undefined}
|
|
253
|
+
onChange={(phoneData: { country: string; number: string; fullNumber: string; isValid: boolean }) => {
|
|
254
|
+
// Update local state
|
|
255
|
+
setPhoneNumbers(prev => ({
|
|
256
|
+
...prev,
|
|
257
|
+
[field.name]: { country: phoneData.country, number: phoneData.number }
|
|
258
|
+
}))
|
|
259
|
+
|
|
260
|
+
// Update form value
|
|
261
|
+
setValue(field.name, phoneData.fullNumber)
|
|
262
|
+
clearErrors(field.name)
|
|
263
|
+
|
|
264
|
+
// Check if required
|
|
265
|
+
if (field.required && !phoneData.fullNumber) {
|
|
266
|
+
setError(field.name, { type: 'required', message: `${field.label} is required` })
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check custom validation
|
|
270
|
+
if (phoneData.fullNumber && field.validation?.custom) {
|
|
271
|
+
const validationResult = field.validation.custom(phoneData.fullNumber)
|
|
272
|
+
if (validationResult !== true) {
|
|
273
|
+
setError(field.name, { type: 'custom', message: validationResult })
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}}
|
|
277
|
+
defaultCountry={field.defaultCountry || "US"}
|
|
278
|
+
showFlags={true}
|
|
279
|
+
showDialCode={true}
|
|
280
|
+
autoFormat={true}
|
|
281
|
+
validateOnChange={true}
|
|
282
|
+
showValidationIcon={true}
|
|
283
|
+
error={error?.message as string}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
|
|
172
287
|
{field.type === "password" && (
|
|
173
288
|
<div className="relative">
|
|
174
289
|
<Input
|
|
@@ -181,7 +296,16 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
181
296
|
minLength: field.validation?.minLength ? {
|
|
182
297
|
value: field.validation.minLength,
|
|
183
298
|
message: `Minimum ${field.validation.minLength} characters required`
|
|
184
|
-
} : undefined
|
|
299
|
+
} : undefined,
|
|
300
|
+
maxLength: field.validation?.maxLength ? {
|
|
301
|
+
value: field.validation.maxLength,
|
|
302
|
+
message: `Maximum ${field.validation.maxLength} characters allowed`
|
|
303
|
+
} : undefined,
|
|
304
|
+
pattern: field.validation?.pattern ? {
|
|
305
|
+
value: field.validation.pattern,
|
|
306
|
+
message: "Invalid format"
|
|
307
|
+
} : undefined,
|
|
308
|
+
validate: field.validation?.custom
|
|
185
309
|
})}
|
|
186
310
|
/>
|
|
187
311
|
<Button
|
|
@@ -208,17 +332,29 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
208
332
|
rows={3}
|
|
209
333
|
{...register(field.name, {
|
|
210
334
|
required: field.required ? `${field.label} is required` : false,
|
|
335
|
+
minLength: field.validation?.minLength ? {
|
|
336
|
+
value: field.validation.minLength,
|
|
337
|
+
message: `Minimum ${field.validation.minLength} characters required`
|
|
338
|
+
} : undefined,
|
|
211
339
|
maxLength: field.validation?.maxLength ? {
|
|
212
340
|
value: field.validation.maxLength,
|
|
213
341
|
message: `Maximum ${field.validation.maxLength} characters allowed`
|
|
214
|
-
} : undefined
|
|
342
|
+
} : undefined,
|
|
343
|
+
validate: field.validation?.custom
|
|
215
344
|
})}
|
|
216
345
|
/>
|
|
217
346
|
)}
|
|
218
347
|
|
|
219
348
|
{field.type === "select" && (
|
|
220
349
|
<Select
|
|
221
|
-
onValueChange={(value) =>
|
|
350
|
+
onValueChange={(value) => {
|
|
351
|
+
setValue(field.name, value)
|
|
352
|
+
if (field.required && !value) {
|
|
353
|
+
setError(field.name, { type: 'required', message: `${field.label} is required` })
|
|
354
|
+
} else {
|
|
355
|
+
clearErrors(field.name)
|
|
356
|
+
}
|
|
357
|
+
}}
|
|
222
358
|
defaultValue={field.defaultValue}
|
|
223
359
|
>
|
|
224
360
|
<SelectTrigger className={cn(error && "border-destructive")}>
|
|
@@ -238,10 +374,20 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
238
374
|
<div className="flex items-center space-x-2">
|
|
239
375
|
<Checkbox
|
|
240
376
|
id={field.name}
|
|
241
|
-
{...register(field.name
|
|
242
|
-
|
|
377
|
+
{...register(field.name, {
|
|
378
|
+
required: field.required ? `${field.label} must be checked` : false
|
|
379
|
+
})}
|
|
380
|
+
onCheckedChange={(checked: boolean) => {
|
|
381
|
+
setValue(field.name, checked)
|
|
382
|
+
if (field.required && !checked) {
|
|
383
|
+
setError(field.name, { type: 'required', message: `${field.label} must be checked` })
|
|
384
|
+
} else {
|
|
385
|
+
clearErrors(field.name)
|
|
386
|
+
}
|
|
387
|
+
}}
|
|
388
|
+
defaultChecked={field.defaultValue}
|
|
243
389
|
/>
|
|
244
|
-
<Label htmlFor={field.name} className="text-sm">
|
|
390
|
+
<Label htmlFor={field.name} className="text-sm cursor-pointer">
|
|
245
391
|
{field.placeholder || "Check this option"}
|
|
246
392
|
</Label>
|
|
247
393
|
</div>
|
|
@@ -253,6 +399,7 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
253
399
|
id={field.name}
|
|
254
400
|
{...register(field.name)}
|
|
255
401
|
onCheckedChange={(checked: boolean) => setValue(field.name, checked)}
|
|
402
|
+
defaultChecked={field.defaultValue}
|
|
256
403
|
/>
|
|
257
404
|
<Label htmlFor={field.name} className="text-sm">
|
|
258
405
|
{field.placeholder || "Toggle this option"}
|
|
@@ -328,7 +475,7 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
328
475
|
|
|
329
476
|
return (
|
|
330
477
|
<Card className={cn("w-full max-w-4xl", className)}>
|
|
331
|
-
<CardHeader>
|
|
478
|
+
<CardHeader className={cn(showProgress && "pb-0")}>
|
|
332
479
|
<div className="flex items-center justify-between">
|
|
333
480
|
<div>
|
|
334
481
|
<CardTitle>{title}</CardTitle>
|
|
@@ -340,7 +487,7 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
340
487
|
</div>
|
|
341
488
|
|
|
342
489
|
{showProgress && (
|
|
343
|
-
<div className="space-y-2">
|
|
490
|
+
<div className="space-y-2 mt-4">
|
|
344
491
|
<div className="flex justify-between text-sm">
|
|
345
492
|
<span>Progress</span>
|
|
346
493
|
<span>{Math.round(progress)}%</span>
|
|
@@ -357,7 +504,7 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
357
504
|
)}
|
|
358
505
|
</CardHeader>
|
|
359
506
|
|
|
360
|
-
<CardContent>
|
|
507
|
+
<CardContent className={cn(showProgress && "pt-8")}>
|
|
361
508
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
|
362
509
|
<div className={gridClassName}>
|
|
363
510
|
{fields.map((field, index) => renderField(field, index))}
|
|
@@ -365,12 +512,24 @@ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
|
365
512
|
|
|
366
513
|
<div className="flex items-center justify-between pt-6">
|
|
367
514
|
<div className="text-sm text-muted-foreground">
|
|
368
|
-
{
|
|
515
|
+
{(() => {
|
|
516
|
+
const requiredFields = fields.filter(f => f.required).length
|
|
517
|
+
const completedRequired = fields.filter(f => {
|
|
518
|
+
if (!f.required) return false
|
|
519
|
+
const value = watchedValues[f.name]
|
|
520
|
+
const hasError = errors[f.name]
|
|
521
|
+
if (hasError) return false
|
|
522
|
+
if (f.type === "checkbox") return value === true
|
|
523
|
+
if (f.type === "file") return uploadedFiles[f.name]?.length > 0
|
|
524
|
+
return value && value.toString().trim() !== ""
|
|
525
|
+
}).length
|
|
526
|
+
return `${completedRequired}/${requiredFields} required fields completed`
|
|
527
|
+
})()}
|
|
369
528
|
</div>
|
|
370
529
|
|
|
371
530
|
<Button
|
|
372
531
|
type="submit"
|
|
373
|
-
disabled={isSubmitting || !
|
|
532
|
+
disabled={isSubmitting || !isFormValid()}
|
|
374
533
|
className="min-w-32"
|
|
375
534
|
>
|
|
376
535
|
{isSubmitting ? (
|
|
@@ -36,9 +36,10 @@ const EVENT_TYPES = [
|
|
|
36
36
|
] as const
|
|
37
37
|
|
|
38
38
|
const PRESET_COLORS = [
|
|
39
|
-
'#3b82f6', '#10b981', '#f59e0b', '#8b5cf6',
|
|
40
|
-
'#
|
|
41
|
-
'#
|
|
39
|
+
'#3b82f6', '#10b981', '#f59e0b', '#8b5cf6',
|
|
40
|
+
'#ef4444', '#06b6d4', '#84cc16', '#f97316',
|
|
41
|
+
'#ec4899', '#6366f1', '#14b8a6', '#eab308',
|
|
42
|
+
'#f43f5e', '#a855f7', '#22c55e', '#64748b',
|
|
42
43
|
]
|
|
43
44
|
|
|
44
45
|
export function EventDialog({
|
|
@@ -303,24 +304,28 @@ export function EventDialog({
|
|
|
303
304
|
<Palette className="w-4 h-4" />
|
|
304
305
|
Color
|
|
305
306
|
</Label>
|
|
306
|
-
<div className="
|
|
307
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
307
|
+
<div className="space-y-2">
|
|
308
|
+
<div className="flex items-center gap-2">
|
|
309
|
+
<input
|
|
310
|
+
type="color"
|
|
311
|
+
value={formData.color}
|
|
312
|
+
onChange={(e) => setFormData(prev => ({ ...prev, color: e.target.value }))}
|
|
313
|
+
className="w-8 h-8 rounded border cursor-pointer flex-shrink-0"
|
|
314
|
+
/>
|
|
315
|
+
<span className="text-sm text-muted-foreground">Custom color</span>
|
|
316
|
+
</div>
|
|
317
|
+
<div className="grid grid-cols-8 gap-1">
|
|
314
318
|
{PRESET_COLORS.map((color) => (
|
|
315
319
|
<button
|
|
316
320
|
key={color}
|
|
317
321
|
type="button"
|
|
318
322
|
onClick={() => setFormData(prev => ({ ...prev, color }))}
|
|
319
323
|
className={cn(
|
|
320
|
-
"w-
|
|
321
|
-
formData.color === color ? "border-foreground" : "border-transparent"
|
|
324
|
+
"w-full aspect-square rounded border-2 hover:scale-110 transition-transform",
|
|
325
|
+
formData.color === color ? "border-foreground shadow-sm" : "border-transparent"
|
|
322
326
|
)}
|
|
323
327
|
style={{ backgroundColor: color }}
|
|
328
|
+
aria-label={`Select color ${color}`}
|
|
324
329
|
/>
|
|
325
330
|
))}
|
|
326
331
|
</div>
|