UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

400 lines (397 loc) 14.2 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { GlassButton } from '../button/GlassButton.js'; import { GlassInput } from '../input/GlassInput.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { forwardRef, useState, useCallback } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { GlassCard } from '../card/GlassCard.js'; import { GlassBadge } from '../data-display/GlassBadge.js'; import { GlassSelect } from '../input/GlassSelect.js'; import { GlassTextarea } from '../input/GlassTextarea.js'; /** * GlassFormBuilder component * Dynamic form generator with glassmorphism styling */ const GlassFormBuilder = /*#__PURE__*/forwardRef(({ schema = [], values = {}, errors = {}, onChange, onSubmit, onValidate, variant = "default", size = "md", loading = false, disabled = false, title, description, submitText = "Submit", cancelText = "Cancel", showCancel = false, onCancel, autoSave = false, autoSaveDelay = 1000, showProgress = false, validateOnChange = true, renderField, className, ...props }, ref) => { const [internalValues, setInternalValues] = useState(values); const [internalErrors, setInternalErrors] = useState(errors); const [expandedSections, setExpandedSections] = useState(new Set(schema?.filter(section => section && section.defaultExpanded !== false).map(s => s?.id).filter(Boolean) || [])); const [autoSaveTimer, setAutoSaveTimer] = useState(); const sizeClasses = { sm: "glass-text-sm", md: "glass-text-base", lg: "glass-text-lg" }; const variantClasses = { default: "glass-auto-gap glass-auto-gap-2xl", compact: "glass-auto-gap glass-auto-gap-lg", wizard: "glass-auto-gap glass-auto-gap-3xl", inline: "glass-auto-gap glass-auto-gap-sm" }; // Get all fields for progress calculation const allFields = schema?.flatMap(section => section?.fields || []).filter(Boolean) || []; const filledFields = allFields.filter(field => { if (!field || !field.id) return false; const value = internalValues[field.id]; return value !== undefined && value !== "" && value !== null; }); // Handle value change const handleValueChange = useCallback((fieldId, value) => { const newValues = { ...internalValues, [fieldId]: value }; setInternalValues(newValues); onChange?.(newValues); // Auto-save if (autoSave) { if (autoSaveTimer) { clearTimeout(autoSaveTimer); } setAutoSaveTimer(setTimeout(() => { onSubmit?.(newValues); }, autoSaveDelay)); } // Validate on change if (validateOnChange && onValidate) { const newErrors = onValidate(newValues); setInternalErrors(newErrors); } }, [internalValues, onChange, autoSave, autoSaveTimer, autoSaveDelay, onSubmit, validateOnChange, onValidate]); // Check if field should be shown based on conditional logic const shouldShowField = useCallback(field => { if (!field.conditional) return true; const { field: conditionField, operator, value: conditionValue } = field.conditional; const fieldValue = internalValues[conditionField]; switch (operator) { case "equals": return fieldValue === conditionValue; case "not_equals": return fieldValue !== conditionValue; case "contains": return String(fieldValue).includes(String(conditionValue)); case "greater_than": return Number(fieldValue) > Number(conditionValue); case "less_than": return Number(fieldValue) < Number(conditionValue); default: return true; } }, [internalValues]); // Validate field const validateField = (field, value) => { if (field.required && (!value || value === "")) { return `${field.label} is required`; } if (field.validation) { const { min, max, minLength, maxLength, pattern, custom } = field.validation; if (min !== undefined && Number(value) < min) { return `${field.label} must be at least ${min}`; } if (max !== undefined && Number(value) > max) { return `${field.label} must be at most ${max}`; } if (minLength !== undefined && String(value).length < minLength) { return `${field.label} must be at least ${minLength} characters`; } if (maxLength !== undefined && String(value).length > maxLength) { return `${field.label} must be at most ${maxLength} characters`; } if (pattern && !new RegExp(pattern).test(String(value))) { return `${field.label} format is invalid`; } if (custom) { const customError = custom(value); if (customError) return customError; } } return null; }; // Render field based on type const renderFieldComponent = field => { if (!field || !field.id) return null; const value = internalValues[field.id] ?? field.defaultValue ?? ""; const error = internalErrors[field.id]; const isDisabled = disabled || field.disabled; // Use custom renderer if provided if (renderField) { return renderField(field, value, newValue => handleValueChange(field.id, newValue), error); } const baseProps = { id: field.id, label: field.label, placeholder: field.placeholder, disabled: isDisabled, error: error, required: field.required, size, fullWidth: true }; switch (field.type) { case "text": case "email": case "password": case "number": case "tel": case "url": case "date": return jsx(GlassInput, { "data-glass-component": true, ...baseProps, type: field.type, value: value, onChange: e => handleValueChange(field.id, e.target.value) }); case "textarea": return jsx(GlassTextarea, { ...baseProps, value: value, onChange: e => handleValueChange(field.id, e.target.value), rows: 4 }); case "select": return jsx(GlassSelect, { ...baseProps, value: value, onChange: e => handleValueChange(field.id, e.target.value), options: field.options || [] }); case "checkbox": return jsxs("label", { className: 'glass-flex glass-items-center glass-gap-3 cursor-pointer', children: [jsx(GlassInput, { type: "checkbox", id: field.id, checked: value, onChange: e => handleValueChange(field.id, e.target.checked), disabled: isDisabled, className: 'glass-radius-md glass-border-glass-border focus:ring-primary' }), jsxs("span", { className: cn("font-medium", sizeClasses[size]), children: [field.label, field.required && jsx("span", { className: 'text-destructive glass-ml-1', children: "*" })] })] }); case "radio": return jsxs("div", { className: "glass-auto-gap glass-auto-gap-sm", children: [jsxs("label", { className: cn("font-medium text-foreground", sizeClasses[size]), children: [field.label, field.required && jsx("span", { className: 'text-destructive glass-ml-1', children: "*" })] }), jsx("div", { className: "glass-auto-gap glass-auto-gap-sm", children: field.options?.map(option => jsxs("label", { className: 'glass-flex glass-items-center glass-gap-3 cursor-pointer', children: [jsx(GlassInput, { type: "radio", name: field.id, value: option.value, checked: value === option.value, onChange: e => handleValueChange(field.id, e.target.value), disabled: isDisabled, className: 'glass-border-glass-border focus:ring-primary' }), jsx("span", { className: "glass-text-sm", children: option.label })] }, option.value)) })] }); default: return null; } }; // Get field width class const getFieldWidthClass = field => { if (!field) return "md:col-span-12"; switch (field.layout?.width) { case "half": return "md:col-span-6"; case "third": return "md:col-span-4"; case "quarter": return "md:col-span-3"; default: return "md:col-span-12"; } }; // Handle form submission const handleSubmit = e => { e.preventDefault(); // Validate all fields const newErrors = {}; allFields.forEach(field => { if (field && field.id && shouldShowField(field)) { const value = internalValues[field.id]; const error = validateField(field, value); if (error) { newErrors[field.id] = error; } } }); setInternalErrors(newErrors); // Submit if no errors if (Object.keys(newErrors).length === 0) { onSubmit?.(internalValues); } }; // Toggle section expansion const toggleSection = sectionId => { const newExpanded = new Set(expandedSections); if (newExpanded.has(sectionId)) { newExpanded.delete(sectionId); } else { newExpanded.add(sectionId); } setExpandedSections(newExpanded); }; return jsxs("form", { ref: ref, onSubmit: handleSubmit, className: cn("w-full", className), ...props, children: [(title || description || showProgress) && jsxs("div", { className: 'mb-6', children: [title && jsx("h2", { className: cn("font-semibold text-foreground glass-mb-2", { "glass-text-lg": size === "sm", "glass-text-xl": size === "md", "glass-text-2xl": size === "lg" }), children: title }), description && jsx("p", { className: 'glass-text-secondary mb-4', children: description }), showProgress && jsxs("div", { className: "glass-flex glass-items-center glass-gap-4", children: [jsx("div", { className: 'glass-flex-1 glass-surface-subtle glass-radius-full h-2', children: jsx("div", { className: 'glass-surface-primary h-2 glass-radius-full transition-all duration-300', style: { width: `${allFields.length > 0 ? filledFields.length / allFields.length * 100 : 0}%` } }) }), jsxs("span", { className: "glass-text-sm glass-text-secondary", children: [filledFields.length, " / ", allFields.length, " completed"] })] })] }), jsx("div", { className: variantClasses[variant], children: schema && schema.length > 0 ? schema.filter(section => section && section.id && section.fields && section.fields.length > 0).map(section => jsx(MotionFramer, { preset: "slideDown", children: jsxs(GlassCard, { variant: "default", className: "glass-p-6", children: [jsxs("div", { className: cn("flex items-center justify-between glass-mb-4", section.collapsible && "cursor-pointer"), onClick: section.collapsible ? () => toggleSection(section.id) : undefined, children: [jsxs("div", { children: [jsx("h3", { className: cn("font-medium text-foreground", { "glass-text-base": size === "sm", "glass-text-lg": size === "md", "glass-text-xl": size === "lg" }), children: section.title }), section.description && jsx("p", { className: "glass-text-sm glass-text-secondary glass-mt-1", children: section.description })] }), section.collapsible && jsx(GlassButton, { leftIcon: expandedSections.has(section.id) ? "−" : "+", variant: "ghost", size: "sm", "aria-label": expandedSections.has(section.id) ? "Collapse section" : "Expand section" })] }), (!section.collapsible || expandedSections.has(section.id)) && jsx("div", { className: "glass-grid glass-grid-cols-12 glass-gap-4", children: section.fields?.filter(field => field && field.id && shouldShowField(field))?.sort((a, b) => (a?.layout?.order || 0) - (b?.layout?.order || 0))?.map(field => jsxs("div", { className: cn("col-span-12", getFieldWidthClass(field)), children: [renderFieldComponent(field), field.description && jsx("p", { className: "glass-text-xs glass-text-secondary glass-mt-1", children: field.description })] }, field.id)) })] }) }, section.id)) : jsx("div", { className: 'text-center glass-py-8 glass-text-secondary', children: jsx("p", { children: "No form sections configured" }) }) }), jsxs("div", { className: 'glass-flex glass-items-center glass-justify-end glass-gap-3 mt-8', children: [autoSave && jsx(GlassBadge, { variant: "outline", size: "sm", children: "Auto-save enabled" }), showCancel && jsx(GlassButton, { type: "button", variant: "ghost", size: size, onClick: onCancel, disabled: loading, children: cancelText }), jsx(GlassButton, { type: "submit", variant: "default", size: size, loading: loading, disabled: disabled, children: submitText })] })] }); }); GlassFormBuilder.displayName = "GlassFormBuilder"; export { GlassFormBuilder }; //# sourceMappingURL=GlassFormBuilder.js.map