rhf-dynamic-forms 1.1.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,1611 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let react = require("react");
30
+ let react_hook_form = require("react-hook-form");
31
+ let react_jsx_runtime = require("react/jsx-runtime");
32
+ let zod = require("zod");
33
+ let _hookform_resolvers_zod = require("@hookform/resolvers/zod");
34
+ let json_logic_js = require("json-logic-js");
35
+ json_logic_js = __toESM(json_logic_js);
36
+
37
+ //#region src/context/DynamicFormContext.tsx
38
+ /**
39
+ * Context for sharing form state and configuration with child components.
40
+ *
41
+ * This context is set up by the DynamicForm component and consumed by
42
+ * field renderers and other internal components.
43
+ */
44
+ const DynamicFormContext = (0, react.createContext)(null);
45
+ DynamicFormContext.displayName = "DynamicFormContext";
46
+
47
+ //#endregion
48
+ //#region src/customComponents/ConfigurationError.ts
49
+ var ConfigurationError$1 = class ConfigurationError$1 extends Error {
50
+ path;
51
+ component;
52
+ constructor(message, path, component) {
53
+ super(message);
54
+ this.name = "ConfigurationError";
55
+ this.path = path;
56
+ this.component = component;
57
+ const ErrorWithCapture = Error;
58
+ if (ErrorWithCapture.captureStackTrace) ErrorWithCapture.captureStackTrace(this, ConfigurationError$1);
59
+ }
60
+ static formatMessage(baseMessage, path, component) {
61
+ const parts = [];
62
+ if (component) parts.push(`Component "${component}"`);
63
+ if (path) parts.push(`at ${path}`);
64
+ if (parts.length > 0) return `${parts.join(" ")}: ${baseMessage}`;
65
+ return baseMessage;
66
+ }
67
+ };
68
+
69
+ //#endregion
70
+ //#region src/customComponents/defineCustomComponent.ts
71
+ /**
72
+ * Type-safe helper for defining custom components.
73
+ * Provides TypeScript inference for component props based on propsSchema.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const RatingField = defineCustomComponent({
78
+ * component: RatingFieldComponent,
79
+ * propsSchema: z.object({ maxStars: z.number() }),
80
+ * defaultProps: { maxStars: 5 },
81
+ * });
82
+ * ```
83
+ */
84
+ function defineCustomComponent(definition) {
85
+ return definition;
86
+ }
87
+
88
+ //#endregion
89
+ //#region src/customComponents/types.ts
90
+ const isCustomComponentDefinition = (entry) => {
91
+ return typeof entry === "object" && entry !== null && "component" in entry && typeof entry.component === "function";
92
+ };
93
+ const normalizeComponentDefinition = (entry, name) => {
94
+ if (isCustomComponentDefinition(entry)) return {
95
+ ...entry,
96
+ displayName: entry.displayName ?? name
97
+ };
98
+ return {
99
+ component: entry,
100
+ displayName: name
101
+ };
102
+ };
103
+
104
+ //#endregion
105
+ //#region src/customComponents/validateCustomElement.ts
106
+ /**
107
+ * Validate custom element against its component definition.
108
+ */
109
+ function validateCustomElement(element, registry, path) {
110
+ const entry = registry[element.component];
111
+ if (!entry) {
112
+ const available = Object.keys(registry);
113
+ const availableMessage = available.length > 0 ? `Available components: ${available.join(", ")}` : "No custom components registered.";
114
+ throw new ConfigurationError$1(`Unknown custom component "${element.component}" at ${path}. ${availableMessage}`, path, element.component);
115
+ }
116
+ const definition = normalizeComponentDefinition(entry, element.component);
117
+ const mergedProps = {
118
+ ...definition.defaultProps,
119
+ ...element.componentProps
120
+ };
121
+ if (definition.propsSchema) {
122
+ const result = definition.propsSchema.safeParse(mergedProps);
123
+ if (!result.success) throw new ConfigurationError$1(`Invalid props for "${definition.displayName || element.component}" at ${path}:\n${result.error.issues.map((issue) => ` - ${issue.path.join(".") || "root"}: ${issue.message}`).join("\n")}`, path, element.component);
124
+ return {
125
+ ...element,
126
+ componentProps: result.data,
127
+ __definition: definition
128
+ };
129
+ }
130
+ return {
131
+ ...element,
132
+ componentProps: mergedProps,
133
+ __definition: definition
134
+ };
135
+ }
136
+ function isCustomElement(element) {
137
+ return typeof element === "object" && element !== null && "type" in element && element.type === "custom" && "component" in element && typeof element.component === "string";
138
+ }
139
+
140
+ //#endregion
141
+ //#region src/customComponents/validateConfiguration.ts
142
+ /**
143
+ * Recursively validate all custom elements in a form configuration.
144
+ */
145
+ function validateCustomComponents(config, registry = {}) {
146
+ const validatedElements = validateElements(config.elements, registry, "elements");
147
+ return {
148
+ ...config,
149
+ elements: validatedElements
150
+ };
151
+ }
152
+ function validateElements(elements, registry, basePath) {
153
+ return elements.map((element, index) => {
154
+ return validateElement(element, registry, `${basePath}[${index}]`);
155
+ });
156
+ }
157
+ function validateElement(element, registry, path) {
158
+ if (isCustomElement(element)) return validateCustomElement(element, registry, path);
159
+ if (isContainerElement$1(element)) return validateContainer(element, registry, path);
160
+ if (isColumnElement$1(element)) return validateColumn(element, registry, path);
161
+ return element;
162
+ }
163
+ function validateContainer(container, registry, path) {
164
+ const validatedColumns = container.columns.map((column, index) => validateColumn(column, registry, `${path}.columns[${index}]`));
165
+ return {
166
+ ...container,
167
+ columns: validatedColumns
168
+ };
169
+ }
170
+ function validateColumn(column, registry, path) {
171
+ const validatedElements = validateElements(column.elements, registry, `${path}.elements`);
172
+ return {
173
+ ...column,
174
+ elements: validatedElements
175
+ };
176
+ }
177
+ function isContainerElement$1(element) {
178
+ return element.type === "container";
179
+ }
180
+ function isColumnElement$1(element) {
181
+ return element.type === "column";
182
+ }
183
+
184
+ //#endregion
185
+ //#region src/types/elements.ts
186
+ /**
187
+ * Type guard to check if an element is a field element.
188
+ */
189
+ const isFieldElement = (element) => element.type === "text" || element.type === "email" || element.type === "boolean" || element.type === "phone" || element.type === "date" || element.type === "select" || element.type === "array" || element.type === "custom";
190
+ /**
191
+ * Type guard to check if an element is an array field element.
192
+ */
193
+ const isArrayFieldElement = (element) => element.type === "array";
194
+ /**
195
+ * Type guard to check if an element is a container element.
196
+ */
197
+ const isContainerElement = (element) => element.type === "container";
198
+ /**
199
+ * Type guard to check if an element is a column element.
200
+ */
201
+ const isColumnElement = (element) => element.type === "column";
202
+ /**
203
+ * Type guard to check if an element is a custom field element.
204
+ */
205
+ const isCustomFieldElement = (element) => element.type === "custom";
206
+
207
+ //#endregion
208
+ //#region src/hooks/useDynamicFormContext.ts
209
+ /**
210
+ * Hook to access the DynamicForm context.
211
+ *
212
+ * Must be used within a DynamicForm component.
213
+ * Throws an error if used outside of the form context.
214
+ *
215
+ * @returns The DynamicFormContext value
216
+ * @throws Error if used outside of DynamicForm
217
+ *
218
+ * @example
219
+ * ```tsx
220
+ * function MyCustomField({ config }) {
221
+ * const { form, fieldComponents } = useDynamicFormContext();
222
+ *
223
+ * const value = form.watch(config.name);
224
+ * // ... render field
225
+ * }
226
+ * ```
227
+ */
228
+ const useDynamicFormContext = () => {
229
+ const context = (0, react.useContext)(DynamicFormContext);
230
+ if (!context) throw new Error("useDynamicFormContext must be used within a DynamicForm component. Make sure your component is a child of <DynamicForm>.");
231
+ return context;
232
+ };
233
+ /**
234
+ * Hook to safely access the DynamicForm context.
235
+ * Returns null if used outside of the form context instead of throwing.
236
+ *
237
+ * @returns The DynamicFormContext value or null
238
+ *
239
+ * @example
240
+ * ```tsx
241
+ * function MaybeInForm() {
242
+ * const context = useDynamicFormContextSafe();
243
+ *
244
+ * if (!context) {
245
+ * return <span>Not in a form</span>;
246
+ * }
247
+ *
248
+ * return <span>In a form!</span>;
249
+ * }
250
+ * ```
251
+ */
252
+ const useDynamicFormContextSafe = () => {
253
+ return (0, react.useContext)(DynamicFormContext);
254
+ };
255
+
256
+ //#endregion
257
+ //#region src/components/ContainerRenderer.tsx
258
+ /**
259
+ * Default container styles using flexbox.
260
+ */
261
+ const defaultContainerStyle = {
262
+ display: "flex",
263
+ gap: "16px",
264
+ flexWrap: "wrap"
265
+ };
266
+ /**
267
+ * Default container component used when no custom container is provided.
268
+ * Receives config and children props as per ContainerProps interface.
269
+ */
270
+ const DefaultContainer = ({ children }) => {
271
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
272
+ style: defaultContainerStyle,
273
+ children
274
+ });
275
+ };
276
+ /**
277
+ * Renders a container element with its columns.
278
+ *
279
+ * The ContainerRenderer:
280
+ * 1. Checks visibility (Phase 4 - currently all containers are visible)
281
+ * 2. Looks up custom container component if specified
282
+ * 3. Renders columns as children using ColumnRenderer
283
+ *
284
+ * @example
285
+ * ```tsx
286
+ * <ContainerRenderer
287
+ * config={{
288
+ * type: 'container',
289
+ * columns: [
290
+ * { type: 'column', width: '50%', elements: [...] },
291
+ * { type: 'column', width: '50%', elements: [...] }
292
+ * ]
293
+ * }}
294
+ * />
295
+ * ```
296
+ */
297
+ const ContainerRenderer = ({ config }) => {
298
+ const { customContainers } = useDynamicFormContext();
299
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(customContainers?.default ?? DefaultContainer, {
300
+ config,
301
+ children: config.columns.map((column, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColumnRenderer, { config: column }, `column-${index}`))
302
+ });
303
+ };
304
+ ContainerRenderer.displayName = "ContainerRenderer";
305
+
306
+ //#endregion
307
+ //#region src/components/FieldRenderer.tsx
308
+ const CustomFieldRenderer = ({ config, field, fieldState, formValues, setValue }) => {
309
+ const { customComponents } = useDynamicFormContext();
310
+ if (config.type !== "custom") return null;
311
+ const customConfig = config;
312
+ const entry = customComponents[customConfig.component];
313
+ if (!entry) {
314
+ console.warn(`No custom component registered for: "${customConfig.component}". Make sure to pass it in the customComponents prop.`);
315
+ return null;
316
+ }
317
+ const FieldComponent = normalizeComponentDefinition(entry, customConfig.component).component;
318
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldComponent, {
319
+ componentProps: customConfig.componentProps ?? {},
320
+ config: customConfig,
321
+ field,
322
+ fieldState,
323
+ formValues,
324
+ setValue
325
+ });
326
+ };
327
+ const StandardFieldRenderer = ({ config, field, fieldState, formValues, setValue }) => {
328
+ const { fieldComponents } = useDynamicFormContext();
329
+ const FieldComponent = fieldComponents[config.type];
330
+ if (!FieldComponent) {
331
+ console.warn(`No field component registered for type: "${config.type}". Make sure to provide all field types in the fieldComponents prop.`);
332
+ return null;
333
+ }
334
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldComponent, {
335
+ config,
336
+ field,
337
+ fieldState,
338
+ formValues,
339
+ setValue
340
+ });
341
+ };
342
+ const FieldRenderer = ({ config }) => {
343
+ const { form, visibility, fieldWrapper } = useDynamicFormContext();
344
+ const { field, fieldState } = (0, react_hook_form.useController)({
345
+ name: config.name,
346
+ control: form.control
347
+ });
348
+ if (!(visibility[config.name] !== false)) return null;
349
+ const formValues = form.getValues();
350
+ const setValue = (name, value) => form.setValue(name, value);
351
+ let fieldElement;
352
+ if (isCustomFieldElement(config)) fieldElement = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomFieldRenderer, {
353
+ config,
354
+ field,
355
+ fieldState,
356
+ formValues,
357
+ setValue
358
+ });
359
+ else fieldElement = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StandardFieldRenderer, {
360
+ config,
361
+ field,
362
+ fieldState,
363
+ formValues,
364
+ setValue
365
+ });
366
+ if (fieldWrapper) return fieldWrapper({
367
+ name: config.name,
368
+ config,
369
+ fieldState,
370
+ value: field.value,
371
+ formValues,
372
+ setValue
373
+ }, fieldElement);
374
+ return fieldElement;
375
+ };
376
+ FieldRenderer.displayName = "FieldRenderer";
377
+
378
+ //#endregion
379
+ //#region src/components/ElementRenderer.tsx
380
+ /**
381
+ * Dispatches rendering to the appropriate component based on element type.
382
+ *
383
+ * Supports field elements (Phase 1) and container/column layouts (Phase 2).
384
+ *
385
+ * @example
386
+ * ```tsx
387
+ * // Field element
388
+ * <ElementRenderer element={{ type: 'text', name: 'name', label: 'Name' }} />
389
+ *
390
+ * // Container element with columns
391
+ * <ElementRenderer element={{
392
+ * type: 'container',
393
+ * columns: [
394
+ * { type: 'column', width: '50%', elements: [...] }
395
+ * ]
396
+ * }} />
397
+ * ```
398
+ */
399
+ const ElementRenderer = ({ element }) => {
400
+ if (isFieldElement(element)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldRenderer, { config: element });
401
+ if (isContainerElement(element)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContainerRenderer, { config: element });
402
+ if (isColumnElement(element)) {
403
+ console.warn("Column elements should not be rendered directly. They should be children of a container element.");
404
+ return null;
405
+ }
406
+ console.warn(`Unknown element type: ${element.type}`);
407
+ return null;
408
+ };
409
+ ElementRenderer.displayName = "ElementRenderer";
410
+
411
+ //#endregion
412
+ //#region src/components/ColumnRenderer.tsx
413
+ /**
414
+ * Renders a column element with its nested form elements.
415
+ *
416
+ * The ColumnRenderer:
417
+ * 1. Applies the configured width to the column wrapper
418
+ * 2. Recursively renders nested elements via ElementRenderer
419
+ *
420
+ * Columns support nested containers, enabling complex layout hierarchies.
421
+ *
422
+ * @example
423
+ * ```tsx
424
+ * <ColumnRenderer
425
+ * config={{
426
+ * type: 'column',
427
+ * width: '50%',
428
+ * elements: [
429
+ * { type: 'text', name: 'firstName', label: 'First Name' },
430
+ * { type: 'text', name: 'lastName', label: 'Last Name' }
431
+ * ]
432
+ * }}
433
+ * />
434
+ * ```
435
+ */
436
+ const ColumnRenderer = ({ config }) => {
437
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
438
+ style: {
439
+ flex: `0 1 ${config.width}`,
440
+ maxWidth: config.width,
441
+ minWidth: 0,
442
+ boxSizing: "border-box"
443
+ },
444
+ children: config.elements.map((element, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ElementRenderer, { element }, "name" in element ? element.name : `element-${index}`))
445
+ });
446
+ };
447
+ ColumnRenderer.displayName = "ColumnRenderer";
448
+
449
+ //#endregion
450
+ //#region src/components/FormRenderer.tsx
451
+ /**
452
+ * Renders all form elements from the configuration.
453
+ *
454
+ * Maps over the elements array and renders each element using ElementRenderer.
455
+ * Elements are rendered vertically (one under another) in Phase 1.
456
+ *
457
+ * @example
458
+ * ```tsx
459
+ * const elements = [
460
+ * { type: 'text', name: 'name', label: 'Name' },
461
+ * { type: 'email', name: 'email', label: 'Email' },
462
+ * ];
463
+ *
464
+ * <FormRenderer elements={elements} />
465
+ * ```
466
+ */
467
+ const FormRenderer = ({ elements }) => {
468
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: elements.map((element, index) => {
469
+ const key = "name" in element && element.name ? element.name : `element-${index}`;
470
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ElementRenderer, { element }, key);
471
+ }) });
472
+ };
473
+ FormRenderer.displayName = "FormRenderer";
474
+
475
+ //#endregion
476
+ //#region src/parser/configValidator.ts
477
+ /**
478
+ * JSON Logic rule schema - accepts any object structure.
479
+ * Actual JSON Logic validation happens at runtime.
480
+ */
481
+ const jsonLogicRuleSchema = zod.z.record(zod.z.string(), zod.z.unknown());
482
+ /**
483
+ * Validation configuration schema.
484
+ */
485
+ const validationConfigSchema = zod.z.object({
486
+ required: zod.z.boolean().optional(),
487
+ type: zod.z.enum([
488
+ "number",
489
+ "email",
490
+ "date"
491
+ ]).optional(),
492
+ minLength: zod.z.number().int().min(0).optional(),
493
+ maxLength: zod.z.number().int().min(0).optional(),
494
+ pattern: zod.z.string().optional(),
495
+ message: zod.z.string().optional(),
496
+ condition: jsonLogicRuleSchema.optional()
497
+ }).strict().optional();
498
+ /**
499
+ * Base field element schema (common properties).
500
+ */
501
+ const baseFieldSchema = zod.z.object({
502
+ name: zod.z.string().min(1, "Field name is required"),
503
+ label: zod.z.string().optional(),
504
+ placeholder: zod.z.string().optional(),
505
+ defaultValue: zod.z.union([
506
+ zod.z.string(),
507
+ zod.z.number(),
508
+ zod.z.boolean(),
509
+ zod.z.null(),
510
+ zod.z.array(zod.z.unknown()),
511
+ zod.z.record(zod.z.string(), zod.z.unknown())
512
+ ]).optional(),
513
+ validation: validationConfigSchema,
514
+ visible: jsonLogicRuleSchema.optional(),
515
+ dependsOn: zod.z.string().optional(),
516
+ resetOnParentChange: zod.z.boolean().optional()
517
+ });
518
+ /**
519
+ * Text field element schema.
520
+ */
521
+ const textFieldSchema = baseFieldSchema.extend({ type: zod.z.literal("text") });
522
+ /**
523
+ * Email field element schema.
524
+ */
525
+ const emailFieldSchema = baseFieldSchema.extend({ type: zod.z.literal("email") });
526
+ /**
527
+ * Boolean field element schema.
528
+ */
529
+ const booleanFieldSchema = baseFieldSchema.extend({ type: zod.z.literal("boolean") });
530
+ /**
531
+ * Phone field element schema.
532
+ */
533
+ const phoneFieldSchema = baseFieldSchema.extend({ type: zod.z.literal("phone") });
534
+ /**
535
+ * Date field element schema.
536
+ */
537
+ const dateFieldSchema = baseFieldSchema.extend({ type: zod.z.literal("date") });
538
+ /**
539
+ * Select option schema.
540
+ */
541
+ const selectOptionSchema = zod.z.object({
542
+ value: zod.z.union([zod.z.string(), zod.z.number()]),
543
+ label: zod.z.string(),
544
+ disabled: zod.z.boolean().optional()
545
+ });
546
+ /**
547
+ * Options source schema - describes how to resolve options.
548
+ */
549
+ const optionsSourceSchema = zod.z.discriminatedUnion("type", [
550
+ zod.z.object({ type: zod.z.literal("static") }),
551
+ zod.z.object({
552
+ type: zod.z.literal("map"),
553
+ key: zod.z.string()
554
+ }),
555
+ zod.z.object({
556
+ type: zod.z.literal("api"),
557
+ endpoint: zod.z.string()
558
+ }),
559
+ zod.z.object({
560
+ type: zod.z.literal("search"),
561
+ endpoint: zod.z.string(),
562
+ minChars: zod.z.number().optional()
563
+ }),
564
+ zod.z.object({
565
+ type: zod.z.literal("resolver"),
566
+ name: zod.z.string()
567
+ })
568
+ ]);
569
+ /**
570
+ * Select field element schema.
571
+ * Options are required when optionsSource is not provided.
572
+ */
573
+ const selectFieldSchema = baseFieldSchema.extend({
574
+ type: zod.z.literal("select"),
575
+ options: zod.z.array(selectOptionSchema).optional(),
576
+ optionsSource: optionsSourceSchema.optional(),
577
+ multiple: zod.z.boolean().optional(),
578
+ clearable: zod.z.boolean().optional(),
579
+ searchable: zod.z.boolean().optional(),
580
+ creatable: zod.z.boolean().optional()
581
+ }).refine((data) => {
582
+ if (!data.optionsSource || data.optionsSource.type === "static") return data.options !== void 0 && data.options.length >= 0;
583
+ return true;
584
+ }, { message: "Options are required when optionsSource is not provided" });
585
+ /**
586
+ * Custom field element schema.
587
+ */
588
+ const customFieldSchema = baseFieldSchema.extend({
589
+ type: zod.z.literal("custom"),
590
+ component: zod.z.string().min(1, "Custom component name is required"),
591
+ componentProps: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
592
+ });
593
+ /**
594
+ * Array field element schema.
595
+ * Contains repeatable group of fields.
596
+ */
597
+ const arrayFieldSchema = baseFieldSchema.extend({
598
+ type: zod.z.literal("array"),
599
+ itemFields: zod.z.lazy(() => zod.z.array(fieldElementSchema)),
600
+ minItems: zod.z.number().int().min(0).optional(),
601
+ maxItems: zod.z.number().int().min(0).optional(),
602
+ addButtonLabel: zod.z.string().optional(),
603
+ sortable: zod.z.boolean().optional()
604
+ }).refine((data) => {
605
+ if (data.minItems !== void 0 && data.maxItems !== void 0) return data.minItems <= data.maxItems;
606
+ return true;
607
+ }, { message: "minItems must be less than or equal to maxItems" });
608
+ /**
609
+ * Field element schema - union of all field types.
610
+ */
611
+ const fieldElementSchema = zod.z.discriminatedUnion("type", [
612
+ textFieldSchema,
613
+ emailFieldSchema,
614
+ booleanFieldSchema,
615
+ phoneFieldSchema,
616
+ dateFieldSchema,
617
+ selectFieldSchema,
618
+ customFieldSchema,
619
+ arrayFieldSchema
620
+ ]);
621
+ /**
622
+ * Form element schema - for Phase 1, only field elements are supported.
623
+ * Phase 2 will add container and column schemas.
624
+ *
625
+ * We use a lazy schema to allow for future recursive definitions
626
+ * (containers containing columns containing elements).
627
+ */
628
+ const formElementSchema = zod.z.lazy(() => zod.z.union([
629
+ fieldElementSchema,
630
+ containerElementSchema,
631
+ columnElementSchema
632
+ ]));
633
+ /**
634
+ * Column element schema (for Phase 2, but defined here for type completeness).
635
+ */
636
+ const columnElementSchema = zod.z.object({
637
+ type: zod.z.literal("column"),
638
+ width: zod.z.string().min(1, "Column width is required"),
639
+ elements: zod.z.array(zod.z.lazy(() => formElementSchema)),
640
+ visible: jsonLogicRuleSchema.optional()
641
+ });
642
+ /**
643
+ * Container element schema (for Phase 2).
644
+ */
645
+ const containerElementSchema = zod.z.object({
646
+ type: zod.z.literal("container"),
647
+ columns: zod.z.array(columnElementSchema),
648
+ visible: jsonLogicRuleSchema.optional()
649
+ });
650
+ /**
651
+ * Custom component definition schema.
652
+ */
653
+ const customComponentDefinitionSchema = zod.z.object({ defaultProps: zod.z.record(zod.z.string(), zod.z.unknown()).optional() });
654
+ /**
655
+ * Root form configuration schema.
656
+ */
657
+ const formConfigurationSchema = zod.z.object({
658
+ name: zod.z.string().optional(),
659
+ elements: zod.z.array(formElementSchema).min(1, "At least one element is required"),
660
+ customComponents: zod.z.record(zod.z.string(), customComponentDefinitionSchema).optional()
661
+ });
662
+ /**
663
+ * Validates a form configuration object.
664
+ *
665
+ * @param config - Configuration object to validate
666
+ * @returns Validated and typed configuration
667
+ * @throws ZodError if validation fails
668
+ */
669
+ const validateConfiguration = (config) => {
670
+ return formConfigurationSchema.parse(config);
671
+ };
672
+ /**
673
+ * Safely validates a form configuration object without throwing.
674
+ *
675
+ * @param config - Configuration object to validate
676
+ * @returns Result object with success status and data or error
677
+ */
678
+ const safeValidateConfiguration = (config) => {
679
+ return formConfigurationSchema.safeParse(config);
680
+ };
681
+
682
+ //#endregion
683
+ //#region src/parser/configParser.ts
684
+ /**
685
+ * Error thrown when configuration parsing fails.
686
+ */
687
+ var ConfigurationError = class extends Error {
688
+ /** The original validation errors */
689
+ errors;
690
+ constructor(message, errors) {
691
+ super(message);
692
+ this.name = "ConfigurationError";
693
+ this.errors = errors;
694
+ }
695
+ };
696
+ /**
697
+ * Parses and validates a form configuration.
698
+ *
699
+ * @param config - Raw configuration object (typically from JSON)
700
+ * @returns Validated FormConfiguration
701
+ * @throws ConfigurationError if validation fails
702
+ *
703
+ * @example
704
+ * ```typescript
705
+ * try {
706
+ * const config = parseConfiguration({
707
+ * elements: [
708
+ * { type: 'text', name: 'name', label: 'Name' }
709
+ * ]
710
+ * });
711
+ * // config is now typed as FormConfiguration
712
+ * } catch (error) {
713
+ * if (error instanceof ConfigurationError) {
714
+ * console.error('Invalid configuration:', error.errors);
715
+ * }
716
+ * }
717
+ * ```
718
+ */
719
+ const parseConfiguration = (config) => {
720
+ try {
721
+ return validateConfiguration(config);
722
+ } catch (error) {
723
+ if (error && typeof error === "object" && "issues" in error) throw new ConfigurationError("Invalid form configuration", error.issues);
724
+ throw error;
725
+ }
726
+ };
727
+ /**
728
+ * Safely parses and validates a form configuration without throwing.
729
+ *
730
+ * @param config - Raw configuration object
731
+ * @returns ParseResult with success status and config or errors
732
+ *
733
+ * @example
734
+ * ```typescript
735
+ * const result = safeParseConfiguration(rawConfig);
736
+ * if (result.success) {
737
+ * // result.config is available
738
+ * renderForm(result.config);
739
+ * } else {
740
+ * // result.errors contains validation messages
741
+ * showErrors(result.errors);
742
+ * }
743
+ * ```
744
+ */
745
+ const safeParseConfiguration = (config) => {
746
+ const result = safeValidateConfiguration(config);
747
+ if (result.success) return {
748
+ success: true,
749
+ config: result.data
750
+ };
751
+ return {
752
+ success: false,
753
+ errors: result.error.issues.map((issue) => {
754
+ const path = issue.path.join(".");
755
+ return path ? `${path}: ${issue.message}` : issue.message;
756
+ })
757
+ };
758
+ };
759
+
760
+ //#endregion
761
+ //#region src/resolver/visibilityAwareResolver.ts
762
+ /**
763
+ * Check if a value is a leaf error node.
764
+ * React-hook-form leaf errors have both 'type' and 'message' properties.
765
+ */
766
+ const isLeafError = (value) => value !== null && typeof value === "object" && "type" in value && "message" in value;
767
+ /**
768
+ * Create a warning error from an existing error object.
769
+ */
770
+ const createWarningError = (error) => ({
771
+ ...error,
772
+ type: "warning"
773
+ });
774
+ /**
775
+ * Process a single error entry based on visibility rules.
776
+ * Returns the error to include or undefined to skip.
777
+ */
778
+ const processLeafError = (value, path, visibility, warnMode) => {
779
+ if (visibility[path] !== false) return value;
780
+ if (warnMode) return createWarningError(value);
781
+ };
782
+ /**
783
+ * Recursively filter errors based on field visibility.
784
+ * Handles nested error structures for dot-notation paths.
785
+ */
786
+ const filterErrorsByVisibility = (errors, visibility, warnMode, parentPath = "") => {
787
+ const acc = {};
788
+ for (const [key, value] of Object.entries(errors)) {
789
+ const path = parentPath ? `${parentPath}.${key}` : key;
790
+ if (!isLeafError(value) && value && typeof value === "object") {
791
+ const nested = filterErrorsByVisibility(value, visibility, warnMode, path);
792
+ if (Object.keys(nested).length > 0) acc[key] = nested;
793
+ continue;
794
+ }
795
+ const processed = processLeafError(value, path, visibility, warnMode);
796
+ if (processed !== void 0) acc[key] = processed;
797
+ }
798
+ return acc;
799
+ };
800
+ /**
801
+ * Creates a resolver that respects field visibility.
802
+ *
803
+ * This resolver wraps the standard zodResolver and filters validation
804
+ * errors based on field visibility. This is useful when fields are
805
+ * conditionally shown/hidden and you want to skip validation for
806
+ * hidden fields.
807
+ *
808
+ * @param options - Configuration for the resolver
809
+ * @returns A react-hook-form resolver
810
+ *
811
+ * @example
812
+ * ```typescript
813
+ * const resolver = createVisibilityAwareResolver({
814
+ * schema: myZodSchema,
815
+ * getVisibility: () => ({ name: true, phone: false }),
816
+ * invisibleFieldValidation: "skip",
817
+ * });
818
+ *
819
+ * const form = useForm({ resolver });
820
+ * ```
821
+ */
822
+ const createVisibilityAwareResolver = (options) => {
823
+ const baseResolver = (0, _hookform_resolvers_zod.zodResolver)(options.schema);
824
+ return async (values, context, resolverOptions) => {
825
+ const result = await baseResolver(values, context, resolverOptions);
826
+ if (!result.errors || options.invisibleFieldValidation === "validate") return result;
827
+ const visibility = options.getVisibility();
828
+ const warnMode = options.invisibleFieldValidation === "warn";
829
+ const filteredErrors = filterErrorsByVisibility(result.errors, visibility, warnMode);
830
+ return {
831
+ values: result.values,
832
+ errors: filteredErrors
833
+ };
834
+ };
835
+ };
836
+
837
+ //#endregion
838
+ //#region src/schema/fieldSchemas.ts
839
+ /**
840
+ * Build the base Zod schema for a select field.
841
+ *
842
+ * @param field - Select field configuration
843
+ * @returns Base Zod schema for the select field
844
+ */
845
+ const buildSelectSchema = (field) => {
846
+ if (field.multiple) return zod.z.array(zod.z.union([zod.z.string(), zod.z.number()]));
847
+ return zod.z.union([
848
+ zod.z.string(),
849
+ zod.z.number(),
850
+ zod.z.null()
851
+ ]);
852
+ };
853
+ /**
854
+ * Build the base Zod schema for an array field.
855
+ * Recursively generates schema from itemFields.
856
+ *
857
+ * @param field - Array field configuration
858
+ * @returns Base Zod schema for the array field
859
+ */
860
+ const buildArraySchema = (field) => {
861
+ const itemShape = {};
862
+ for (const itemField of field.itemFields) itemShape[itemField.name] = buildFieldSchema(itemField);
863
+ let arraySchema = zod.z.array(zod.z.object(itemShape));
864
+ if (field.minItems !== void 0) arraySchema = arraySchema.min(field.minItems, `At least ${field.minItems} item(s) required`);
865
+ if (field.maxItems !== void 0) arraySchema = arraySchema.max(field.maxItems, `Maximum ${field.maxItems} item(s) allowed`);
866
+ return arraySchema;
867
+ };
868
+ /**
869
+ * Build the base Zod schema for a field based on its type.
870
+ *
871
+ * @param field - The field element configuration
872
+ * @returns Base Zod schema for the field type
873
+ */
874
+ const buildBaseSchema = (field) => {
875
+ switch (field.type) {
876
+ case "text":
877
+ case "phone": return zod.z.string();
878
+ case "email": return zod.z.string().email("Invalid email address");
879
+ case "boolean": return zod.z.boolean();
880
+ case "date": return zod.z.string();
881
+ case "select": return buildSelectSchema(field);
882
+ case "array": return buildArraySchema(field);
883
+ case "custom": return zod.z.unknown();
884
+ default: return zod.z.unknown();
885
+ }
886
+ };
887
+ /**
888
+ * Apply validation rules to a string schema.
889
+ *
890
+ * @param schema - Base string schema
891
+ * @param validation - Validation configuration
892
+ * @returns Schema with validation rules applied
893
+ */
894
+ const applyStringValidation = (schema, validation) => {
895
+ let result = schema;
896
+ if (validation.required) result = result.min(1, "This field is required");
897
+ if (validation.minLength !== void 0) result = result.min(validation.minLength, `Must be at least ${validation.minLength} characters`);
898
+ if (validation.maxLength !== void 0) result = result.max(validation.maxLength, `Must be no more than ${validation.maxLength} characters`);
899
+ if (validation.pattern) try {
900
+ const regex = new RegExp(validation.pattern);
901
+ result = result.regex(regex, validation.message || "Invalid format");
902
+ } catch {
903
+ console.warn(`Invalid regex pattern: ${validation.pattern}`);
904
+ }
905
+ return result;
906
+ };
907
+ /**
908
+ * Apply validation rules to a boolean schema.
909
+ *
910
+ * @param schema - Base boolean schema
911
+ * @param validation - Validation configuration
912
+ * @returns Schema with validation rules applied
913
+ */
914
+ const applyBooleanValidation = (schema, validation) => {
915
+ if (validation.required) return schema.refine((val) => val === true, { message: "This field is required" });
916
+ return schema;
917
+ };
918
+ /**
919
+ * Apply validation rules to a select schema.
920
+ *
921
+ * @param schema - Base select schema
922
+ * @param validation - Validation configuration
923
+ * @param isMultiple - Whether this is a multi-select
924
+ * @returns Schema with validation rules applied
925
+ */
926
+ const applySelectValidation = (schema, validation, isMultiple) => {
927
+ if (validation.required && isMultiple) return schema.min(1, "At least one selection is required");
928
+ if (validation.required && !isMultiple) return schema.refine((val) => val !== null && val !== void 0, { message: "This field is required" });
929
+ return schema;
930
+ };
931
+ /**
932
+ * Apply validation configuration to a Zod schema based on field type.
933
+ *
934
+ * @param schema - Base Zod schema
935
+ * @param validation - Validation configuration
936
+ * @param field - Field element configuration
937
+ * @returns Schema with validation rules applied
938
+ */
939
+ const applyValidationRules = (schema, validation, field) => {
940
+ const fieldType = field.type;
941
+ if (fieldType === "text" || fieldType === "phone" || fieldType === "email" || fieldType === "date") return applyStringValidation(schema, validation);
942
+ if (fieldType === "boolean") return applyBooleanValidation(schema, validation);
943
+ if (fieldType === "select") return applySelectValidation(schema, validation, field.multiple ?? false);
944
+ return schema;
945
+ };
946
+ /**
947
+ * Build a complete Zod schema for a single field.
948
+ *
949
+ * @param field - Field element configuration
950
+ * @returns Zod schema for the field
951
+ *
952
+ * @example
953
+ * ```typescript
954
+ * const textField = {
955
+ * type: 'text',
956
+ * name: 'name',
957
+ * validation: { required: true, minLength: 3 }
958
+ * };
959
+ *
960
+ * const schema = buildFieldSchema(textField);
961
+ * // schema is z.string().min(1, 'required').min(3, '...')
962
+ * ```
963
+ */
964
+ const buildFieldSchema = (field) => {
965
+ let schema = buildBaseSchema(field);
966
+ if (field.validation) schema = applyValidationRules(schema, field.validation, field);
967
+ return schema;
968
+ };
969
+
970
+ //#endregion
971
+ //#region src/validation/jsonLogic.ts
972
+ /**
973
+ * Register custom JSON Logic operations.
974
+ * Called once at module initialization.
975
+ */
976
+ const registerCustomOperations = () => {
977
+ /**
978
+ * regex_match: Tests if a value matches a regex pattern.
979
+ *
980
+ * @example
981
+ * ```json
982
+ * { "regex_match": ["^[0-9]{10}$", { "var": "phone" }] }
983
+ * ```
984
+ */
985
+ json_logic_js.default.add_operation("regex_match", (pattern, value) => {
986
+ if (typeof value !== "string" || typeof pattern !== "string") return false;
987
+ try {
988
+ return new RegExp(pattern).test(value);
989
+ } catch {
990
+ return false;
991
+ }
992
+ });
993
+ };
994
+ registerCustomOperations();
995
+ /**
996
+ * Evaluate a JSON Logic rule against form data.
997
+ *
998
+ * @param rule - JSON Logic rule to evaluate
999
+ * @param data - Form data to evaluate against
1000
+ * @returns Result of the evaluation
1001
+ *
1002
+ * @example
1003
+ * ```typescript
1004
+ * const rule = { "==": [{ var: "status" }, "active"] };
1005
+ * const data = { status: "active" };
1006
+ * applyJsonLogic(rule, data); // true
1007
+ * ```
1008
+ */
1009
+ const applyJsonLogic = (rule, data) => {
1010
+ return json_logic_js.default.apply(rule, data);
1011
+ };
1012
+ /**
1013
+ * Evaluate a JSON Logic rule and return boolean result.
1014
+ * Returns true if rule evaluates to a truthy value.
1015
+ *
1016
+ * @param rule - JSON Logic condition
1017
+ * @param data - Form data
1018
+ * @returns true if condition passes, false otherwise
1019
+ *
1020
+ * @example
1021
+ * ```typescript
1022
+ * const rule = { "and": [{ var: "active" }, { var: "confirmed" }] };
1023
+ * evaluateCondition(rule, { active: true, confirmed: true }); // true
1024
+ * evaluateCondition(rule, { active: true, confirmed: false }); // false
1025
+ * ```
1026
+ */
1027
+ const evaluateCondition = (rule, data) => {
1028
+ const result = applyJsonLogic(rule, data);
1029
+ return Boolean(result);
1030
+ };
1031
+
1032
+ //#endregion
1033
+ //#region src/utils/calculateVisibility.ts
1034
+ /**
1035
+ * Compare two visibility states and return the new state only if changed.
1036
+ * Returns prev if no change (preserves reference equality for React).
1037
+ */
1038
+ const getUpdatedVisibility = (prev, next) => {
1039
+ const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
1040
+ for (const key of keys) if (prev[key] !== next[key]) return next;
1041
+ return prev;
1042
+ };
1043
+ /**
1044
+ * Calculate visibility state for all fields based on their visibility rules.
1045
+ * Evaluates JSON Logic rules against current form data.
1046
+ *
1047
+ * @param elements - Array of form elements
1048
+ * @param formData - Current form values
1049
+ * @returns Visibility state for all fields
1050
+ *
1051
+ * @example
1052
+ * ```typescript
1053
+ * const elements = [
1054
+ * { type: 'text', name: 'reason', visible: { "==": [{ "var": "type" }, "other"] } },
1055
+ * { type: 'text', name: 'name' }
1056
+ * ];
1057
+ *
1058
+ * const visibility = calculateVisibility(elements, { type: 'other' });
1059
+ * // Returns: { reason: true, name: true }
1060
+ *
1061
+ * const visibility2 = calculateVisibility(elements, { type: 'standard' });
1062
+ * // Returns: { reason: false, name: true }
1063
+ * ```
1064
+ */
1065
+ const calculateVisibility = (elements, formData) => {
1066
+ const visibility = {};
1067
+ const processElement = (element, parentVisible) => {
1068
+ if (isFieldElement(element)) processField(element, parentVisible);
1069
+ else if (isContainerElement(element)) processContainer(element, parentVisible);
1070
+ else if (isColumnElement(element)) processColumn(element, parentVisible);
1071
+ };
1072
+ const processField = (field, parentVisible) => {
1073
+ if (!parentVisible) {
1074
+ visibility[field.name] = false;
1075
+ return;
1076
+ }
1077
+ if (!field.visible) {
1078
+ visibility[field.name] = true;
1079
+ return;
1080
+ }
1081
+ visibility[field.name] = evaluateCondition(field.visible, formData);
1082
+ };
1083
+ const processContainer = (container, parentVisible) => {
1084
+ let containerVisible = parentVisible;
1085
+ if (container.visible && parentVisible) containerVisible = evaluateCondition(container.visible, formData);
1086
+ for (const column of container.columns) processColumn(column, containerVisible);
1087
+ };
1088
+ const processColumn = (column, parentVisible) => {
1089
+ let columnVisible = parentVisible;
1090
+ if (column.visible && parentVisible) columnVisible = evaluateCondition(column.visible, formData);
1091
+ for (const element of column.elements) processElement(element, columnVisible);
1092
+ };
1093
+ for (const element of elements) processElement(element, true);
1094
+ return visibility;
1095
+ };
1096
+
1097
+ //#endregion
1098
+ //#region src/utils/flattenFields.ts
1099
+ /**
1100
+ * Recursively extracts all field elements from a form configuration.
1101
+ * Traverses containers and columns to find nested fields.
1102
+ *
1103
+ * @param elements - Array of form elements (may include containers/columns)
1104
+ * @returns Flat array of all field elements
1105
+ *
1106
+ * @example
1107
+ * ```typescript
1108
+ * const config = {
1109
+ * elements: [
1110
+ * { type: 'text', name: 'name' },
1111
+ * {
1112
+ * type: 'container',
1113
+ * columns: [{
1114
+ * type: 'column',
1115
+ * width: '50%',
1116
+ * elements: [{ type: 'email', name: 'email' }]
1117
+ * }]
1118
+ * }
1119
+ * ]
1120
+ * };
1121
+ *
1122
+ * const fields = flattenFields(config.elements);
1123
+ * // Returns: [{ type: 'text', name: 'name' }, { type: 'email', name: 'email' }]
1124
+ * ```
1125
+ */
1126
+ const flattenFields = (elements) => {
1127
+ const fields = [];
1128
+ const processElement = (element) => {
1129
+ if (isFieldElement(element)) fields.push(element);
1130
+ else if (isContainerElement(element)) processContainer(element);
1131
+ else if (isColumnElement(element)) processColumn(element);
1132
+ };
1133
+ const processContainer = (container) => {
1134
+ for (const column of container.columns) processColumn(column);
1135
+ };
1136
+ const processColumn = (column) => {
1137
+ for (const element of column.elements) processElement(element);
1138
+ };
1139
+ for (const element of elements) processElement(element);
1140
+ return fields;
1141
+ };
1142
+ /**
1143
+ * Gets all field names from a form configuration.
1144
+ * Useful for initializing form state or visibility tracking.
1145
+ *
1146
+ * @param elements - Array of form elements
1147
+ * @returns Array of field names (including nested paths like 'source.name')
1148
+ */
1149
+ const getFieldNames = (elements) => {
1150
+ return flattenFields(elements).map((field) => field.name);
1151
+ };
1152
+
1153
+ //#endregion
1154
+ //#region src/utils/dependencies.ts
1155
+ /**
1156
+ * Builds a dependency map from form elements.
1157
+ * Maps parent field names to their dependent children.
1158
+ *
1159
+ * @param elements - Array of form elements
1160
+ * @returns Map of parent field names to dependent field names
1161
+ *
1162
+ * @example
1163
+ * ```typescript
1164
+ * const elements = [
1165
+ * { type: 'select', name: 'country', options: [...] },
1166
+ * { type: 'select', name: 'city', dependsOn: 'country', options: [...] }
1167
+ * ];
1168
+ *
1169
+ * const map = buildDependencyMap(elements);
1170
+ * // Returns: { country: ['city'] }
1171
+ * ```
1172
+ */
1173
+ const buildDependencyMap = (elements) => {
1174
+ const map = {};
1175
+ const fields = flattenFields(elements);
1176
+ for (const field of fields) if (field.dependsOn) {
1177
+ const parent = field.dependsOn;
1178
+ if (!map[parent]) map[parent] = [];
1179
+ map[parent].push(field.name);
1180
+ }
1181
+ return map;
1182
+ };
1183
+ /**
1184
+ * Finds a field by name in the form elements.
1185
+ *
1186
+ * @param elements - Array of form elements
1187
+ * @param name - Field name to find
1188
+ * @returns Field element if found, undefined otherwise
1189
+ */
1190
+ const findFieldByName = (elements, name) => {
1191
+ return flattenFields(elements).find((field) => field.name === name);
1192
+ };
1193
+ /**
1194
+ * Gets the default value for a field based on its type.
1195
+ * Used when resetting dependent fields.
1196
+ *
1197
+ * @param field - Field element
1198
+ * @returns Default value appropriate for the field type
1199
+ */
1200
+ const getFieldTypeDefault = (field) => {
1201
+ switch (field.type) {
1202
+ case "boolean": return false;
1203
+ case "select": return field.multiple ? [] : null;
1204
+ case "array": return [];
1205
+ default: return "";
1206
+ }
1207
+ };
1208
+ /**
1209
+ * Gets the effective default value for a field.
1210
+ * Priority: config.defaultValue > type default
1211
+ *
1212
+ * @param field - Field element
1213
+ * @returns Default value to use when resetting
1214
+ */
1215
+ const getFieldDefault = (field) => {
1216
+ if (field.defaultValue !== void 0) return field.defaultValue;
1217
+ return getFieldTypeDefault(field);
1218
+ };
1219
+
1220
+ //#endregion
1221
+ //#region src/utils/mergeDefaults.ts
1222
+ /**
1223
+ * Sets a value in a nested object using dot notation path.
1224
+ *
1225
+ * @param obj - Object to modify
1226
+ * @param path - Dot-notation path (e.g., 'source.name')
1227
+ * @param value - Value to set
1228
+ *
1229
+ * @example
1230
+ * ```typescript
1231
+ * const obj = {};
1232
+ * setNestedValue(obj, 'source.name', 'John');
1233
+ * // obj is now { source: { name: 'John' } }
1234
+ * ```
1235
+ */
1236
+ const setNestedValue = (obj, path, value) => {
1237
+ const parts = path.split(".");
1238
+ let current = obj;
1239
+ for (let i = 0; i < parts.length - 1; i++) {
1240
+ const part = parts[i];
1241
+ if (!(part in current) || typeof current[part] !== "object" || current[part] === null) current[part] = {};
1242
+ current = current[part];
1243
+ }
1244
+ const lastPart = parts.at(-1);
1245
+ if (lastPart !== void 0) current[lastPart] = value;
1246
+ };
1247
+ /**
1248
+ * Gets a value from a nested object using dot notation path.
1249
+ *
1250
+ * @param obj - Object to read from
1251
+ * @param path - Dot-notation path (e.g., 'source.name')
1252
+ * @returns The value at the path, or undefined if not found
1253
+ *
1254
+ * @example
1255
+ * ```typescript
1256
+ * const obj = { source: { name: 'John' } };
1257
+ * getNestedValue(obj, 'source.name'); // 'John'
1258
+ * getNestedValue(obj, 'source.email'); // undefined
1259
+ * ```
1260
+ */
1261
+ const getNestedValue = (obj, path) => {
1262
+ const parts = path.split(".");
1263
+ let current = obj;
1264
+ for (const part of parts) {
1265
+ if (current === null || current === void 0 || typeof current !== "object") return;
1266
+ current = current[part];
1267
+ }
1268
+ return current;
1269
+ };
1270
+ /**
1271
+ * Deep merge two objects. Source values override target values.
1272
+ *
1273
+ * @param target - Base object
1274
+ * @param source - Object to merge in (takes precedence)
1275
+ * @returns New merged object
1276
+ */
1277
+ const deepMerge = (target, source) => {
1278
+ const result = { ...target };
1279
+ for (const key in source) if (Object.hasOwn(source, key)) {
1280
+ const sourceValue = source[key];
1281
+ const targetValue = result[key];
1282
+ if (sourceValue !== null && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue !== null && typeof targetValue === "object" && !Array.isArray(targetValue)) result[key] = deepMerge(targetValue, sourceValue);
1283
+ else result[key] = sourceValue;
1284
+ }
1285
+ return result;
1286
+ };
1287
+ /**
1288
+ * Gets the default value for a field based on its type.
1289
+ *
1290
+ * @param field - Field element
1291
+ * @returns Appropriate default value for the field type
1292
+ */
1293
+ const getTypeDefault = (field) => {
1294
+ switch (field.type) {
1295
+ case "boolean": return false;
1296
+ case "select": return field.multiple ? [] : null;
1297
+ case "array": return [];
1298
+ case "text":
1299
+ case "email":
1300
+ case "phone":
1301
+ case "date": return "";
1302
+ default: return "";
1303
+ }
1304
+ };
1305
+ /**
1306
+ * Merges configuration default values with initial data.
1307
+ * Priority: initialData > config.defaultValue > type default
1308
+ *
1309
+ * @param config - Form configuration containing field definitions
1310
+ * @param initialData - Initial data provided by the user
1311
+ * @returns Merged default values for react-hook-form
1312
+ *
1313
+ * @example
1314
+ * ```typescript
1315
+ * const config = {
1316
+ * elements: [
1317
+ * { type: 'text', name: 'source.name', defaultValue: 'Default Name' },
1318
+ * { type: 'boolean', name: 'source.active' }
1319
+ * ]
1320
+ * };
1321
+ *
1322
+ * const initialData = { source: { name: 'Provided Name' } };
1323
+ * const defaults = mergeDefaults(config, initialData);
1324
+ * // Result: { source: { name: 'Provided Name', active: false } }
1325
+ * ```
1326
+ */
1327
+ const mergeDefaults = (config, initialData) => {
1328
+ const defaults = {};
1329
+ const fields = flattenFields(config.elements);
1330
+ for (const field of fields) {
1331
+ const typeDefault = getTypeDefault(field);
1332
+ const configDefault = field.defaultValue ?? typeDefault;
1333
+ setNestedValue(defaults, field.name, configDefault);
1334
+ }
1335
+ if (initialData) return deepMerge(defaults, initialData);
1336
+ return defaults;
1337
+ };
1338
+
1339
+ //#endregion
1340
+ //#region src/schema/nestedPaths.ts
1341
+ /**
1342
+ * Sets a nested schema value using dot notation path.
1343
+ * Builds nested z.object structures as needed.
1344
+ *
1345
+ * @param shape - The shape object to modify
1346
+ * @param path - Dot-notation path (e.g., 'source.name')
1347
+ * @param schema - Zod schema to set at the path
1348
+ *
1349
+ * @example
1350
+ * ```typescript
1351
+ * const shape = {};
1352
+ * setNestedSchema(shape, 'source.name', z.string());
1353
+ * // shape is now: { source: ZodObject({ name: ZodString }) }
1354
+ *
1355
+ * setNestedSchema(shape, 'source.email', z.string().email());
1356
+ * // shape is now: { source: ZodObject({ name: ZodString, email: ZodString }) }
1357
+ * ```
1358
+ */
1359
+ const setNestedSchema = (shape, path, schema) => {
1360
+ const parts = path.split(".");
1361
+ if (parts.length === 1) {
1362
+ shape[path] = schema;
1363
+ return;
1364
+ }
1365
+ const [first, ...rest] = parts;
1366
+ const remainingPath = rest.join(".");
1367
+ if (!shape[first]) shape[first] = zod.z.object({});
1368
+ const existingSchema = shape[first];
1369
+ if (existingSchema instanceof zod.ZodObject) {
1370
+ const innerShape = { ...existingSchema.shape };
1371
+ setNestedSchema(innerShape, remainingPath, schema);
1372
+ shape[first] = zod.z.object(innerShape);
1373
+ } else {
1374
+ const innerShape = {};
1375
+ setNestedSchema(innerShape, remainingPath, schema);
1376
+ shape[first] = zod.z.object(innerShape);
1377
+ }
1378
+ };
1379
+
1380
+ //#endregion
1381
+ //#region src/schema/generateSchema.ts
1382
+ /**
1383
+ * Extract all JSON Logic validation conditions from fields.
1384
+ *
1385
+ * @param fields - Array of field elements
1386
+ * @returns Array of field conditions to evaluate
1387
+ */
1388
+ const collectConditions = (fields) => {
1389
+ const conditions = [];
1390
+ for (const field of fields) if (field.validation?.condition) conditions.push({
1391
+ fieldPath: field.name,
1392
+ condition: field.validation.condition,
1393
+ message: field.validation.message || "Validation failed"
1394
+ });
1395
+ return conditions;
1396
+ };
1397
+ /**
1398
+ * Generate a Zod schema from form configuration.
1399
+ * Supports nested field paths via dot notation.
1400
+ *
1401
+ * This function is called once when the form initializes and the schema
1402
+ * is memoized. Visibility changes are handled at validation time, not
1403
+ * by regenerating the schema.
1404
+ *
1405
+ * @param config - Form configuration object
1406
+ * @returns Zod object schema for validating form data
1407
+ *
1408
+ * @example
1409
+ * ```typescript
1410
+ * const config = {
1411
+ * elements: [
1412
+ * { type: 'text', name: 'source.name', validation: { required: true } },
1413
+ * { type: 'email', name: 'source.email' },
1414
+ * { type: 'boolean', name: 'active' }
1415
+ * ]
1416
+ * };
1417
+ *
1418
+ * const schema = generateZodSchema(config);
1419
+ *
1420
+ * // The generated schema is equivalent to:
1421
+ * // z.object({
1422
+ * // source: z.object({
1423
+ * // name: z.string().min(1, 'required'),
1424
+ * // email: z.string().email()
1425
+ * // }),
1426
+ * // active: z.boolean()
1427
+ * // })
1428
+ *
1429
+ * schema.parse({
1430
+ * source: { name: 'John', email: 'john@example.com' },
1431
+ * active: true
1432
+ * }); // Valid
1433
+ * ```
1434
+ */
1435
+ const generateZodSchema = (config) => {
1436
+ const fields = flattenFields(config.elements);
1437
+ const schemaShape = {};
1438
+ for (const field of fields) {
1439
+ const fieldSchema = buildFieldSchema(field);
1440
+ setNestedSchema(schemaShape, field.name, fieldSchema);
1441
+ }
1442
+ let schema = zod.z.object(schemaShape);
1443
+ const conditions = collectConditions(fields);
1444
+ if (conditions.length > 0) schema = schema.superRefine((data, ctx) => {
1445
+ for (const { fieldPath, condition, message } of conditions) if (!evaluateCondition(condition, data)) ctx.addIssue({
1446
+ code: zod.z.ZodIssueCode.custom,
1447
+ message,
1448
+ path: fieldPath.split(".")
1449
+ });
1450
+ });
1451
+ return schema;
1452
+ };
1453
+ /**
1454
+ * Extract field paths from a generated schema.
1455
+ * Returns all top-level and nested paths.
1456
+ *
1457
+ * @param schema - Generated Zod schema
1458
+ * @param prefix - Current path prefix (used in recursion)
1459
+ * @returns Array of all field paths
1460
+ */
1461
+ const getSchemaFieldPaths = (schema, prefix = "") => {
1462
+ const paths = [];
1463
+ const shape = schema.shape;
1464
+ for (const key in shape) if (Object.hasOwn(shape, key)) {
1465
+ const fullPath = prefix ? `${prefix}.${key}` : key;
1466
+ const fieldSchema = shape[key];
1467
+ if (fieldSchema instanceof zod.ZodObject) paths.push(...getSchemaFieldPaths(fieldSchema, fullPath));
1468
+ else paths.push(fullPath);
1469
+ }
1470
+ return paths;
1471
+ };
1472
+
1473
+ //#endregion
1474
+ //#region src/DynamicForm.tsx
1475
+ const DynamicForm = ({ config, initialData, fieldComponents, customComponents = {}, customContainers = {}, onSubmit, onChange, onValidationChange, onReset, onError, mode = "onChange", invisibleFieldValidation = "skip", className, style, id, children, fieldWrapper, ref }) => {
1476
+ const parsedConfig = (0, react.useMemo)(() => {
1477
+ return validateCustomComponents(parseConfiguration(config), customComponents);
1478
+ }, [config, customComponents]);
1479
+ const zodSchema = (0, react.useMemo)(() => generateZodSchema(parsedConfig), [parsedConfig]);
1480
+ const defaultValues = (0, react.useMemo)(() => mergeDefaults(parsedConfig, initialData), [parsedConfig, initialData]);
1481
+ const [visibility, setVisibility] = (0, react.useState)(() => calculateVisibility(parsedConfig.elements, defaultValues));
1482
+ const visibilityRef = (0, react.useRef)(visibility);
1483
+ visibilityRef.current = visibility;
1484
+ const onChangeRef = (0, react.useRef)(onChange);
1485
+ onChangeRef.current = onChange;
1486
+ const form = (0, react_hook_form.useForm)({
1487
+ defaultValues,
1488
+ resolver: (0, react.useMemo)(() => createVisibilityAwareResolver({
1489
+ schema: zodSchema,
1490
+ getVisibility: () => visibilityRef.current,
1491
+ invisibleFieldValidation
1492
+ }), [zodSchema, invisibleFieldValidation]),
1493
+ mode
1494
+ });
1495
+ (0, react.useImperativeHandle)(ref, () => ({
1496
+ getValues: () => form.getValues(),
1497
+ setValue: (name, value) => form.setValue(name, value),
1498
+ watchAll: () => form.watch(),
1499
+ watchField: (name) => form.watch(name),
1500
+ reset: (values) => form.reset(values ?? defaultValues),
1501
+ trigger: (name) => form.trigger(name)
1502
+ }), [form, defaultValues]);
1503
+ const dependencyMap = (0, react.useMemo)(() => buildDependencyMap(parsedConfig.elements), [parsedConfig]);
1504
+ const previousValuesRef = (0, react.useRef)({});
1505
+ (0, react.useEffect)(() => {
1506
+ const handleDependencyReset = (fieldName, formValues) => {
1507
+ const dependents = dependencyMap[fieldName];
1508
+ if (!dependents) return;
1509
+ const currentValue = getNestedValue(formValues, fieldName);
1510
+ if (currentValue === getNestedValue(previousValuesRef.current, fieldName)) return;
1511
+ setNestedValue(previousValuesRef.current, fieldName, currentValue);
1512
+ for (const dep of dependents) {
1513
+ const field = findFieldByName(parsedConfig.elements, dep);
1514
+ if (field && field.resetOnParentChange !== false) form.setValue(dep, getFieldDefault(field));
1515
+ }
1516
+ };
1517
+ const subscription = form.watch((values, { name }) => {
1518
+ const formValues = values;
1519
+ const newVisibility = calculateVisibility(parsedConfig.elements, formValues);
1520
+ setVisibility((prev) => getUpdatedVisibility(prev, newVisibility));
1521
+ if (!name) return;
1522
+ handleDependencyReset(name, formValues);
1523
+ onChangeRef.current?.(values, name);
1524
+ });
1525
+ return () => subscription.unsubscribe();
1526
+ }, [
1527
+ form,
1528
+ parsedConfig,
1529
+ dependencyMap
1530
+ ]);
1531
+ const { errors: formErrors, isValid: formIsValid } = (0, react_hook_form.useFormState)({ control: form.control });
1532
+ (0, react.useEffect)(() => {
1533
+ if (!onValidationChange) return;
1534
+ onValidationChange(formErrors, formIsValid);
1535
+ }, [
1536
+ formErrors,
1537
+ formIsValid,
1538
+ onValidationChange
1539
+ ]);
1540
+ const contextValue = (0, react.useMemo)(() => ({
1541
+ form,
1542
+ config: parsedConfig,
1543
+ fieldComponents,
1544
+ customComponents,
1545
+ customContainers,
1546
+ visibility,
1547
+ fieldWrapper
1548
+ }), [
1549
+ form,
1550
+ parsedConfig,
1551
+ fieldComponents,
1552
+ customComponents,
1553
+ customContainers,
1554
+ visibility,
1555
+ fieldWrapper
1556
+ ]);
1557
+ const handleSubmit = form.handleSubmit(onSubmit, (errors) => onError?.(errors));
1558
+ const handleReset = (0, react.useCallback)(() => {
1559
+ form.reset(defaultValues);
1560
+ onReset?.();
1561
+ }, [
1562
+ defaultValues,
1563
+ onReset,
1564
+ form
1565
+ ]);
1566
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_hook_form.FormProvider, {
1567
+ ...form,
1568
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DynamicFormContext.Provider, {
1569
+ value: contextValue,
1570
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1571
+ className,
1572
+ id,
1573
+ noValidate: true,
1574
+ onReset: handleReset,
1575
+ onSubmit: handleSubmit,
1576
+ style,
1577
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormRenderer, { elements: parsedConfig.elements }), children]
1578
+ })
1579
+ })
1580
+ });
1581
+ };
1582
+ DynamicForm.displayName = "DynamicForm";
1583
+ var DynamicForm_default = DynamicForm;
1584
+
1585
+ //#endregion
1586
+ exports.ConfigurationError = ConfigurationError;
1587
+ exports.DynamicForm = DynamicForm;
1588
+ exports.DynamicFormContext = DynamicFormContext;
1589
+ exports.applyJsonLogic = applyJsonLogic;
1590
+ exports.buildFieldSchema = buildFieldSchema;
1591
+ exports.calculateVisibility = calculateVisibility;
1592
+ exports.createVisibilityAwareResolver = createVisibilityAwareResolver;
1593
+ exports.default = DynamicForm_default;
1594
+ exports.defineCustomComponent = defineCustomComponent;
1595
+ exports.evaluateCondition = evaluateCondition;
1596
+ exports.flattenFields = flattenFields;
1597
+ exports.generateZodSchema = generateZodSchema;
1598
+ exports.getFieldNames = getFieldNames;
1599
+ exports.getNestedValue = getNestedValue;
1600
+ exports.getSchemaFieldPaths = getSchemaFieldPaths;
1601
+ exports.isArrayFieldElement = isArrayFieldElement;
1602
+ exports.isColumnElement = isColumnElement;
1603
+ exports.isContainerElement = isContainerElement;
1604
+ exports.isCustomFieldElement = isCustomFieldElement;
1605
+ exports.isFieldElement = isFieldElement;
1606
+ exports.mergeDefaults = mergeDefaults;
1607
+ exports.parseConfiguration = parseConfiguration;
1608
+ exports.safeParseConfiguration = safeParseConfiguration;
1609
+ exports.setNestedValue = setNestedValue;
1610
+ exports.useDynamicFormContext = useDynamicFormContext;
1611
+ exports.useDynamicFormContextSafe = useDynamicFormContextSafe;