aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
400 lines (397 loc) • 14.2 kB
JavaScript
'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