react-smart-fields 1.1.6 → 2.2.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.cjs ADDED
@@ -0,0 +1,595 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ DynamicFields: () => DynamicFields
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/components/DynamicFields.tsx
38
+ var import_react = __toESM(require("react"), 1);
39
+ var EMPTY_OBJECT = {};
40
+ var cn = (...values) => values.filter(Boolean).join(" ").trim();
41
+ var parsePath = (path) => {
42
+ const segments = path.match(/[^.[\]]+/g) ?? [];
43
+ return segments.map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
44
+ };
45
+ var getIn = (source, path) => {
46
+ const segments = parsePath(path);
47
+ let current = source;
48
+ for (const segment of segments) {
49
+ if (current === null || current === void 0) {
50
+ return void 0;
51
+ }
52
+ current = current[segment];
53
+ }
54
+ return current;
55
+ };
56
+ var setIn = (source, path, value) => {
57
+ const segments = parsePath(path);
58
+ if (!segments.length) {
59
+ return source;
60
+ }
61
+ const result = Array.isArray(source) ? [...source] : { ...source };
62
+ let currentResult = result;
63
+ let currentSource = source;
64
+ for (let index = 0; index < segments.length - 1; index += 1) {
65
+ const segment = segments[index];
66
+ const nextSegment = segments[index + 1];
67
+ const sourceValue = currentSource?.[segment];
68
+ let nextValue;
69
+ if (Array.isArray(sourceValue)) {
70
+ nextValue = [...sourceValue];
71
+ } else if (sourceValue && typeof sourceValue === "object") {
72
+ nextValue = { ...sourceValue };
73
+ } else {
74
+ nextValue = typeof nextSegment === "number" ? [] : {};
75
+ }
76
+ currentResult[segment] = nextValue;
77
+ currentResult = nextValue;
78
+ currentSource = sourceValue;
79
+ }
80
+ const lastSegment = segments[segments.length - 1];
81
+ currentResult[lastSegment] = value;
82
+ return result;
83
+ };
84
+ var isEmptyValue = (value) => {
85
+ if (value === null || value === void 0) {
86
+ return true;
87
+ }
88
+ if (typeof value === "string") {
89
+ return value.trim() === "";
90
+ }
91
+ if (Array.isArray(value)) {
92
+ return value.length === 0;
93
+ }
94
+ return false;
95
+ };
96
+ var toComparable = (value) => {
97
+ try {
98
+ return JSON.stringify(value);
99
+ } catch {
100
+ return String(value);
101
+ }
102
+ };
103
+ var buildInitialValues = (fields, defaultValues, controlledValues) => {
104
+ const seed = { ...defaultValues ?? EMPTY_OBJECT, ...controlledValues ?? EMPTY_OBJECT };
105
+ return fields.reduce((accumulator, field) => {
106
+ const existing = getIn(accumulator, field.name);
107
+ if (existing !== void 0) {
108
+ return accumulator;
109
+ }
110
+ const fallbackValue = field.defaultValue !== void 0 ? field.defaultValue : field.type === "checkbox" ? false : "";
111
+ return setIn(accumulator, field.name, fallbackValue);
112
+ }, seed);
113
+ };
114
+ var baseTheme = {
115
+ default: {
116
+ wrapper: "w-full max-w-2xl mx-auto p-6 bg-white dark:bg-zinc-950 rounded-lg border border-zinc-200 dark:border-zinc-800 shadow-sm",
117
+ title: "text-2xl font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
118
+ description: "text-sm text-zinc-500 dark:text-zinc-400",
119
+ fieldsContainer: "space-y-4",
120
+ field: "w-full space-y-2",
121
+ label: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50",
122
+ help: "text-[0.8rem] text-zinc-500 dark:text-zinc-400",
123
+ control: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
124
+ textarea: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 transition-colors min-h-[80px] resize-none",
125
+ select: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 text-zinc-950 dark:text-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
126
+ option: "text-zinc-950 dark:text-zinc-50 bg-white dark:bg-zinc-950",
127
+ checkbox: "h-4 w-4 shrink-0 rounded-sm border border-zinc-900 dark:border-zinc-50 bg-white dark:bg-zinc-950 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50",
128
+ radio: "h-4 w-4 shrink-0 rounded-full border border-zinc-900 dark:border-zinc-50 bg-white dark:bg-zinc-950 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50",
129
+ error: "text-[0.8rem] font-medium text-red-500 dark:text-red-400"
130
+ },
131
+ minimal: {
132
+ wrapper: "w-full max-w-2xl mx-auto",
133
+ title: "text-xl font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
134
+ description: "text-sm text-zinc-500 dark:text-zinc-400",
135
+ fieldsContainer: "space-y-4",
136
+ field: "w-full space-y-2",
137
+ label: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50",
138
+ help: "text-[0.8rem] text-zinc-500 dark:text-zinc-400",
139
+ control: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-transparent text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
140
+ textarea: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-transparent text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors min-h-[80px] resize-none",
141
+ select: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-transparent text-zinc-950 dark:text-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
142
+ option: "text-zinc-950 dark:text-zinc-50",
143
+ checkbox: "h-4 w-4 shrink-0 rounded-sm border border-zinc-200 dark:border-zinc-800 bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
144
+ radio: "h-4 w-4 shrink-0 rounded-full border border-zinc-200 dark:border-zinc-800 bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
145
+ error: "text-[0.8rem] font-medium text-red-500 dark:text-red-400"
146
+ },
147
+ filled: {
148
+ wrapper: "w-full max-w-2xl mx-auto p-6 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-zinc-100 dark:border-zinc-800",
149
+ title: "text-2xl font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
150
+ description: "text-sm text-zinc-500 dark:text-zinc-400",
151
+ fieldsContainer: "space-y-4",
152
+ field: "w-full space-y-2",
153
+ label: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50",
154
+ help: "text-[0.8rem] text-zinc-500 dark:text-zinc-400",
155
+ control: "w-full rounded-md border border-zinc-100 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-800 text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
156
+ textarea: "w-full rounded-md border border-zinc-100 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-800 text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50 transition-colors min-h-[80px] resize-none",
157
+ select: "w-full rounded-md border border-zinc-100 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-800 text-zinc-950 dark:text-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
158
+ option: "text-zinc-950 dark:text-zinc-50 bg-white dark:bg-zinc-900",
159
+ checkbox: "h-4 w-4 shrink-0 rounded-sm border border-zinc-200 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50",
160
+ radio: "h-4 w-4 shrink-0 rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50",
161
+ error: "text-[0.8rem] font-medium text-red-500 dark:text-red-400"
162
+ },
163
+ underline: {
164
+ wrapper: "w-full max-w-2xl mx-auto py-4",
165
+ title: "text-xl font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
166
+ description: "text-sm text-zinc-500 dark:text-zinc-400",
167
+ fieldsContainer: "space-y-4",
168
+ field: "w-full space-y-2",
169
+ label: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50",
170
+ help: "text-[0.8rem] text-zinc-500 dark:text-zinc-400",
171
+ control: "w-full border-0 border-b border-zinc-200 dark:border-zinc-700 bg-transparent text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 rounded-none focus-visible:outline-none focus-visible:ring-0 focus-visible:border-zinc-950 dark:focus-visible:border-zinc-300 disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-0",
172
+ textarea: "w-full border-0 border-b border-zinc-200 dark:border-zinc-700 bg-transparent text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 rounded-none focus-visible:outline-none focus-visible:ring-0 focus-visible:border-zinc-950 dark:focus-visible:border-zinc-300 disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-0 min-h-[80px] resize-none",
173
+ select: "w-full border-0 border-b border-zinc-200 dark:border-zinc-700 bg-transparent text-zinc-950 dark:text-zinc-50 focus-visible:outline-none focus-visible:ring-0 focus-visible:border-zinc-950 dark:focus-visible:border-zinc-300 disabled:cursor-not-allowed disabled:opacity-50 transition-colors rounded-none px-0",
174
+ option: "text-zinc-950 dark:text-zinc-50",
175
+ checkbox: "h-4 w-4 shrink-0 rounded-sm border border-zinc-200 dark:border-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
176
+ radio: "h-4 w-4 shrink-0 rounded-full border border-zinc-200 dark:border-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
177
+ error: "text-[0.8rem] font-medium text-red-500 dark:text-red-400"
178
+ }
179
+ };
180
+ var sizeMap = {
181
+ sm: {
182
+ control: "px-3 py-1.5 text-xs",
183
+ label: "text-xs",
184
+ help: "text-[0.7rem]",
185
+ error: "text-xs"
186
+ },
187
+ md: {
188
+ control: "px-3 py-2 text-sm",
189
+ label: "text-sm",
190
+ help: "text-[0.8rem]",
191
+ error: "text-[0.8rem]"
192
+ },
193
+ lg: {
194
+ control: "px-4 py-2.5 text-base",
195
+ label: "text-base",
196
+ help: "text-sm",
197
+ error: "text-sm"
198
+ }
199
+ };
200
+ var DynamicFields = ({
201
+ fields,
202
+ onChange,
203
+ value,
204
+ defaultValues,
205
+ resolver,
206
+ validateMode = "change",
207
+ showErrorWhen = "touched",
208
+ headless = false,
209
+ theme = "default",
210
+ size = "md",
211
+ ui,
212
+ stateClassNames,
213
+ className = "",
214
+ inputClassName = "",
215
+ labelClassName = "",
216
+ mainFieldClassName = "",
217
+ fieldClassName = "",
218
+ errorClassName = "",
219
+ selectClassName = "",
220
+ optionClassName = "",
221
+ checkboxClassName = "",
222
+ radioClassName = "",
223
+ title,
224
+ description,
225
+ renderLabel,
226
+ renderDescription,
227
+ renderError,
228
+ renderControl,
229
+ renderField
230
+ }) => {
231
+ const isControlled = value !== void 0;
232
+ const initialValuesRef = (0, import_react.useRef)(buildInitialValues(fields, defaultValues, value));
233
+ const [internalValues, setInternalValues] = (0, import_react.useState)(initialValuesRef.current);
234
+ const [fieldErrors, setFieldErrors] = (0, import_react.useState)({});
235
+ const [resolverErrors, setResolverErrors] = (0, import_react.useState)({});
236
+ const [touchedMap, setTouchedMap] = (0, import_react.useState)({});
237
+ const [dirtyMap, setDirtyMap] = (0, import_react.useState)({});
238
+ const [loadedOptions, setLoadedOptions] = (0, import_react.useState)({});
239
+ const [loadingOptionsMap, setLoadingOptionsMap] = (0, import_react.useState)({});
240
+ const resolverRunId = (0, import_react.useRef)(0);
241
+ const values = isControlled ? value ?? EMPTY_OBJECT : internalValues;
242
+ (0, import_react.useEffect)(() => {
243
+ if (!isControlled) {
244
+ const rebuilt = buildInitialValues(fields, defaultValues, void 0);
245
+ initialValuesRef.current = rebuilt;
246
+ setInternalValues(rebuilt);
247
+ setDirtyMap({});
248
+ setTouchedMap({});
249
+ setFieldErrors({});
250
+ setResolverErrors({});
251
+ }
252
+ }, [fields, defaultValues, isControlled]);
253
+ const visibleFields = (0, import_react.useMemo)(
254
+ () => fields.filter((field) => field.showWhen ? field.showWhen(values) : true),
255
+ [fields, values]
256
+ );
257
+ const runFieldValidation = (field, nextValues) => {
258
+ const valueAtPath = getIn(nextValues, field.name);
259
+ const isRequired = field.requiredWhen ? field.requiredWhen(nextValues) : Boolean(field.required);
260
+ if (isRequired && isEmptyValue(valueAtPath)) {
261
+ return field.requiredMessage || "This field is required";
262
+ }
263
+ if (isEmptyValue(valueAtPath)) {
264
+ return void 0;
265
+ }
266
+ if (typeof valueAtPath === "number") {
267
+ if (typeof field.min === "number" && valueAtPath < field.min) {
268
+ return `Minimum value is ${field.min}`;
269
+ }
270
+ if (typeof field.max === "number" && valueAtPath > field.max) {
271
+ return `Maximum value is ${field.max}`;
272
+ }
273
+ }
274
+ if (typeof valueAtPath === "string") {
275
+ if (typeof field.minLength === "number" && valueAtPath.length < field.minLength) {
276
+ return `Minimum length is ${field.minLength}`;
277
+ }
278
+ if (typeof field.maxLength === "number" && valueAtPath.length > field.maxLength) {
279
+ return `Maximum length is ${field.maxLength}`;
280
+ }
281
+ if (field.pattern && !field.pattern.test(valueAtPath)) {
282
+ return field.patternMessage || "Invalid format";
283
+ }
284
+ }
285
+ if (field.validate) {
286
+ return field.validate(valueAtPath, nextValues) || void 0;
287
+ }
288
+ return void 0;
289
+ };
290
+ const validateVisibleFields = (nextValues) => {
291
+ const nextErrors = {};
292
+ visibleFields.forEach((field) => {
293
+ const error = runFieldValidation(field, nextValues);
294
+ if (error) {
295
+ nextErrors[field.name] = error;
296
+ }
297
+ });
298
+ return nextErrors;
299
+ };
300
+ (0, import_react.useEffect)(() => {
301
+ const fieldsToLoad = fields.filter((field) => field.type === "select" && field.loadOptions && field.loadOptionsOn !== "change");
302
+ if (!fieldsToLoad.length) {
303
+ return;
304
+ }
305
+ fieldsToLoad.forEach((field) => {
306
+ const fieldName = field.name;
307
+ setLoadingOptionsMap((previous) => ({ ...previous, [fieldName]: true }));
308
+ field.loadOptions?.({ values, field }).then((result) => {
309
+ setLoadedOptions((previous) => ({ ...previous, [fieldName]: result }));
310
+ }).finally(() => {
311
+ setLoadingOptionsMap((previous) => ({ ...previous, [fieldName]: false }));
312
+ });
313
+ });
314
+ }, [fields]);
315
+ (0, import_react.useEffect)(() => {
316
+ const fieldsToLoad = fields.filter((field) => field.type === "select" && field.loadOptions && field.loadOptionsOn === "change");
317
+ if (!fieldsToLoad.length) {
318
+ return;
319
+ }
320
+ fieldsToLoad.forEach((field) => {
321
+ const fieldName = field.name;
322
+ setLoadingOptionsMap((previous) => ({ ...previous, [fieldName]: true }));
323
+ field.loadOptions?.({ values, field }).then((result) => {
324
+ setLoadedOptions((previous) => ({ ...previous, [fieldName]: result }));
325
+ }).finally(() => {
326
+ setLoadingOptionsMap((previous) => ({ ...previous, [fieldName]: false }));
327
+ });
328
+ });
329
+ }, [fields, values]);
330
+ (0, import_react.useEffect)(() => {
331
+ if (!resolver) {
332
+ setResolverErrors({});
333
+ return;
334
+ }
335
+ const runId = resolverRunId.current + 1;
336
+ resolverRunId.current = runId;
337
+ Promise.resolve(resolver(values)).then((errors) => {
338
+ if (resolverRunId.current === runId) {
339
+ setResolverErrors(errors ?? {});
340
+ }
341
+ }).catch(() => {
342
+ if (resolverRunId.current === runId) {
343
+ setResolverErrors({});
344
+ }
345
+ });
346
+ }, [resolver, values]);
347
+ const combinedErrors = (0, import_react.useMemo)(() => ({ ...fieldErrors, ...resolverErrors }), [fieldErrors, resolverErrors]);
348
+ (0, import_react.useEffect)(() => {
349
+ onChange(values, {
350
+ errors: combinedErrors,
351
+ touched: touchedMap,
352
+ dirty: dirtyMap
353
+ });
354
+ }, [values, combinedErrors, touchedMap, dirtyMap, onChange]);
355
+ const updateValues = (nextValues) => {
356
+ if (!isControlled) {
357
+ setInternalValues(nextValues);
358
+ }
359
+ if (validateMode === "change") {
360
+ setFieldErrors(validateVisibleFields(nextValues));
361
+ }
362
+ };
363
+ const handleFieldChange = (field, rawValue) => {
364
+ const transformedValue = field.transformOut ? field.transformOut(rawValue, values) : rawValue;
365
+ const nextValues = setIn(values, field.name, transformedValue);
366
+ const initialValue = getIn(initialValuesRef.current, field.name);
367
+ const isDirty = toComparable(initialValue) !== toComparable(transformedValue);
368
+ setDirtyMap((previous) => ({ ...previous, [field.name]: isDirty }));
369
+ updateValues(nextValues);
370
+ };
371
+ const handleFieldBlur = (field) => {
372
+ setTouchedMap((previous) => ({ ...previous, [field.name]: true }));
373
+ if (validateMode === "blur") {
374
+ const nextError = runFieldValidation(field, values);
375
+ setFieldErrors((previous) => {
376
+ const updated = { ...previous };
377
+ if (nextError) {
378
+ updated[field.name] = nextError;
379
+ } else {
380
+ delete updated[field.name];
381
+ }
382
+ return updated;
383
+ });
384
+ }
385
+ };
386
+ const defaultUi = headless ? {
387
+ wrapper: "",
388
+ title: "",
389
+ description: "",
390
+ fieldsContainer: "",
391
+ field: "",
392
+ label: "",
393
+ help: "",
394
+ control: "",
395
+ textarea: "",
396
+ select: "",
397
+ option: "",
398
+ checkbox: "",
399
+ radio: "",
400
+ error: ""
401
+ } : baseTheme[theme];
402
+ const sizeClasses = sizeMap[size];
403
+ const resolveUi = (field) => ({
404
+ wrapper: cn(defaultUi.wrapper, ui?.wrapper, className),
405
+ title: cn(defaultUi.title, ui?.title),
406
+ description: cn(defaultUi.description, ui?.description),
407
+ fieldsContainer: cn(defaultUi.fieldsContainer, ui?.fieldsContainer, mainFieldClassName),
408
+ field: cn(defaultUi.field, fieldClassName, field?.className, ui?.field, field?.ui?.field),
409
+ label: cn(defaultUi.label, sizeClasses.label, labelClassName, ui?.label, field?.labelClassName, field?.ui?.label),
410
+ help: cn(defaultUi.help, sizeClasses.help, ui?.help, field?.ui?.help),
411
+ control: cn(defaultUi.control, sizeClasses.control, inputClassName, ui?.control, field?.inputClassName, field?.ui?.control),
412
+ textarea: cn(defaultUi.textarea, sizeClasses.control, inputClassName, ui?.textarea, field?.inputClassName, field?.ui?.textarea),
413
+ select: cn(defaultUi.select, sizeClasses.control, selectClassName, ui?.select, field?.ui?.select),
414
+ option: cn(defaultUi.option, optionClassName, ui?.option, field?.ui?.option),
415
+ checkbox: cn(defaultUi.checkbox, checkboxClassName, ui?.checkbox, field?.ui?.checkbox),
416
+ radio: cn(defaultUi.radio, radioClassName, ui?.radio, field?.ui?.radio),
417
+ error: cn(defaultUi.error, sizeClasses.error, errorClassName, field?.errorClassName, ui?.error, field?.ui?.error)
418
+ });
419
+ const buildStateClassName = (field, hasError, disabled, readOnly) => cn(
420
+ hasError && (stateClassNames?.invalid ?? "border-red-500 dark:border-red-500 focus-visible:ring-red-500 dark:focus-visible:ring-red-500"),
421
+ disabled && stateClassNames?.disabled,
422
+ readOnly && stateClassNames?.readonly,
423
+ dirtyMap[field.name] && stateClassNames?.dirty,
424
+ touchedMap[field.name] && stateClassNames?.touched
425
+ );
426
+ const shouldShowError = (fieldName) => {
427
+ if (showErrorWhen === "always") {
428
+ return true;
429
+ }
430
+ if (showErrorWhen === "dirty") {
431
+ return Boolean(dirtyMap[fieldName]);
432
+ }
433
+ return Boolean(touchedMap[fieldName]);
434
+ };
435
+ const renderDefaultControl = (context) => {
436
+ const { field, value: value2, options, isLoadingOptions, classes, handleChange, handleBlur, disabled, readOnly, error } = context;
437
+ const showsError = Boolean(error) && shouldShowError(field.name);
438
+ const controlStateClass = buildStateClassName(field, showsError, disabled, readOnly);
439
+ if (field.type === "textarea") {
440
+ return /* @__PURE__ */ import_react.default.createElement(
441
+ "textarea",
442
+ {
443
+ id: field.name,
444
+ name: field.name,
445
+ value: value2 ?? "",
446
+ placeholder: field.placeholder,
447
+ onChange: (event) => handleChange(event.target.value),
448
+ onBlur: handleBlur,
449
+ disabled,
450
+ readOnly,
451
+ className: cn(classes.textarea, controlStateClass)
452
+ }
453
+ );
454
+ }
455
+ if (field.type === "select") {
456
+ return /* @__PURE__ */ import_react.default.createElement(
457
+ "select",
458
+ {
459
+ id: field.name,
460
+ name: field.name,
461
+ value: value2 ?? "",
462
+ onChange: (event) => handleChange(event.target.value),
463
+ onBlur: handleBlur,
464
+ disabled: disabled || isLoadingOptions,
465
+ className: cn(classes.select, controlStateClass)
466
+ },
467
+ /* @__PURE__ */ import_react.default.createElement("option", { value: "", className: classes.option }, isLoadingOptions ? "Loading options..." : field.placeholder || "Select an option"),
468
+ options.map((option) => /* @__PURE__ */ import_react.default.createElement(
469
+ "option",
470
+ {
471
+ key: `${field.name}-${String(option.value)}`,
472
+ value: String(option.value ?? ""),
473
+ disabled: option.disabled,
474
+ className: classes.option
475
+ },
476
+ option.label
477
+ ))
478
+ );
479
+ }
480
+ if (field.type === "radio") {
481
+ return /* @__PURE__ */ import_react.default.createElement("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3" }, options.map((option) => /* @__PURE__ */ import_react.default.createElement(
482
+ "label",
483
+ {
484
+ key: `${field.name}-${String(option.value)}`,
485
+ htmlFor: `${field.name}-${String(option.value)}`,
486
+ className: "flex items-center gap-2 cursor-pointer select-none"
487
+ },
488
+ /* @__PURE__ */ import_react.default.createElement(
489
+ "input",
490
+ {
491
+ id: `${field.name}-${String(option.value)}`,
492
+ type: "radio",
493
+ name: field.name,
494
+ value: String(option.value ?? ""),
495
+ checked: toComparable(value2) === toComparable(option.value),
496
+ onChange: () => handleChange(option.value),
497
+ onBlur: handleBlur,
498
+ disabled: disabled || option.disabled,
499
+ readOnly,
500
+ className: cn(classes.radio, controlStateClass)
501
+ }
502
+ ),
503
+ /* @__PURE__ */ import_react.default.createElement("span", { className: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-70" }, option.label)
504
+ )));
505
+ }
506
+ if (field.type === "checkbox") {
507
+ return /* @__PURE__ */ import_react.default.createElement(
508
+ "input",
509
+ {
510
+ id: field.name,
511
+ name: field.name,
512
+ type: "checkbox",
513
+ checked: Boolean(value2),
514
+ onChange: (event) => handleChange(event.target.checked),
515
+ onBlur: handleBlur,
516
+ disabled,
517
+ readOnly,
518
+ className: cn(classes.checkbox, controlStateClass)
519
+ }
520
+ );
521
+ }
522
+ return /* @__PURE__ */ import_react.default.createElement(
523
+ "input",
524
+ {
525
+ id: field.name,
526
+ name: field.name,
527
+ type: field.type,
528
+ value: value2 ?? "",
529
+ placeholder: field.placeholder,
530
+ onChange: (event) => {
531
+ if (field.type === "number") {
532
+ const nextValue = event.target.value === "" ? "" : Number(event.target.value);
533
+ handleChange(Number.isNaN(nextValue) ? "" : nextValue);
534
+ return;
535
+ }
536
+ handleChange(event.target.value);
537
+ },
538
+ onBlur: handleBlur,
539
+ disabled,
540
+ readOnly,
541
+ className: cn(classes.control, controlStateClass)
542
+ }
543
+ );
544
+ };
545
+ return /* @__PURE__ */ import_react.default.createElement("div", { className: resolveUi().wrapper }, title && /* @__PURE__ */ import_react.default.createElement("div", { className: "flex flex-col space-y-1.5 pb-6 mb-6 border-b border-zinc-200 dark:border-zinc-800" }, /* @__PURE__ */ import_react.default.createElement("h2", { className: resolveUi().title }, title), description && /* @__PURE__ */ import_react.default.createElement("p", { className: resolveUi().description }, description)), /* @__PURE__ */ import_react.default.createElement("div", { className: resolveUi().fieldsContainer }, visibleFields.map((field) => {
546
+ const classes = resolveUi(field);
547
+ const storedValue = getIn(values, field.name);
548
+ const renderedValue = field.transformIn ? field.transformIn(storedValue, values) : storedValue;
549
+ const options = loadedOptions[field.name] ?? field.options ?? [];
550
+ const isLoadingOptions = Boolean(loadingOptionsMap[field.name]);
551
+ const disabled = field.disableWhen ? field.disableWhen(values) : Boolean(field.disabled);
552
+ const readOnly = Boolean(field.readOnly);
553
+ const error = combinedErrors[field.name];
554
+ const canShowError = Boolean(error) && shouldShowError(field.name);
555
+ const isTouched = Boolean(touchedMap[field.name]);
556
+ const isDirty = Boolean(dirtyMap[field.name]);
557
+ const context = {
558
+ field,
559
+ value: renderedValue,
560
+ values,
561
+ error,
562
+ isTouched,
563
+ isDirty,
564
+ disabled,
565
+ readOnly,
566
+ options,
567
+ isLoadingOptions,
568
+ classes,
569
+ handleChange: (nextValue) => handleFieldChange(field, nextValue),
570
+ handleBlur: () => handleFieldBlur(field)
571
+ };
572
+ const requiredMark = field.required || field.requiredWhen?.(values);
573
+ const labelNode = field.type === "checkbox" ? null : renderLabel ? renderLabel(context) : field.label ? /* @__PURE__ */ import_react.default.createElement("label", { htmlFor: field.name, className: classes.label }, field.label, requiredMark && /* @__PURE__ */ import_react.default.createElement("span", { className: "text-red-500 ml-1" }, "*")) : null;
574
+ const helpNode = renderDescription ? renderDescription(context) : field.description ? /* @__PURE__ */ import_react.default.createElement("p", { className: classes.help }, field.description) : null;
575
+ const errorNode = canShowError ? renderError ? renderError(context) : /* @__PURE__ */ import_react.default.createElement("p", { className: classes.error, role: "alert" }, error) : null;
576
+ const controlNode = field.renderControl?.(context) ?? renderControl?.(context) ?? renderDefaultControl(context);
577
+ if (renderField) {
578
+ return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, { key: field.name }, renderField({
579
+ ...context,
580
+ labelNode,
581
+ helpNode,
582
+ errorNode,
583
+ controlNode
584
+ }));
585
+ }
586
+ if (field.type === "checkbox") {
587
+ return /* @__PURE__ */ import_react.default.createElement("div", { key: field.name, className: classes.field }, /* @__PURE__ */ import_react.default.createElement("label", { htmlFor: field.name, className: "inline-flex items-start gap-2 cursor-pointer select-none" }, controlNode, /* @__PURE__ */ import_react.default.createElement("span", { className: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-70" }, field.label, requiredMark && /* @__PURE__ */ import_react.default.createElement("span", { className: "text-red-500 ml-1" }, "*"))), helpNode, errorNode);
588
+ }
589
+ return /* @__PURE__ */ import_react.default.createElement("div", { key: field.name, className: classes.field }, labelNode, helpNode, controlNode, errorNode);
590
+ })));
591
+ };
592
+ // Annotate the CommonJS export names for ESM import in node:
593
+ 0 && (module.exports = {
594
+ DynamicFields
595
+ });