UNPKG

@goutham1494/form-engine

Version:

A schema-driven dynamic form builder for React

1,436 lines (1,424 loc) 62.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { DynamicForm: () => DynamicForm_default, FIELD_TYPES: () => FIELD_TYPES, FieldRenderer: () => FormEngine_default, FormWizard: () => FormWizard_default }); module.exports = __toCommonJS(index_exports); // src/DynamicForm/DynamicForm.tsx var import_react9 = require("react"); var import_react_hook_form9 = require("react-hook-form"); // src/FormEngine.tsx var import_react8 = __toESM(require("react"), 1); var import_react_hook_form8 = require("react-hook-form"); // src/DynamicForm/FieldTypes/CheckboxField.tsx var import_react = __toESM(require("react"), 1); var import_react_hook_form = require("react-hook-form"); var import_jsx_runtime = require("react/jsx-runtime"); var CheckboxFieldComponent = ({ field, name, error }) => { const { control, setValue, getValues, trigger } = (0, import_react_hook_form.useFormContext)(); const debounceTimer = (0, import_react.useRef)(null); const [loading, setLoading] = (0, import_react.useState)(false); const isDarkMode = field.theme === "dark"; import_react.default.useEffect(() => { return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current); }; }, []); const { field: controllerField, fieldState: { error: controllerError } } = (0, import_react_hook_form.useController)({ name, control, defaultValue: field.defaultValue ?? (field.options ? [] : false), rules: { required: field.required, validate: field.validation?.custom } }); const getAutoErrorMessage = (error2) => { switch (error2?.type) { case "required": return `${field.label} is required`; default: return "Invalid selection"; } }; const handleDebounced = (value) => { if (field.onValueChangeDebounced) { if (debounceTimer.current) clearTimeout(debounceTimer.current); setLoading(true); debounceTimer.current = setTimeout(async () => { await field.onValueChangeDebounced?.(value, { setValue, getValues, trigger }); setLoading(false); }, field.debounceMs ?? 500); } }; const handleChange = (e, value) => { const isGroup2 = !!field.options?.length; const newValue = isGroup2 ? e.target.checked ? [...controllerField.value || [], value] : controllerField.value.filter((v) => v !== value) : e.target.checked; controllerField.onChange(newValue); field.onChange?.(e); field.onValueChange?.(newValue, { setValue, getValues, trigger }); handleDebounced(newValue); if ((error || controllerError) && field.showErrorOnBlur) { trigger(name); } }; const inputStyle = { width: "16px", height: "16px", accentColor: "#004DB2", cursor: field.disabled ? "not-allowed" : "pointer", ...field.inputStyle }; const labelStyle = field.labelStyle ?? { fontSize: "14px", color: isDarkMode ? "#e5e7eb" : "#333" }; const helpTextStyle = field.helpTextStyle ?? { fontSize: "12px", marginTop: "4px", color: isDarkMode ? "#9ca3af" : "#6b7280" }; const errorStyle = field.errorStyle ?? { color: "#d93025", marginTop: "6px", fontSize: "13px" }; const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" }; const optionWrapperStyle = field.optionWrapperStyle ?? { display: "flex", alignItems: "center", gap: "8px" }; const checkBoxGroupStyle = field.checkBoxGroupStyle ?? { display: "flex", flexDirection: "column", gap: "0.75rem" }; const isGroup = !!field.options?.length; return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [ (field.checkboxLabel ?? field.label) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "label", { htmlFor: name, className: field.labelClass ?? "", style: labelStyle, children: field.checkboxLabel ?? field.label } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: field.checkboxGroupClass ?? "", style: isGroup ? checkBoxGroupStyle : optionWrapperStyle, children: isGroup ? field.options?.map((opt) => { const isChecked = controllerField.value?.includes(opt.value); const inputId = `${name}-${opt.value}`; const helpTextAlignment = opt.helpTextAlignment ?? "underLabel"; return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "div", { style: { display: "flex", flexDirection: "column" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "label", { htmlFor: inputId, className: field.optionWrapperClass ?? "", style: { display: "flex", alignItems: "center", gap: "8px", cursor: field.disabled ? "not-allowed" : "pointer" }, title: opt.tooltip, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "input", { id: inputId, type: "checkbox", value: opt.value, checked: isChecked, onChange: (e) => handleChange(e, opt.value), onBlur: () => { if (field.showErrorOnBlur) trigger(name); }, className: field.inputClass ?? "", style: inputStyle, disabled: field.disabled || opt.disabled, "aria-describedby": opt.helpText ? `${inputId}-desc` : void 0 } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: opt.label }) ] } ), opt.helpText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { id: `${inputId}-desc`, role: "note", style: { ...helpTextStyle, marginLeft: helpTextAlignment === "underLabel" ? "24px" : "0" }, children: opt.helpText } ) ] }, opt.value ); }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "label", { className: field.optionWrapperClass ?? "", style: optionWrapperStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "input", { id: name, type: "checkbox", className: field.inputClass ?? "", style: inputStyle, checked: !!controllerField.value, onChange: (e) => handleChange(e), onBlur: () => { if (field.showErrorOnBlur) trigger(name); }, disabled: field.disabled, "aria-describedby": field.helpText ? `${name}-description` : void 0 } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: field.label }) ] } ) } ), field.helpText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "p", { id: `${name}-description`, className: field.helpTextClass ?? "", style: helpTextStyle, children: field.helpText } ), (error || controllerError) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error || controllerError) || (error || controllerError)?.message || getAutoErrorMessage(error || controllerError) }) ] }); }; var CheckboxField_default = import_react.default.memo(CheckboxFieldComponent); // src/DynamicForm/FieldTypes/GroupField.tsx var import_react2 = __toESM(require("react"), 1); var import_react_hook_form2 = require("react-hook-form"); var import_jsx_runtime2 = require("react/jsx-runtime"); function getErrorMessage(error, field) { if (!error) return ""; if (field?.errorText) return field.errorText; if (typeof error === "string") return error; if (field?.getErrorMessage && typeof field.getErrorMessage === "function") { return field.getErrorMessage(error); } if (typeof error === "object") { if ("message" in error && typeof error.message === "string") { return error.message; } const entries = Object.entries(error); for (const [, val] of entries) { if (val?.message) return val.message; } } return `${field?.label || "Field"} is invalid`; } var GroupFieldComponent = ({ field, name, error }) => { const { control, trigger } = (0, import_react_hook_form2.useFormContext)(); const { field: groupField, formState: { errors } } = (0, import_react_hook_form2.useController)({ name, control, rules: { required: field.required, validate: field.validation?.custom }, defaultValue: field.defaultValue ?? {} }); const watchedGroupValue = (0, import_react_hook_form2.useWatch)({ name, control }); const groupError = errors?.[name]; const isDarkMode = field.theme === "dark"; const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" }; const labelStyle = field.labelStyle ?? { display: "block", marginBottom: "6px", fontWeight: 500, fontSize: "14px", color: isDarkMode ? "#e5e7eb" : "#333" }; const errorStyle = field.errorStyle ?? { color: "#d93025", marginTop: "6px", fontSize: "13px" }; const helpTextStyle = field.helpTextStyle ?? { fontSize: "12px", marginTop: "4px", color: isDarkMode ? "#9ca3af" : "#6b7280" }; (0, import_react2.useEffect)(() => { const hasChanged = JSON.stringify(watchedGroupValue) !== JSON.stringify(groupField.value); if (hasChanged) groupField.onChange(watchedGroupValue); }, [watchedGroupValue, groupField]); return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [ (field.checkboxLabel ?? field.label) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "label", { htmlFor: name, className: field.labelClass ?? "", style: labelStyle, children: field.checkboxLabel ?? field.label } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "div", { id: name, className: field.layoutClass ?? "", style: field.layoutStyle, children: field.children?.map((child) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( FormEngine_default, { field: child, parentName: name }, child.name )) } ), field.helpText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "p", { id: `${name}-description`, className: field.helpTextClass ?? "", style: helpTextStyle, children: field.helpText } ), (error || groupError) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: getErrorMessage(error || groupError, field) }) ] }); }; var GroupField_default = import_react2.default.memo(GroupFieldComponent); // src/DynamicForm/FieldTypes/NumberField.tsx var import_react3 = __toESM(require("react"), 1); var import_react_hook_form3 = require("react-hook-form"); var import_jsx_runtime3 = require("react/jsx-runtime"); var NumberFieldComponent = ({ field, name, error }) => { const { setValue, getValues, trigger, control } = (0, import_react_hook_form3.useFormContext)(); const { field: controllerField } = (0, import_react_hook_form3.useController)({ name, control, defaultValue: field.defaultValue ?? "", rules: { required: field.required, pattern: field.validation?.pattern, minLength: field.validation?.minLength, maxLength: field.validation?.maxLength, validate: field.validation?.custom } }); const debounceTimer = (0, import_react3.useRef)(null); const [loading, setLoading] = (0, import_react3.useState)(false); const isDarkMode = field.theme === "dark"; const isNumberField = field.type === "number"; import_react3.default.useEffect(() => { return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current); }; }, []); const getAutoErrorMessage = (error2) => { switch (error2?.type) { case "required": return `${field.label} is required`; case "minLength": return `${field.label} must be at least ${field.validation?.minLength?.value} digits`; case "maxLength": return `${field.label} must be at most ${field.validation?.maxLength?.value} digits`; case "pattern": return `${field.label} format is invalid`; default: return "Invalid value"; } }; const handleChange = (0, import_react3.useCallback)( (e) => { const value = isNumberField ? +e.target.value : e.target.value; controllerField.onChange(e); field.onChange?.(e); if (field.onValueChange) { field.onValueChange(value, { setValue, getValues, trigger }); } if (error && field.showErrorOnBlur) { trigger(name); } if (field.onValueChangeDebounced) { if (debounceTimer.current) clearTimeout(debounceTimer.current); setLoading(true); debounceTimer.current = setTimeout(async () => { await field.onValueChangeDebounced?.(value, { setValue, getValues, trigger }); setLoading(false); }, field.debounceMs ?? 500); } }, [ controllerField, field, isNumberField, setValue, getValues, trigger, error, name ] ); const handleBlur = (e) => { field.onBlur?.(e); if (field.showErrorOnBlur) { trigger(name); } }; const getDefaultInputStyle = (isDark, hasError) => ({ padding: "10px", border: "1px solid", borderColor: hasError ? "#f87171" : isDark ? "#4b5563" : "#ccc", borderRadius: "6px", fontSize: "14px", width: "100%", backgroundColor: isDark ? "#1f2937" : "#fff", color: isDark ? "#f9fafb" : "#111827", outline: "none", boxSizing: "border-box", transition: "border-color 0.2s ease-in-out", opacity: field.disabled ? 0.6 : 1, cursor: field.disabled ? "not-allowed" : "text" }); const getDefaultLabelStyle = (isDark) => ({ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px", fontWeight: 500, fontSize: "14px", color: isDark ? "#e5e7eb" : "#333" }); const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" }; const labelStyle = field.labelStyle ?? getDefaultLabelStyle(isDarkMode); const inputStyle = { ...getDefaultInputStyle(isDarkMode, Boolean(error)), ...field.inputStyle }; const helpTextStyle = field.helpTextStyle ?? { fontSize: "12px", marginTop: "4px", color: isDarkMode ? "#9ca3af" : "#6b7280" }; const errorStyle = field.errorStyle ?? { color: "#d93025", marginTop: "6px", fontSize: "13px" }; const renderLabel = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { htmlFor: name, className: field.labelClass ?? "", style: labelStyle, children: [ field.icon && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: field.icon }), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { title: field.tooltip, children: field.label }), loading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "0.75rem" }, children: "\u23F3" }) ] }); return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [ renderLabel(), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( "input", { id: name, type: isNumberField ? "number" : "text", inputMode: field.inputMode ?? (isNumberField ? "numeric" : "text"), placeholder: field.placeholder, "aria-invalid": Boolean(error), "aria-describedby": field.helpText ? `${name}-description` : void 0, value: controllerField.value ?? "", onChange: handleChange, onBlur: handleBlur, onKeyDown: (e) => { if (field.allowedPattern) { const char = e.key; const isControlKey = [ "Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab" ].includes(char); if (!isControlKey && !field.allowedPattern.test(char)) { e.preventDefault(); } } field.onKeyDown?.(e); }, className: field.inputClass ?? "", style: inputStyle, disabled: field.disabled } ), field.helpText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( "p", { id: `${name}-description`, className: field.helpTextClass ?? "", style: { ...helpTextStyle, marginLeft: field.helpTextAlignment === "underLabel" ? "24px" : "0" }, children: field.helpText } ), error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error) || error.message || getAutoErrorMessage(error) }) ] }); }; var NumberField_default = import_react3.default.memo(NumberFieldComponent); // src/DynamicForm/FieldTypes/RadioField.tsx var import_react4 = __toESM(require("react"), 1); var import_react_hook_form4 = require("react-hook-form"); var import_jsx_runtime4 = require("react/jsx-runtime"); var RadioFieldComponent = ({ field, name, register, error }) => { const { control, setValue, getValues, trigger } = (0, import_react_hook_form4.useFormContext)(); const debounceTimer = (0, import_react4.useRef)(null); const [loading, setLoading] = (0, import_react4.useState)(false); const isDarkMode = field.theme === "dark"; import_react4.default.useEffect(() => { return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current); }; }, []); const { field: controllerField, fieldState: { error: fieldError } } = (0, import_react_hook_form4.useController)({ name, control, rules: { required: field.required } }); const getAutoErrorMessage = (error2) => { switch (error2?.type) { case "required": return `${field.label} is required`; default: return "Invalid selection"; } }; const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" }; const labelStyle = field.labelStyle ?? { display: "block", marginBottom: "6px", fontWeight: 500, fontSize: "14px", color: isDarkMode ? "#e5e7eb" : "#333" }; const radioGroupStyle = field.radioGroupStyle ?? { display: "flex", flexDirection: field.inline ? "row" : "column", gap: "0.75rem", alignItems: field.inline ? "center" : "flex-start", flexWrap: field.inline ? "wrap" : "nowrap" }; const radioInputStyle = { width: "16px", height: "16px", accentColor: "#004DB2", cursor: field.disabled ? "not-allowed" : "pointer", transition: "all 0.2s ease", ...field.inputStyle }; const helpTextStyle = field.helpTextStyle ?? { fontSize: "12px", color: isDarkMode ? "#9ca3af" : "#6b7280", marginTop: "4px" }; const errorStyle = field.errorStyle ?? { color: "#d93025", marginTop: "6px", fontSize: "13px" }; const handleChange = (value, event) => { controllerField.onChange(value); field.onChange?.(event); field.onValueChange?.(value, { setValue, getValues, trigger }); if ((error || fieldError) && field.showErrorOnBlur) { trigger(name); } if (field.onValueChangeDebounced) { if (debounceTimer.current) clearTimeout(debounceTimer.current); setLoading(true); debounceTimer.current = setTimeout(async () => { await field.onValueChangeDebounced?.(value, { setValue, getValues, trigger }); setLoading(false); }, field.debounceMs ?? 500); } }; return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: field.labelClass ?? "", style: labelStyle, children: [ field.label, loading && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: "0.75rem" }, children: " \u23F3" }) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: field.radioGroupClass ?? "", style: radioGroupStyle, children: field.options?.map((option) => { const helpTextAlignment = option.helpTextAlignment ?? "underLabel"; const inputId = `${name}-${option.value}`; const isSelected = controllerField.value === option.value; return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-start" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)( "label", { htmlFor: inputId, className: field.optionWrapperClass, style: { display: "flex", alignItems: "center", gap: "8px", cursor: field.disabled ? "not-allowed" : "pointer" }, title: option.tooltip, children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { id: inputId, type: "radio", value: option.value, checked: isSelected, onChange: (e) => handleChange(e.target.value, e), onBlur: () => { if (field.showErrorOnBlur) trigger(name); }, className: field.inputClass, style: radioInputStyle, disabled: field.disabled || option.disabled, "aria-describedby": option.helpText ? `${inputId}-desc` : void 0 } ), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "span", { style: { fontSize: "14px", fontWeight: isSelected ? 600 : 400, color: isSelected ? "#004DB2" : void 0 }, children: option.label } ) ] } ), option.helpText && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "div", { id: `${inputId}-desc`, role: "note", style: { ...helpTextStyle, marginLeft: helpTextAlignment === "underLabel" ? "24px" : "0" }, children: option.helpText } ) ] }, option.value ); }) }), field.helpText && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "p", { id: `${name}-description`, className: field.helpTextClass ?? "", style: helpTextStyle, children: field.helpText } ), (error || fieldError) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error || fieldError) || (error || fieldError)?.message || getAutoErrorMessage(error || fieldError) }) ] }); }; var RadioField_default = import_react4.default.memo(RadioFieldComponent); // src/DynamicForm/FieldTypes/SelectField.tsx var import_react5 = __toESM(require("react"), 1); var import_react_hook_form5 = require("react-hook-form"); var import_jsx_runtime5 = require("react/jsx-runtime"); var SelectFieldComponent = ({ field, name, error }) => { const { control, setValue, getValues, trigger } = (0, import_react_hook_form5.useFormContext)(); const { field: controllerField, fieldState: { error: fieldError } } = (0, import_react_hook_form5.useController)({ name, control, defaultValue: field.defaultValue ?? "", rules: { required: field.required } }); const parentValue = (0, import_react_hook_form5.useWatch)({ name: field.dependsOn || "__no_field__" }); const [dynamicOptions, setDynamicOptions] = (0, import_react5.useState)(field.options || []); const [loading, setLoading] = (0, import_react5.useState)(false); const [fetchError, setFetchError] = (0, import_react5.useState)(null); (0, import_react5.useEffect)(() => { let isMounted = true; const updateOptions = async () => { if (field.getOptions) { setLoading(true); setFetchError(null); try { const result = field.getOptions(parentValue); const resolved = result instanceof Promise ? await result : result; if (isMounted) setDynamicOptions(resolved || []); } catch (err) { if (isMounted) { setFetchError("Failed to load options."); setDynamicOptions([]); } } finally { if (isMounted) setLoading(false); } } else { setDynamicOptions(field.options || []); } }; updateOptions(); return () => { isMounted = false; }; }, [field, parentValue]); const getAutoErrorMessage = (error2) => { switch (error2?.type) { case "required": return `${field.label} is required`; default: return "Invalid selection"; } }; const handleChange = (e) => { const value = e.target.value; controllerField.onChange(value); field.onChange?.(e); field.onValueChange?.(value, { setValue, getValues, trigger }); field.onValueChangeDebounced?.(value, { setValue, getValues, trigger }); if ((error || fieldError) && field.showErrorOnBlur) { trigger(name); } }; const handleBlur = () => { if (field.showErrorOnBlur) { trigger(name); } }; const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" }; const labelStyle = field.labelStyle ?? { display: "block", marginBottom: "6px", fontWeight: 500, fontSize: "14px", color: "#333" }; const selectStyle = { padding: "10px", border: "1px solid", borderColor: error || fieldError ? "#f87171" : "#ccc", borderRadius: "6px", fontSize: "14px", width: "100%", backgroundColor: "#fff", color: "#111827", outline: "none", boxSizing: "border-box", appearance: "none", WebkitAppearance: "none", MozAppearance: "none", paddingRight: "2rem", cursor: field.disabled ? "not-allowed" : "pointer", ...field.inputStyle }; const errorStyle = field.errorStyle ?? { color: "#d93025", marginTop: "6px", fontSize: "13px" }; const helpTextStyle = field.helpTextStyle ?? { fontSize: "12px", marginTop: "4px", color: "#6b7280" }; const selectedOption = dynamicOptions.find( (opt) => opt.value === controllerField.value ); if (typeof field.render === "function") { return field.render({ name, error, register: void 0, defaultValue: field.defaultValue }); } return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)( "label", { htmlFor: name, className: field.labelClass ?? "", style: labelStyle, children: [ field.label, loading && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontSize: "0.75rem" }, children: " \u23F3" }) ] } ), /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { position: "relative", width: "100%" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)( "select", { id: name, ...controllerField, onChange: handleChange, onBlur: handleBlur, className: field.inputClass ?? "", style: selectStyle, disabled: field.disabled || loading, "aria-describedby": field.helpText ? `${name}-description` : void 0, "aria-invalid": !!(error || fieldError), children: [ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: loading ? "Loading..." : "Select..." }), dynamicOptions.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( "option", { value: opt.value, disabled: opt.disabled, title: opt.tooltip, children: opt.label }, opt.value )) ] } ), /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( "span", { style: { position: "absolute", right: "0.75rem", top: "50%", transform: "translateY(-50%)", pointerEvents: "none", fontSize: "1rem", color: "#6b7280" }, "aria-hidden": "true", children: "\u25BC" } ) ] }), field.helpText && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( "p", { id: `${name}-description`, className: field.helpTextClass ?? "", style: helpTextStyle, children: field.helpText } ), selectedOption?.helpText && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...helpTextStyle, marginTop: "2px" }, children: selectedOption.helpText }), fetchError && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { color: "orange", fontSize: "0.875rem", marginTop: "4px" }, children: fetchError }), (error || fieldError) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error || fieldError) || (error || fieldError)?.message || getAutoErrorMessage(error || fieldError) }) ] }); }; var SelectField_default = import_react5.default.memo(SelectFieldComponent); // src/DynamicForm/FieldTypes/TextAreaField.tsx var import_react6 = __toESM(require("react"), 1); var import_react_hook_form6 = require("react-hook-form"); var import_jsx_runtime6 = require("react/jsx-runtime"); var TextAreaFieldComponent = ({ field, name, error, register }) => { const { setValue, getValues, trigger, control } = (0, import_react_hook_form6.useFormContext)(); const { field: controllerField } = (0, import_react_hook_form6.useController)({ name, control, defaultValue: field.defaultValue ?? "", rules: { required: field.required, pattern: field.validation?.pattern, minLength: field.validation?.minLength, maxLength: field.validation?.maxLength, validate: field.validation?.custom } }); const debounceTimer = (0, import_react6.useRef)(null); const [loading, setLoading] = (0, import_react6.useState)(false); import_react6.default.useEffect(() => { return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current); }; }, []); const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" }; const labelStyle = field.labelStyle ?? { display: "block", marginBottom: "6px", fontWeight: 500, fontSize: "14px", color: "#333" }; const inputStyle = { padding: "10px", border: "1px solid", borderColor: error ? "#f87171" : "#ccc", borderRadius: "6px", fontSize: "14px", width: "100%", minHeight: "100px", backgroundColor: "#fff", color: "#111827", outline: "none", boxSizing: "border-box", transition: "border-color 0.2s ease-in-out", opacity: field.disabled ? 0.6 : 1, cursor: field.disabled ? "not-allowed" : "text", ...field.inputStyle }; const errorStyle = field.errorStyle ?? { color: "#d93025", marginTop: "6px", fontSize: "13px" }; const helpTextStyle = field.helpTextStyle ?? { fontSize: "12px", marginTop: "4px", color: "#6b7280" }; const getAutoErrorMessage = (error2) => { switch (error2?.type) { case "required": return `${field.label} is required`; case "minLength": return `${field.label} must be at least ${field.validation?.minLength?.value} characters`; case "maxLength": return `${field.label} must be at most ${field.validation?.maxLength?.value} characters`; case "pattern": return `${field.label} format is invalid`; default: return "Invalid value"; } }; const handleChange = (e) => { const value2 = e.target.value; controllerField.onChange(e); field.onChange?.(e); if (field.onValueChange) { field.onValueChange(value2, { setValue, getValues, trigger }); } if (error && field.showErrorOnBlur) { trigger(name); } if (field.onValueChangeDebounced) { if (debounceTimer.current) clearTimeout(debounceTimer.current); setLoading(true); debounceTimer.current = setTimeout(async () => { await field.onValueChangeDebounced?.(value2, { setValue, getValues, trigger }); setLoading(false); }, field.debounceMs ?? 500); } }; const value = controllerField.value ?? ""; const wordCount = value.trim() === "" ? 0 : value.trim().split(/\s+/).length; return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)( "label", { htmlFor: name, className: field.labelClass ?? "", style: labelStyle, children: [ field.label, field.tooltip && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)( "span", { title: field.tooltip, style: { marginLeft: "6px", cursor: "help" }, children: "\u2139\uFE0F" } ), loading && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: "0.75rem", marginLeft: "6px" }, children: "\u23F3" }) ] } ), /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "flex-start" }, children: [ field.prefixIcon && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { marginRight: "6px" }, children: field.prefixIcon }), /* @__PURE__ */ (0, import_jsx_runtime6.jsx)( "textarea", { id: name, "aria-invalid": !!error, "aria-required": field.required, "aria-disabled": field.disabled, "aria-describedby": field.helpText ? `${name}-description` : void 0, value, minLength: field.minLength, maxLength: field.maxLength, onChange: handleChange, onBlur: (e) => { controllerField.onBlur(); field.onBlur?.(e); if (field.showErrorOnBlur) { trigger(name); } }, className: field.inputClass ?? "", style: inputStyle, disabled: field.disabled } ), field.suffixIcon && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { marginLeft: "6px" }, children: field.suffixIcon }) ] }), field.helpText && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)( "p", { id: `${name}-description`, className: field.helpTextClass ?? "", style: { ...helpTextStyle, marginLeft: "0" }, children: field.helpText } ), (field.maxLength || field.showWordCount) && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)( "div", { style: { fontSize: "12px", marginTop: "4px", textAlign: "right", color: "#9ca3af" }, children: [ field.showWordCount && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [ wordCount, " words" ] }), field.maxLength && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [ " ", value.length, "/", field.maxLength ] }) ] } ), error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error) || error.message || getAutoErrorMessage(error) }) ] }); }; var TextAreaField_default = import_react6.default.memo(TextAreaFieldComponent); // src/DynamicForm/FieldTypes/TextField.tsx var import_react7 = __toESM(require("react"), 1); var import_react_hook_form7 = require("react-hook-form"); var import_jsx_runtime7 = require("react/jsx-runtime"); var TextFieldComponent = ({ field, name, error }) => { const { setValue, getValues, trigger, control } = (0, import_react_hook_form7.useFormContext)(); const { field: controllerField } = (0, import_react_hook_form7.useController)({ name, control, defaultValue: field.defaultValue ?? "", rules: { required: field.required, pattern: field.validation?.pattern, minLength: field.validation?.minLength, maxLength: field.validation?.maxLength, validate: field.validation?.custom } }); const debounceTimer = (0, import_react7.useRef)(null); const [loading, setLoading] = (0, import_react7.useState)(false); import_react7.default.useEffect(() => { return () => { if (debounceTimer.current) { clearTimeout(debounceTimer.current); } }; }, []); const helpTextPosition = field.helpTextAlignment ?? "underLabel"; const handleChange = (0, import_react7.useCallback)( (e) => { const value = e.target.value; controllerField.onChange(e); field.onChange?.(e); if (field.onValueChange) { field.onValueChange(value, { setValue, getValues, trigger }); } if (error && field.showErrorOnBlur) { trigger(name); } if (field.onValueChangeDebounced) { if (debounceTimer.current) clearTimeout(debounceTimer.current); setLoading(true); debounceTimer.current = setTimeout(async () => { await field.onValueChangeDebounced?.(value, { setValue, getValues, trigger }); setLoading(false); }, field.debounceMs ?? 500); } }, [field, setValue, getValues, trigger, controllerField] ); const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" }; const labelStyle = field.labelStyle ?? { display: "flex", alignItems: "center", gap: "0.4rem", marginBottom: "6px", fontWeight: 500, fontSize: "14px", color: "#333" }; const inputStyle = { padding: "10px", border: "1px solid", borderColor: error ? "#f87171" : "#ccc", borderRadius: "6px", fontSize: "14px", width: "100%", backgroundColor: "#fff", color: "#111827", outline: "none", boxSizing: "border-box", transition: "border-color 0.2s ease-in-out", opacity: field.disabled ? 0.6 : 1, cursor: field.disabled ? "not-allowed" : "text", ...field.inputStyle }; const helpTextStyle = field.helpTextStyle ?? { fontSize: "12px", marginTop: "4px", color: "#6b7280" }; const errorStyle = field.errorStyle ?? { color: "#d93025", marginTop: "6px", fontSize: "13px" }; const Tooltip = field.tooltip ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { title: field.tooltip, style: { cursor: "help", fontSize: "13px" }, children: "\u2753" }) : null; const LabelIcon = field.icon ?? null; const getAutoErrorMessage = (error2) => { switch (error2?.type) { case "required": return `${field.label} is required`; case "minLength": return `${field.label} must be at least ${field.validation?.minLength?.value} characters`; case "maxLength": return `${field.label} must be at most ${field.validation?.maxLength?.value} characters`; case "pattern": return `${field.label} format is invalid`; default: return "Invalid value"; } }; return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)( "label", { htmlFor: name, className: field.labelClass ?? "", style: labelStyle, children: [ field.label, " ", LabelIcon, " ", Tooltip, loading && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "0.75rem" }, children: "\u23F3" }) ] } ), helpTextPosition === "underLabel" && field.helpText && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)( "p", { id: `${name}-description`, className: field.helpTextClass ?? "", style: { ...helpTextStyle }, children: field.helpText } ), /* @__PURE__ */ (0, import_jsx_runtime7.jsx)( "input", { id: name, type: field.type, placeholder: field.placeholder, "aria-invalid": Boolean(error), "aria-describedby": field.helpText ? `${name}-description` : void 0, inputMode: field.inputMode, value: controllerField.value ?? "", minLength: field.minLength, maxLength: field.maxLength, onChange: handleChange, onBlur: (e) => { field.onBlur?.(e); if (field.showErrorOnBlur) { trigger(name); } }, onKeyDown: (e) => { if (field.allowedPattern) { const char = e.key; const isControlKey = [ "Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab" ].includes(char); if (!isControlKey && !field.allowedPattern.test(char)) { e.preventDefault(); } } field.onKeyDown?.(e); }, className: field.inputClass ?? "", style: inputStyle, disabled: field.disabled } ), helpTextPosition !== "underLabel" && field.helpText && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)( "p", { id: `${name}-description`, className: field.helpTextClass ?? "", style: helpTextStyle, children: field.helpText } ), error && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error) || error.message || getAutoErrorMessage(error) }) ] }); }; var TextField_default = import_react7.default.memo(TextFieldComponent); // src/FormEngine.tsx var import_jsx_runtime8 = require("react/jsx-runtime"); var resolveNestedValue = (obj, path) => path.split(".").reduce((acc, key) => acc?.[key], obj); var BaseFieldRenderer = ({ field, parentName }) => { const { register, formState, control, setValue, getValues } = (0, import_react_hook_form8.useFormContext)(); const qualifiedFieldName = parentName ? `${parentName}.${field.name}` : field.name; const error = resolveNestedValue(formState.errors, qualifiedFieldName); const formValues = (0, import_react_hook_form8.useWatch)({ control }); let shouldRenderField = true; if (field.visibleWhen?.conditions?.length) { const { conditions, logic = "AND" } = field.visibleWhen; const results = conditions.map(({ field: target, value, operator }) => { const currentVal = resolveNestedValue(formValues, target); switch (operator) { case "equals": return currentVal === value; case "notEquals": return currentVal !== value; case "in": return Array.isArray(value) && value.includes(currentVal); case "notIn": return Array.isArray(value) && !value.includes(currentVal); case "exists": return currentVal !== void 0 && currentVal !== null && currentVal !== ""; case "notExists": return currentVal === void 0 || currentVal === null || currentVal === ""; default: return false; } }); shouldRenderField = logic === "AND" ? results.every(Boolean) : results.some(Boolean); } (0, import_react8.useEffect)(() => { if (!shouldRenderField && !field.preserveValue) { const existing = resolveNestedValue(getValues(), qualifiedFieldName); if (existing !== void 0) { const fallback = field.defaultValue ?? ""; setValue(qualifiedFieldName, fallback); } } }, [ shouldRenderField, qualifiedFieldName, field.defaultValue, field.preserveValue, getValues, setValue ]); if (!shouldRenderField) return null; const sharedFieldProps = { field, name: qualifiedFieldName, error, register }; if (typeof field.render === "function") { return field.render({ name: qualifiedFieldName, error, register, defaultValue: field.defaultValue }); } if (field.overrideComponent) { const OverrideComponent = field.overrideComponent; return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)( OverrideComponent, { ...field.overrideComponentProps ?? {}, field, name: qualifiedFieldName, error, register } ); } switch (field.type) { case "text": case "email": return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TextField_default, { ...sharedFieldProps }); case "number": return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(NumberField_default, { ...sharedFieldProps }); case "textarea": return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TextAreaField_default, { ...sharedFieldProps }); case "select": return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectField_default, { ...sharedFieldProps }); case "radio": return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(RadioField_default, { ...sharedFieldProps }); case "checkbox": return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)( CheckboxField_default, { field, name: qualifiedFieldName, register } ); case "group": return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GroupField_default, { ...sharedFieldProps }); default: return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [ "Unsupported field type: ", field.type ] }); } }; var FormEngine_default = import_react8.default.memo(BaseFieldRenderer); // src/DynamicForm/DynamicForm.tsx var import_jsx_runtime9 = require("react/jsx-runtime"); var defaultButtonStyle = { padding: "0.75rem 1.5rem", background: "#2563eb", color: "#fff", border: "none", borderRadius: "0.5rem", fontWeight: 600, boxShadow: "0 1px 2px 0 rgba(30,41,59,.06)", transition: "background 0.18s", cursor: "pointer" }; var defaultFormStyle = { background: "#fff", boxShadow: "0 4px 24px 0 rgba(30, 41, 59, 0.10)", borderRadius: "1rem", padding: "1.5rem", display: "grid", gri