hs-uix 1.0.0 → 1.0.1

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/form.js CHANGED
@@ -72,7 +72,132 @@ var isValueEmpty = (value, field) => {
72
72
  if ((field.type === "toggle" || field.type === "checkbox") && value === false) return true;
73
73
  return false;
74
74
  };
75
- var runValidators = (value, field, allValues, fieldTypes) => {
75
+ var isPromise = (value) => value && typeof value.then === "function";
76
+ var isAsyncFunction = (fn) => fn && fn.constructor && fn.constructor.name === "AsyncFunction";
77
+ var normalizeValidatorResult = (result) => {
78
+ if (result === true || result === void 0 || result === null || result === false) return null;
79
+ return String(result);
80
+ };
81
+ var isDateValueObject = (value) => isPlainObject(value) && Number.isInteger(value.year) && Number.isInteger(value.month) && Number.isInteger(value.date);
82
+ var isTimeValueObject = (value) => isPlainObject(value) && Number.isInteger(value.hours) && Number.isInteger(value.minutes);
83
+ var compareDateValues = (a, b) => {
84
+ if (a.year !== b.year) return a.year - b.year;
85
+ if (a.month !== b.month) return a.month - b.month;
86
+ return a.date - b.date;
87
+ };
88
+ var compareTimeValues = (a, b) => {
89
+ if (a.hours !== b.hours) return a.hours - b.hours;
90
+ return a.minutes - b.minutes;
91
+ };
92
+ var runDefaultFieldValidator = (value, field, allValues) => {
93
+ const errorPrefix = field.label || field.name;
94
+ switch (field.type) {
95
+ case "text":
96
+ case "password":
97
+ case "textarea":
98
+ if (typeof value !== "string") return `${errorPrefix} must be text`;
99
+ break;
100
+ case "number":
101
+ case "stepper":
102
+ case "currency":
103
+ if (typeof value !== "number" || Number.isNaN(value)) return `${errorPrefix} must be a number`;
104
+ break;
105
+ case "toggle":
106
+ case "checkbox":
107
+ if (typeof value !== "boolean") return `${errorPrefix} must be true or false`;
108
+ break;
109
+ case "select":
110
+ case "radioGroup": {
111
+ const options = resolveOptions(field, allValues);
112
+ if (options.length > 0 && !options.some((o) => Object.is(o.value, value))) {
113
+ return `${errorPrefix} has an invalid selection`;
114
+ }
115
+ break;
116
+ }
117
+ case "multiselect":
118
+ case "checkboxGroup": {
119
+ if (!Array.isArray(value)) return `${errorPrefix} must be a list`;
120
+ const options = resolveOptions(field, allValues);
121
+ if (options.length > 0) {
122
+ const validValues = new Set(options.map((o) => o.value));
123
+ const hasInvalid = value.some((item) => !validValues.has(item));
124
+ if (hasInvalid) return `${errorPrefix} has an invalid selection`;
125
+ }
126
+ break;
127
+ }
128
+ case "date":
129
+ if (!isDateValueObject(value)) return `${errorPrefix} has an invalid date`;
130
+ break;
131
+ case "time":
132
+ if (!isTimeValueObject(value)) return `${errorPrefix} has an invalid time`;
133
+ break;
134
+ case "datetime": {
135
+ if (isDateValueObject(value)) break;
136
+ if (!isPlainObject(value)) return `${errorPrefix} has an invalid date/time`;
137
+ const hasDate = value.date !== void 0;
138
+ const hasTime = value.time !== void 0;
139
+ if (!hasDate && !hasTime) return `${errorPrefix} has an invalid date/time`;
140
+ if (hasDate && !isDateValueObject(value.date)) return `${errorPrefix} has an invalid date`;
141
+ if (hasTime && !isTimeValueObject(value.time)) return `${errorPrefix} has an invalid time`;
142
+ break;
143
+ }
144
+ case "repeater":
145
+ if (!Array.isArray(value)) return `${errorPrefix} has invalid rows`;
146
+ break;
147
+ default:
148
+ break;
149
+ }
150
+ return null;
151
+ };
152
+ var runCustomSyncValidators = (value, field, allValues) => {
153
+ const validators = Array.isArray(field.validators) ? field.validators : [];
154
+ for (const validator of validators) {
155
+ if (isAsyncFunction(validator)) continue;
156
+ try {
157
+ const result = validator(value, allValues);
158
+ if (isPromise(result)) continue;
159
+ const err = normalizeValidatorResult(result);
160
+ if (err) return err;
161
+ } catch (err) {
162
+ return (err == null ? void 0 : err.message) || "Validation failed";
163
+ }
164
+ }
165
+ if (field.validate && !isAsyncFunction(field.validate)) {
166
+ try {
167
+ const result = field.validate(value, allValues);
168
+ if (!isPromise(result)) {
169
+ const err = normalizeValidatorResult(result);
170
+ if (err) return err;
171
+ }
172
+ } catch (err) {
173
+ return (err == null ? void 0 : err.message) || "Validation failed";
174
+ }
175
+ }
176
+ return null;
177
+ };
178
+ var collectAsyncValidatorPromises = (value, field, allValues, context) => {
179
+ const promises = [];
180
+ const validators = Array.isArray(field.validators) ? field.validators : [];
181
+ for (const validator of validators) {
182
+ try {
183
+ const result = validator(value, allValues, context);
184
+ if (isPromise(result)) promises.push(result);
185
+ } catch (err) {
186
+ promises.push(Promise.reject(err));
187
+ }
188
+ }
189
+ if (field.validate) {
190
+ try {
191
+ const result = field.validate(value, allValues, context);
192
+ if (isPromise(result)) promises.push(result);
193
+ } catch (err) {
194
+ promises.push(Promise.reject(err));
195
+ }
196
+ }
197
+ return promises;
198
+ };
199
+ var runValidators = (value, field, allValues, fieldTypes, options = {}) => {
200
+ const includeCustomValidators = options.includeCustomValidators !== false;
76
201
  if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList") return null;
77
202
  const isRequired = resolveRequired(field, allValues);
78
203
  const plugin = fieldTypes && fieldTypes[field.type];
@@ -81,6 +206,10 @@ var runValidators = (value, field, allValues, fieldTypes) => {
81
206
  return `${field.label} is required`;
82
207
  }
83
208
  if (empty) return null;
209
+ if (field.useDefaultValidators !== false) {
210
+ const typeError = runDefaultFieldValidator(value, field, allValues);
211
+ if (typeError) return typeError;
212
+ }
84
213
  if (field.pattern && typeof value === "string") {
85
214
  if (!field.pattern.test(value)) {
86
215
  return field.patternMessage || "Invalid format";
@@ -102,10 +231,25 @@ var runValidators = (value, field, allValues, fieldTypes) => {
102
231
  return `Must be no more than ${field.max}`;
103
232
  }
104
233
  }
105
- if (field.validate) {
106
- const result = field.validate(value, allValues);
107
- if (result && typeof result.then === "function") return null;
108
- if (result !== true && result) return result;
234
+ if (field.type === "date" && isDateValueObject(value)) {
235
+ if (field.min && isDateValueObject(field.min) && compareDateValues(value, field.min) < 0) {
236
+ return field.minValidationMessage || "Date is too early";
237
+ }
238
+ if (field.max && isDateValueObject(field.max) && compareDateValues(value, field.max) > 0) {
239
+ return field.maxValidationMessage || "Date is too late";
240
+ }
241
+ }
242
+ if (field.type === "time" && isTimeValueObject(value)) {
243
+ if (field.min && isTimeValueObject(field.min) && compareTimeValues(value, field.min) < 0) {
244
+ return field.minValidationMessage || "Time is too early";
245
+ }
246
+ if (field.max && isTimeValueObject(field.max) && compareTimeValues(value, field.max) > 0) {
247
+ return field.maxValidationMessage || "Time is too late";
248
+ }
249
+ }
250
+ if (includeCustomValidators) {
251
+ const customError = runCustomSyncValidators(value, field, allValues);
252
+ if (customError) return customError;
109
253
  }
110
254
  return null;
111
255
  };
@@ -117,6 +261,58 @@ var resolveOptions = (field, allValues) => {
117
261
  if (typeof field.options === "function") return field.options(allValues);
118
262
  return field.options || [];
119
263
  };
264
+ var getDependsOnName = (field) => field.dependsOnConfig && field.dependsOnConfig.field;
265
+ var getDependsOnDisplay = (field) => field.dependsOnConfig && field.dependsOnConfig.display || "grouped";
266
+ var getDependsOnLabel = (field) => field.dependsOnConfig && field.dependsOnConfig.label;
267
+ var getDependsOnMessage = (field) => field.dependsOnConfig && field.dependsOnConfig.message;
268
+ var getRepeaterErrorKey = (fieldName, rowIdx, subFieldName) => `${fieldName}[${rowIdx}].${subFieldName}`;
269
+ var isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
270
+ var deepEqual = (a, b) => {
271
+ if (Object.is(a, b)) return true;
272
+ if (typeof a !== typeof b) return false;
273
+ if (a == null || b == null) return false;
274
+ if (Array.isArray(a)) {
275
+ if (!Array.isArray(b) || a.length !== b.length) return false;
276
+ for (let i = 0; i < a.length; i++) {
277
+ if (!deepEqual(a[i], b[i])) return false;
278
+ }
279
+ return true;
280
+ }
281
+ if (a instanceof Date && b instanceof Date) {
282
+ return a.getTime() === b.getTime();
283
+ }
284
+ if (isPlainObject(a) && isPlainObject(b)) {
285
+ const aKeys = Object.keys(a);
286
+ const bKeys = Object.keys(b);
287
+ if (aKeys.length !== bKeys.length) return false;
288
+ for (const key of aKeys) {
289
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
290
+ if (!deepEqual(a[key], b[key])) return false;
291
+ }
292
+ return true;
293
+ }
294
+ return false;
295
+ };
296
+ var fieldSetHasErrors = (errors, fields) => {
297
+ if (!errors || !fields || fields.length === 0) return false;
298
+ const names = new Set(fields.map((field) => field.name));
299
+ return Object.keys(errors).some((errorKey) => {
300
+ const base = errorKey.split("[")[0];
301
+ return names.has(base);
302
+ });
303
+ };
304
+ var deepClone = (value) => {
305
+ if (Array.isArray(value)) return value.map(deepClone);
306
+ if (value instanceof Date) return new Date(value.getTime());
307
+ if (isPlainObject(value)) {
308
+ const next = {};
309
+ for (const key of Object.keys(value)) {
310
+ next[key] = deepClone(value[key]);
311
+ }
312
+ return next;
313
+ }
314
+ return value;
315
+ };
120
316
  var useFormPrefill = (properties, mapping) => {
121
317
  return (0, import_react.useMemo)(() => {
122
318
  if (!properties) return {};
@@ -186,28 +382,30 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
186
382
  // validate current step fields before Next
187
383
  } = props;
188
384
  const {
189
- submitLabel = "Submit",
190
- // submit button text
385
+ labels,
386
+ // { submit, cancel, back, next } — i18n label object
191
387
  submitVariant = "primary",
192
388
  // submit button variant
193
389
  showCancel = false,
194
390
  // show cancel button
195
- cancelLabel = "Cancel",
196
- // cancel button text
197
391
  onCancel,
198
392
  // () => void
199
393
  submitPosition = "bottom",
200
394
  // "bottom" | "none"
201
395
  loading: controlledLoading,
202
396
  // controlled loading state
203
- disabled = false
397
+ disabled = false,
204
398
  // disable entire form
399
+ renderButtons: renderButtonsProp
400
+ // custom action row renderer
205
401
  } = props;
206
402
  const {
207
403
  columns = 1,
208
404
  // number of grid columns (1 = full-width stack)
209
405
  columnWidth,
210
406
  // AutoGrid columnWidth — responsive layout (overrides columns)
407
+ maxColumns,
408
+ // cap number of columns per row in AutoGrid mode
211
409
  layout,
212
410
  // explicit row layout array (overrides columns + columnWidth)
213
411
  sections,
@@ -218,6 +416,10 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
218
416
  // show * on required fields
219
417
  noFormWrapper = false,
220
418
  // skip HubSpot <Form> wrapper
419
+ autoComplete,
420
+ // form autoComplete attribute
421
+ formProps,
422
+ // pass-through props for Form wrapper
221
423
  fieldTypes
222
424
  // Record<string, FieldTypePlugin> — custom field type registry
223
425
  } = props;
@@ -228,8 +430,12 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
228
430
  // string — form-level success alert
229
431
  readOnly: formReadOnly = false,
230
432
  // boolean — lock all fields
231
- readOnlyMessage
433
+ readOnlyMessage,
232
434
  // string — warning alert when readOnly
435
+ alerts,
436
+ // { addAlert, readOnlyTitle, errorTitle, successTitle }
437
+ errors: controlledErrors
438
+ // controlled validation errors
233
439
  } = props;
234
440
  const {
235
441
  onDirtyChange,
@@ -237,6 +443,38 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
237
443
  autoSave
238
444
  // { debounce: number, onAutoSave: (values) => void }
239
445
  } = props;
446
+ const submitButtonLabel = (labels == null ? void 0 : labels.submit) || "Submit";
447
+ const cancelButtonLabel = (labels == null ? void 0 : labels.cancel) || "Cancel";
448
+ const backButtonLabel = (labels == null ? void 0 : labels.back) || "Back";
449
+ const nextButtonLabel = (labels == null ? void 0 : labels.next) || "Next";
450
+ const addAlert = alerts == null ? void 0 : alerts.addAlert;
451
+ const readOnlyTitle = (alerts == null ? void 0 : alerts.readOnlyTitle) || "Read Only";
452
+ const errorTitle = (alerts == null ? void 0 : alerts.errorTitle) || "Error";
453
+ const successTitle = (alerts == null ? void 0 : alerts.successTitle) || "Success";
454
+ const prevErrorRef = (0, import_react.useRef)(formError);
455
+ const prevSuccessRef = (0, import_react.useRef)(formSuccess);
456
+ (0, import_react.useEffect)(() => {
457
+ if (!addAlert) return;
458
+ if (formError && formError !== prevErrorRef.current) {
459
+ addAlert({
460
+ type: "danger",
461
+ title: errorTitle,
462
+ message: typeof formError === "string" ? formError : void 0
463
+ });
464
+ }
465
+ prevErrorRef.current = formError;
466
+ }, [addAlert, formError, errorTitle]);
467
+ (0, import_react.useEffect)(() => {
468
+ if (!addAlert) return;
469
+ if (formSuccess && formSuccess !== prevSuccessRef.current) {
470
+ addAlert({
471
+ type: "success",
472
+ title: successTitle,
473
+ message: formSuccess
474
+ });
475
+ }
476
+ prevSuccessRef.current = formSuccess;
477
+ }, [addAlert, formSuccess, successTitle]);
240
478
  const computeInitialValues = () => {
241
479
  const vals = {};
242
480
  for (const field of fields) {
@@ -252,25 +490,99 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
252
490
  const [internalErrors, setInternalErrors] = (0, import_react.useState)({});
253
491
  const [internalStep, setInternalStep] = (0, import_react.useState)(0);
254
492
  const [internalLoading, setInternalLoading] = (0, import_react.useState)(false);
255
- const [touchedFields, setTouchedFields] = (0, import_react.useState)({});
256
493
  const [validatingFields, setValidatingFields] = (0, import_react.useState)({});
257
494
  const asyncValidationRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
495
+ const asyncAbortRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
496
+ const asyncValidationVersionRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
258
497
  const debounceTimersRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
498
+ const inputDebounceRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
499
+ const rowKeyRef = (0, import_react.useRef)(/* @__PURE__ */ new WeakMap());
500
+ const rowKeyCounterRef = (0, import_react.useRef)(0);
259
501
  const initialSnapshot = (0, import_react.useRef)(null);
260
502
  if (initialSnapshot.current === null) {
261
- initialSnapshot.current = JSON.stringify(computeInitialValues());
503
+ initialSnapshot.current = deepClone(computeInitialValues());
262
504
  }
263
505
  const formValues = values != null ? values : internalValues;
506
+ const formErrors = controlledErrors != null ? controlledErrors : internalErrors;
264
507
  const currentStep = controlledStep != null ? controlledStep : internalStep;
265
508
  const isLoading = controlledLoading != null ? controlledLoading : internalLoading;
266
509
  const isMultiStep = Array.isArray(steps) && steps.length > 0;
510
+ const formValuesRef = (0, import_react.useRef)(formValues);
511
+ const formErrorsRef = (0, import_react.useRef)(formErrors);
512
+ const draftValuesRef = (0, import_react.useRef)(null);
513
+ formValuesRef.current = formValues;
514
+ formErrorsRef.current = formErrors;
515
+ const fieldByName = (0, import_react.useMemo)(() => {
516
+ const map = /* @__PURE__ */ new Map();
517
+ for (const field of fields) map.set(field.name, field);
518
+ return map;
519
+ }, [fields]);
520
+ const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
521
+ const configWarningsRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
522
+ const warnConfig = (0, import_react.useCallback)((message) => {
523
+ if (!isDev) return;
524
+ if (configWarningsRef.current.has(message)) return;
525
+ configWarningsRef.current.add(message);
526
+ if (typeof console !== "undefined" && console.warn) {
527
+ console.warn(`[FormBuilder] ${message}`);
528
+ }
529
+ }, [isDev]);
530
+ const replaceErrors = (0, import_react.useCallback)(
531
+ (nextErrors) => {
532
+ if (controlledErrors == null) setInternalErrors(nextErrors);
533
+ if (onValidationChange) onValidationChange(nextErrors);
534
+ },
535
+ [controlledErrors, onValidationChange]
536
+ );
537
+ const updateErrors = (0, import_react.useCallback)(
538
+ (newErrors) => {
539
+ const mergeErrors = (base) => {
540
+ const merged = { ...base, ...newErrors };
541
+ for (const key of Object.keys(newErrors)) {
542
+ if (newErrors[key] === null || newErrors[key] === void 0) {
543
+ delete merged[key];
544
+ }
545
+ }
546
+ return merged;
547
+ };
548
+ if (controlledErrors != null) {
549
+ const merged = mergeErrors(formErrorsRef.current || {});
550
+ if (onValidationChange) onValidationChange(merged);
551
+ return;
552
+ }
553
+ setInternalErrors((prev) => {
554
+ const merged = mergeErrors(prev);
555
+ if (onValidationChange) onValidationChange(merged);
556
+ return merged;
557
+ });
558
+ },
559
+ [controlledErrors, onValidationChange]
560
+ );
561
+ const getFieldEmptyValue = (0, import_react.useCallback)(
562
+ (field) => {
563
+ const plugin = fieldTypes && fieldTypes[field.type];
564
+ return plugin && plugin.getEmptyValue ? plugin.getEmptyValue() : getEmptyValue(field);
565
+ },
566
+ [fieldTypes]
567
+ );
568
+ const getRowKey = (0, import_react.useCallback)((fieldName, row, index) => {
569
+ if (!row || typeof row !== "object") return `${fieldName}-idx-${index}`;
570
+ if (!rowKeyRef.current.has(row)) {
571
+ rowKeyCounterRef.current += 1;
572
+ rowKeyRef.current.set(row, `${fieldName}-row-${rowKeyCounterRef.current}`);
573
+ }
574
+ return rowKeyRef.current.get(row);
575
+ }, []);
267
576
  (0, import_react.useEffect)(() => {
268
577
  return () => {
269
578
  for (const timer of debounceTimersRef.current.values()) clearTimeout(timer);
579
+ for (const timer of inputDebounceRef.current.values()) clearTimeout(timer);
580
+ for (const controller of asyncAbortRef.current.values()) controller.abort();
581
+ if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
270
582
  };
271
583
  }, []);
272
584
  const isDirty = (0, import_react.useMemo)(() => {
273
- return JSON.stringify(formValues) !== initialSnapshot.current;
585
+ return !deepEqual(formValues, initialSnapshot.current);
274
586
  }, [formValues]);
275
587
  const prevDirtyRef = (0, import_react.useRef)(false);
276
588
  (0, import_react.useEffect)(() => {
@@ -280,36 +592,129 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
280
592
  }
281
593
  }, [isDirty, onDirtyChange]);
282
594
  const autoSaveTimerRef = (0, import_react.useRef)(null);
595
+ const autoSaveRef = (0, import_react.useRef)(autoSave);
596
+ autoSaveRef.current = autoSave;
597
+ const prevAutoSaveValues = (0, import_react.useRef)(deepClone(formValues));
283
598
  (0, import_react.useEffect)(() => {
284
- if (!autoSave || !autoSave.onAutoSave || !isDirty) return;
599
+ const cfg = autoSaveRef.current;
600
+ if (!cfg || !cfg.onAutoSave || !isDirty) return;
601
+ if (deepEqual(prevAutoSaveValues.current, formValues)) return;
602
+ prevAutoSaveValues.current = deepClone(formValues);
285
603
  if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
286
604
  autoSaveTimerRef.current = setTimeout(() => {
287
605
  autoSaveTimerRef.current = null;
288
- autoSave.onAutoSave(formValues);
289
- }, autoSave.debounce || 1e3);
606
+ autoSaveRef.current.onAutoSave(formValues);
607
+ }, cfg.debounce || 1e3);
290
608
  return () => {
291
609
  if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
292
610
  };
293
- }, [formValues, isDirty, autoSave]);
294
- const visibleFields = (0, import_react.useMemo)(() => {
295
- let filtered = fields.filter((f) => {
611
+ }, [formValues, isDirty]);
612
+ const allVisibleFields = (0, import_react.useMemo)(() => {
613
+ return fields.filter((f) => {
296
614
  if (f.visible && !f.visible(formValues)) return false;
297
615
  return true;
298
616
  });
617
+ }, [fields, formValues]);
618
+ const visibleFields = (0, import_react.useMemo)(() => {
619
+ let filtered = allVisibleFields;
299
620
  if (isMultiStep && steps[currentStep] && steps[currentStep].fields) {
300
621
  const stepFieldNames = new Set(steps[currentStep].fields);
301
622
  filtered = filtered.filter((f) => stepFieldNames.has(f.name));
302
623
  }
303
624
  return filtered;
304
- }, [fields, formValues, isMultiStep, steps, currentStep]);
625
+ }, [allVisibleFields, isMultiStep, steps, currentStep]);
626
+ (0, import_react.useEffect)(() => {
627
+ const nameSet = new Set(fields.map((f) => f.name));
628
+ if (nameSet.size !== fields.length) {
629
+ warnConfig("Duplicate field names detected. Field names must be unique.");
630
+ }
631
+ for (const field of fields) {
632
+ const parentName = getDependsOnName(field);
633
+ if (parentName && !nameSet.has(parentName)) {
634
+ warnConfig(`Field "${field.name}" depends on missing field "${parentName}".`);
635
+ }
636
+ }
637
+ if (steps) {
638
+ for (let i = 0; i < steps.length; i++) {
639
+ const step = steps[i];
640
+ if (!step.fields) continue;
641
+ for (const fieldName of step.fields) {
642
+ if (!nameSet.has(fieldName)) {
643
+ warnConfig(`Step ${i + 1} references missing field "${fieldName}".`);
644
+ }
645
+ }
646
+ }
647
+ }
648
+ if (layout) {
649
+ for (const row of layout) {
650
+ for (const entry of row) {
651
+ const fieldName = typeof entry === "string" ? entry : entry.field;
652
+ if (!nameSet.has(fieldName)) {
653
+ warnConfig(`Layout references missing field "${fieldName}".`);
654
+ }
655
+ }
656
+ }
657
+ }
658
+ if (sections) {
659
+ for (const section of sections) {
660
+ for (const fieldName of section.fields || []) {
661
+ if (!nameSet.has(fieldName)) {
662
+ warnConfig(`Section "${section.id}" references missing field "${fieldName}".`);
663
+ }
664
+ }
665
+ }
666
+ }
667
+ }, [fields, steps, layout, sections, warnConfig]);
668
+ const validateRepeaterField = (0, import_react.useCallback)(
669
+ (field, value, allValues) => {
670
+ const errors = {};
671
+ const rows = Array.isArray(value) ? value : [];
672
+ const subFields = field.fields || [];
673
+ let firstSubError = null;
674
+ if (resolveRequired(field, allValues) && rows.length === 0) {
675
+ const requiredError = `${field.label} is required`;
676
+ errors[field.name] = requiredError;
677
+ return { errors, hasErrors: true };
678
+ }
679
+ if (typeof field.min === "number" && rows.length < field.min) {
680
+ errors[field.name] = `Must have at least ${field.min} ${field.min === 1 ? "row" : "rows"}`;
681
+ } else if (typeof field.max === "number" && rows.length > field.max) {
682
+ errors[field.name] = `Must have no more than ${field.max} ${field.max === 1 ? "row" : "rows"}`;
683
+ }
684
+ rows.forEach((row, rowIdx) => {
685
+ const rowValues = { ...allValues, [field.name]: rows };
686
+ subFields.forEach((subField) => {
687
+ if (subField.visible && !subField.visible(rowValues)) return;
688
+ const err = runValidators(row == null ? void 0 : row[subField.name], subField, rowValues, fieldTypes);
689
+ if (!err) return;
690
+ const key = getRepeaterErrorKey(field.name, rowIdx, subField.name);
691
+ errors[key] = err;
692
+ if (!firstSubError) firstSubError = { row: rowIdx + 1, message: err };
693
+ });
694
+ });
695
+ if (!errors[field.name] && firstSubError) {
696
+ errors[field.name] = `Row ${firstSubError.row}: ${firstSubError.message}`;
697
+ }
698
+ return { errors, hasErrors: Object.keys(errors).length > 0 };
699
+ },
700
+ [fieldTypes]
701
+ );
305
702
  const validateField = (0, import_react.useCallback)(
306
703
  (name, value) => {
307
- const field = fields.find((f) => f.name === name);
704
+ const field = fieldByName.get(name);
308
705
  if (!field) return null;
309
706
  if (field.visible && !field.visible(formValues)) return null;
707
+ if (field.type === "repeater") {
708
+ const repeaterResult = validateRepeaterField(
709
+ field,
710
+ value != null ? value : formValues[name],
711
+ formValues
712
+ );
713
+ return repeaterResult.errors[name] || null;
714
+ }
310
715
  return runValidators(value != null ? value : formValues[name], field, formValues, fieldTypes);
311
716
  },
312
- [fields, formValues, fieldTypes]
717
+ [fieldByName, formValues, validateRepeaterField, fieldTypes]
313
718
  );
314
719
  const validateVisibleFields = (0, import_react.useCallback)(
315
720
  (fieldSubset) => {
@@ -317,6 +722,14 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
317
722
  const errors = {};
318
723
  let hasErrors = false;
319
724
  for (const field of toValidate) {
725
+ if (field.type === "repeater") {
726
+ const repeaterResult = validateRepeaterField(field, formValues[field.name], formValues);
727
+ if (repeaterResult.hasErrors) {
728
+ Object.assign(errors, repeaterResult.errors);
729
+ hasErrors = true;
730
+ }
731
+ continue;
732
+ }
320
733
  const err = runValidators(formValues[field.name], field, formValues, fieldTypes);
321
734
  if (err) {
322
735
  errors[field.name] = err;
@@ -325,64 +738,87 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
325
738
  }
326
739
  return { errors, hasErrors };
327
740
  },
328
- [visibleFields, formValues]
329
- );
330
- const updateErrors = (0, import_react.useCallback)(
331
- (newErrors) => {
332
- setInternalErrors((prev) => {
333
- const merged = { ...prev, ...newErrors };
334
- for (const key of Object.keys(merged)) {
335
- if (newErrors[key] === null || newErrors[key] === void 0) {
336
- delete merged[key];
337
- }
338
- }
339
- if (onValidationChange) onValidationChange(merged);
340
- return merged;
341
- });
342
- },
343
- [onValidationChange]
741
+ [visibleFields, formValues, validateRepeaterField, fieldTypes]
344
742
  );
345
743
  const runAsyncValidation = (0, import_react.useCallback)(
346
744
  (name, value) => {
347
- const field = fields.find((f) => f.name === name);
348
- if (!field || !field.validate) return;
745
+ const field = fieldByName.get(name);
746
+ if (!field || field.type === "repeater") return null;
349
747
  const val = value != null ? value : formValues[name];
350
- const syncError = runValidators(val, field, formValues, fieldTypes);
351
- if (syncError) return;
352
- const result = field.validate(val, formValues);
353
- if (!result || typeof result.then !== "function") return;
354
- const validationPromise = result.then(
355
- (asyncResult) => {
356
- if (asyncValidationRef.current.get(name) !== validationPromise) return;
748
+ const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false });
749
+ const prevController = asyncAbortRef.current.get(name);
750
+ if (prevController) prevController.abort();
751
+ asyncAbortRef.current.delete(name);
752
+ setValidatingFields((prev) => {
753
+ if (!prev[name]) return prev;
754
+ const next = { ...prev };
755
+ delete next[name];
756
+ return next;
757
+ });
758
+ if (syncError) return null;
759
+ const version = (asyncValidationVersionRef.current.get(name) || 0) + 1;
760
+ asyncValidationVersionRef.current.set(name, version);
761
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
762
+ if (controller) asyncAbortRef.current.set(name, controller);
763
+ let asyncPromises;
764
+ try {
765
+ asyncPromises = collectAsyncValidatorPromises(
766
+ val,
767
+ field,
768
+ formValues,
769
+ controller ? { signal: controller.signal } : void 0
770
+ );
771
+ } catch (err) {
772
+ updateErrors({ [name]: (err == null ? void 0 : err.message) || "Validation failed" });
773
+ return null;
774
+ }
775
+ if (asyncPromises.length === 0) {
776
+ asyncAbortRef.current.delete(name);
777
+ return null;
778
+ }
779
+ const validationPromise = Promise.all(asyncPromises).then(
780
+ (results) => {
781
+ if (asyncValidationVersionRef.current.get(name) !== version) return;
357
782
  asyncValidationRef.current.delete(name);
783
+ asyncAbortRef.current.delete(name);
358
784
  setValidatingFields((prev) => {
359
785
  const next = { ...prev };
360
786
  delete next[name];
361
787
  return next;
362
788
  });
363
- const err = asyncResult !== true && asyncResult ? asyncResult : null;
789
+ let err = null;
790
+ for (const result of results) {
791
+ const normalized = normalizeValidatorResult(result);
792
+ if (normalized) {
793
+ err = normalized;
794
+ break;
795
+ }
796
+ }
364
797
  updateErrors({ [name]: err });
365
798
  },
366
799
  (rejection) => {
367
- if (asyncValidationRef.current.get(name) !== validationPromise) return;
800
+ if (asyncValidationVersionRef.current.get(name) !== version) return;
368
801
  asyncValidationRef.current.delete(name);
802
+ asyncAbortRef.current.delete(name);
369
803
  setValidatingFields((prev) => {
370
804
  const next = { ...prev };
371
805
  delete next[name];
372
806
  return next;
373
807
  });
808
+ if (rejection && rejection.name === "AbortError") return;
374
809
  updateErrors({ [name]: (rejection == null ? void 0 : rejection.message) || "Validation failed" });
375
810
  }
376
811
  );
377
812
  asyncValidationRef.current.set(name, validationPromise);
378
813
  setValidatingFields((prev) => ({ ...prev, [name]: true }));
814
+ return validationPromise;
379
815
  },
380
- [fields, formValues, updateErrors]
816
+ [fieldByName, formValues, fieldTypes, updateErrors]
381
817
  );
382
818
  const triggerAsyncValidation = (0, import_react.useCallback)(
383
819
  (name, value) => {
384
- const field = fields.find((f) => f.name === name);
385
- if (!field || !field.validate) return;
820
+ const field = fieldByName.get(name);
821
+ if (!field || field.type === "repeater") return;
386
822
  const debounceMs = field.validateDebounce;
387
823
  if (debounceMs && debounceMs > 0) {
388
824
  const existing = debounceTimersRef.current.get(name);
@@ -396,44 +832,93 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
396
832
  runAsyncValidation(name, value);
397
833
  }
398
834
  },
399
- [fields, runAsyncValidation]
835
+ [fieldByName, runAsyncValidation]
400
836
  );
401
- const setFieldValueSilent = (0, import_react.useCallback)(
402
- (name, value) => {
837
+ const commitValues = (0, import_react.useCallback)(
838
+ (nextValues) => {
839
+ formValuesRef.current = nextValues;
403
840
  if (values != null) {
404
- if (onChange) onChange({ ...formValues, [name]: value });
841
+ if (onChange) onChange(nextValues);
405
842
  } else {
406
- setInternalValues((prev) => ({ ...prev, [name]: value }));
843
+ setInternalValues(nextValues);
407
844
  }
408
845
  },
409
- [values, onChange, formValues]
846
+ [values, onChange]
847
+ );
848
+ const setFieldValueSilent = (0, import_react.useCallback)(
849
+ (name, value) => {
850
+ const base = draftValuesRef.current || formValuesRef.current || {};
851
+ const nextValues = { ...base, [name]: value };
852
+ draftValuesRef.current = nextValues;
853
+ commitValues(nextValues);
854
+ },
855
+ [commitValues]
410
856
  );
411
857
  const handleFieldChange = (0, import_react.useCallback)(
412
858
  (name, value) => {
413
- const newValues = { ...formValues, [name]: value };
414
- if (values != null) {
415
- if (onChange) onChange(newValues);
416
- } else {
417
- setInternalValues(newValues);
859
+ const newValues = { ...formValuesRef.current, [name]: value };
860
+ const queue = [name];
861
+ const visited = /* @__PURE__ */ new Set();
862
+ const clearedErrors = {};
863
+ while (queue.length > 0) {
864
+ const current = queue.shift();
865
+ if (!current || visited.has(current)) continue;
866
+ visited.add(current);
867
+ fields.forEach((dep) => {
868
+ const parentName = getDependsOnName(dep);
869
+ if (parentName !== current || dep.name === current) return;
870
+ if (!dep.options) return;
871
+ const depOptions = resolveOptions(dep, newValues);
872
+ const depValue = newValues[dep.name];
873
+ if (depValue == null || depValue === "") return;
874
+ const validValues = new Set(depOptions.map((o) => o.value));
875
+ let nextDepValue = depValue;
876
+ let changed = false;
877
+ if (Array.isArray(depValue)) {
878
+ const filtered = depValue.filter((v) => validValues.has(v));
879
+ if (filtered.length !== depValue.length) {
880
+ nextDepValue = filtered;
881
+ changed = true;
882
+ }
883
+ } else if (!validValues.has(depValue)) {
884
+ nextDepValue = getFieldEmptyValue(dep);
885
+ changed = true;
886
+ }
887
+ if (changed) {
888
+ newValues[dep.name] = nextDepValue;
889
+ queue.push(dep.name);
890
+ if (formErrorsRef.current[dep.name] != null) {
891
+ clearedErrors[dep.name] = null;
892
+ }
893
+ }
894
+ });
418
895
  }
419
- if (onFieldChange) onFieldChange(name, value, newValues);
420
- if (internalErrors[name]) {
421
- updateErrors({ [name]: null });
896
+ if (formErrorsRef.current[name] != null) {
897
+ clearedErrors[name] = null;
898
+ }
899
+ for (const key of Object.keys(formErrorsRef.current)) {
900
+ if (key.startsWith(`${name}[`)) {
901
+ clearedErrors[key] = null;
902
+ }
422
903
  }
423
- const field = fields.find((f) => f.name === name);
904
+ draftValuesRef.current = newValues;
905
+ commitValues(newValues);
906
+ if (onFieldChange) onFieldChange(name, value, newValues);
907
+ if (Object.keys(clearedErrors).length > 0) updateErrors(clearedErrors);
908
+ const field = fieldByName.get(name);
424
909
  if (field && field.onFieldChange) {
425
910
  field.onFieldChange(value, newValues, {
426
911
  setFieldValue: setFieldValueSilent,
427
912
  setFieldError: (fieldName, message) => updateErrors({ [fieldName]: message })
428
913
  });
429
914
  }
915
+ draftValuesRef.current = null;
430
916
  },
431
- [formValues, values, onChange, onFieldChange, internalErrors, updateErrors, fields, setFieldValueSilent]
917
+ [fields, getFieldEmptyValue, commitValues, onFieldChange, updateErrors, fieldByName, setFieldValueSilent]
432
918
  );
433
- const inputDebounceRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
434
919
  const handleDebouncedFieldChange = (0, import_react.useCallback)(
435
920
  (name, value) => {
436
- const field = fields.find((f) => f.name === name);
921
+ const field = fieldByName.get(name);
437
922
  const debounceMs = field && field.debounce;
438
923
  if (debounceMs && debounceMs > 0) {
439
924
  const existing = inputDebounceRef.current.get(name);
@@ -447,26 +932,24 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
447
932
  handleFieldChange(name, value);
448
933
  }
449
934
  },
450
- [fields, handleFieldChange]
935
+ [fieldByName, handleFieldChange]
451
936
  );
452
937
  const handleFieldInput = (0, import_react.useCallback)(
453
938
  (name, value) => {
454
- if (validateOnChange) {
455
- const err = validateField(name, value);
456
- updateErrors({ [name]: err });
457
- }
939
+ if (!validateOnChange) return;
940
+ const err = validateField(name, value);
941
+ updateErrors({ [name]: err });
458
942
  },
459
943
  [validateOnChange, validateField, updateErrors]
460
944
  );
461
945
  const handleFieldBlur = (0, import_react.useCallback)(
462
946
  (name, value) => {
463
- setTouchedFields((prev) => ({ ...prev, [name]: true }));
464
- if (validateOnBlur) {
465
- const err = validateField(name, value != null ? value : formValues[name]);
466
- updateErrors({ [name]: err });
467
- if (!err) {
468
- triggerAsyncValidation(name, value != null ? value : formValues[name]);
469
- }
947
+ if (!validateOnBlur) return;
948
+ const resolvedValue = value != null ? value : formValues[name];
949
+ const err = validateField(name, resolvedValue);
950
+ updateErrors({ [name]: err });
951
+ if (!err) {
952
+ triggerAsyncValidation(name, resolvedValue);
470
953
  }
471
954
  },
472
955
  [validateOnBlur, validateField, updateErrors, formValues, triggerAsyncValidation]
@@ -475,30 +958,33 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
475
958
  async (e) => {
476
959
  if (e && e.preventDefault) e.preventDefault();
477
960
  if (validateOnSubmit) {
478
- const allVisible = fields.filter((f) => !f.visible || f.visible(formValues));
479
- const { errors, hasErrors } = validateVisibleFields(allVisible);
961
+ const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
480
962
  if (hasErrors) {
481
- setInternalErrors(errors);
482
- if (onValidationChange) onValidationChange(errors);
963
+ replaceErrors(errors);
483
964
  return;
484
965
  }
485
- if (asyncValidationRef.current.size > 0) {
486
- await Promise.all(asyncValidationRef.current.values());
487
- const currentErrors = { ...internalErrors };
488
- const hasAsyncErrors = Object.keys(currentErrors).length > 0;
489
- if (hasAsyncErrors) return;
966
+ const asyncSubmitValidations = allVisibleFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
967
+ if (asyncSubmitValidations.length > 0 || asyncValidationRef.current.size > 0) {
968
+ const pendingValidations = [
969
+ .../* @__PURE__ */ new Set([
970
+ ...asyncSubmitValidations,
971
+ ...Array.from(asyncValidationRef.current.values())
972
+ ])
973
+ ];
974
+ await Promise.all(pendingValidations);
975
+ if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) return;
490
976
  }
491
977
  }
492
978
  const reset = () => {
493
979
  const fresh = computeInitialValues();
494
980
  if (values == null) setInternalValues(fresh);
495
- setInternalErrors({});
496
- setTouchedFields({});
497
- initialSnapshot.current = JSON.stringify(fresh);
981
+ replaceErrors({});
982
+ initialSnapshot.current = deepClone(fresh);
983
+ prevAutoSaveValues.current = deepClone(fresh);
498
984
  };
499
985
  const rawValues = {};
500
986
  for (const key of Object.keys(formValues)) {
501
- const f = fields.find((fd) => fd.name === key);
987
+ const f = fieldByName.get(key);
502
988
  if (f && (f.type === "display" || f.type === "crmPropertyList" || f.type === "crmAssociationPropertyList")) continue;
503
989
  rawValues[key] = formValues[key];
504
990
  }
@@ -522,25 +1008,34 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
522
1008
  if (controlledLoading == null) setInternalLoading(false);
523
1009
  }
524
1010
  },
525
- [validateOnSubmit, fields, formValues, validateVisibleFields, onValidationChange, onSubmit, values, controlledLoading, internalErrors, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess]
1011
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, runAsyncValidation]
526
1012
  );
527
- const handleNext = (0, import_react.useCallback)(() => {
1013
+ const handleNext = (0, import_react.useCallback)(async () => {
528
1014
  if (!isMultiStep) return;
529
1015
  if (validateStepOnNext && steps[currentStep] && steps[currentStep].fields) {
530
- const stepFields = fields.filter(
531
- (f) => steps[currentStep].fields.includes(f.name) && (!f.visible || f.visible(formValues))
532
- );
1016
+ const stepFieldNames = new Set(steps[currentStep].fields);
1017
+ const stepFields = allVisibleFields.filter((f) => stepFieldNames.has(f.name));
533
1018
  const { errors, hasErrors } = validateVisibleFields(stepFields);
534
1019
  if (hasErrors) {
535
- setInternalErrors((prev) => ({ ...prev, ...errors }));
536
- if (onValidationChange) onValidationChange({ ...internalErrors, ...errors });
1020
+ replaceErrors({ ...formErrorsRef.current, ...errors });
537
1021
  return;
538
1022
  }
1023
+ const asyncStepValidations = stepFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
1024
+ if (asyncStepValidations.length > 0 || asyncValidationRef.current.size > 0) {
1025
+ const pendingValidations = [
1026
+ .../* @__PURE__ */ new Set([
1027
+ ...asyncStepValidations,
1028
+ ...Array.from(asyncValidationRef.current.values())
1029
+ ])
1030
+ ];
1031
+ await Promise.all(pendingValidations);
1032
+ if (fieldSetHasErrors(formErrorsRef.current, stepFields)) return;
1033
+ }
539
1034
  }
540
1035
  if (steps[currentStep] && steps[currentStep].validate) {
541
1036
  const result = steps[currentStep].validate(formValues);
542
1037
  if (result !== true && result) {
543
- setInternalErrors((prev) => ({ ...prev, ...result }));
1038
+ replaceErrors({ ...formErrorsRef.current, ...result });
544
1039
  return;
545
1040
  }
546
1041
  }
@@ -550,7 +1045,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
550
1045
  } else {
551
1046
  setInternalStep(nextStep);
552
1047
  }
553
- }, [isMultiStep, validateStepOnNext, steps, currentStep, fields, formValues, validateVisibleFields, onValidationChange, internalErrors, controlledStep, onStepChange]);
1048
+ }, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, runAsyncValidation]);
554
1049
  const handleBack = (0, import_react.useCallback)(() => {
555
1050
  if (!isMultiStep) return;
556
1051
  const prevStep = Math.max(currentStep - 1, 0);
@@ -575,33 +1070,56 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
575
1070
  (0, import_react.useImperativeHandle)(ref, () => ({
576
1071
  submit: handleSubmit,
577
1072
  validate: () => {
578
- const allVisible = fields.filter((f) => !f.visible || f.visible(formValues));
579
- const { errors, hasErrors } = validateVisibleFields(allVisible);
580
- setInternalErrors(errors);
1073
+ const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
1074
+ replaceErrors(errors);
581
1075
  return { valid: !hasErrors, errors };
582
1076
  },
583
1077
  reset: () => {
584
1078
  const fresh = computeInitialValues();
585
1079
  if (values == null) setInternalValues(fresh);
586
- setInternalErrors({});
587
- setTouchedFields({});
588
- initialSnapshot.current = JSON.stringify(fresh);
1080
+ replaceErrors({});
1081
+ initialSnapshot.current = deepClone(fresh);
1082
+ prevAutoSaveValues.current = deepClone(fresh);
589
1083
  },
590
1084
  getValues: () => formValues,
591
1085
  isDirty: () => isDirty,
592
1086
  setFieldValue: (name, value) => handleFieldChange(name, value),
593
1087
  setFieldError: (name, message) => updateErrors({ [name]: message }),
594
1088
  setErrors: (errors) => {
595
- setInternalErrors(errors);
596
- if (onValidationChange) onValidationChange(errors);
1089
+ replaceErrors(errors);
597
1090
  }
598
1091
  }));
1092
+ const setRepeaterSubFieldError = (0, import_react.useCallback)(
1093
+ (fieldName, rowIdx, subFieldName, errorMessage) => {
1094
+ const key = getRepeaterErrorKey(fieldName, rowIdx, subFieldName);
1095
+ const merged = { ...formErrorsRef.current };
1096
+ if (errorMessage) {
1097
+ merged[key] = errorMessage;
1098
+ } else {
1099
+ delete merged[key];
1100
+ }
1101
+ const subErrors = Object.keys(merged).filter((k) => k.startsWith(`${fieldName}[`)).map((k) => {
1102
+ const match = k.match(/\[(\d+)\]\./);
1103
+ const row = match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
1104
+ return { key: k, row };
1105
+ }).sort((a, b) => a.row - b.row);
1106
+ if (subErrors.length > 0) {
1107
+ const first = subErrors[0];
1108
+ merged[fieldName] = `Row ${first.row + 1}: ${merged[first.key]}`;
1109
+ } else if (!merged[fieldName] || merged[fieldName].startsWith("Row ")) {
1110
+ delete merged[fieldName];
1111
+ }
1112
+ replaceErrors(merged);
1113
+ },
1114
+ [replaceErrors]
1115
+ );
599
1116
  const renderField = (field) => {
600
1117
  const fieldValue = formValues[field.name];
601
- const fieldError = internalErrors[field.name] || null;
1118
+ const fieldError = formErrors[field.name] || null;
602
1119
  const hasError = !!fieldError;
603
1120
  const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
604
- const isReadOnly = field.readOnly || disabled || formReadOnly;
1121
+ const isReadOnly = field.readOnly || formReadOnly;
1122
+ const isDisabled = disabled || field.disabled || formReadOnly;
605
1123
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
606
1124
  if (field.type === "display") {
607
1125
  if (field.render) {
@@ -660,6 +1178,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
660
1178
  tooltip: field.tooltip,
661
1179
  required: isRequired,
662
1180
  readOnly: isReadOnly,
1181
+ disabled: isDisabled,
663
1182
  error: hasError,
664
1183
  validationMessage: fieldError || void 0,
665
1184
  ...field.loading || validatingFields[field.name] ? { loading: true } : {},
@@ -792,6 +1311,9 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
792
1311
  maxValidationMessage: field.maxValidationMessage,
793
1312
  onChange: (v) => {
794
1313
  handleFieldChange(field.name, { ...fieldValue, date: v, time: timeVal });
1314
+ },
1315
+ onBlur: (v) => {
1316
+ handleFieldBlur(field.name, { ...fieldValue, date: v, time: timeVal });
795
1317
  }
796
1318
  }
797
1319
  )), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1 }, /* @__PURE__ */ import_react.default.createElement(
@@ -802,12 +1324,16 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
802
1324
  description: field.description,
803
1325
  tooltip: field.tooltip,
804
1326
  readOnly: isReadOnly,
1327
+ disabled: isDisabled,
805
1328
  error: hasError,
806
1329
  value: timeVal,
807
1330
  interval: field.interval,
808
1331
  timezone: field.timezone,
809
1332
  onChange: (v) => {
810
1333
  handleFieldChange(field.name, { ...fieldValue, date: dateVal, time: v });
1334
+ },
1335
+ onBlur: (v) => {
1336
+ handleFieldBlur(field.name, { ...fieldValue, date: dateVal, time: v });
811
1337
  }
812
1338
  }
813
1339
  )));
@@ -845,6 +1371,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
845
1371
  textChecked: field.textChecked,
846
1372
  textUnchecked: field.textUnchecked,
847
1373
  readonly: isReadOnly,
1374
+ disabled: isDisabled,
848
1375
  onChange: fieldOnChange,
849
1376
  ...field.fieldProps || {}
850
1377
  }
@@ -857,6 +1384,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
857
1384
  checked: !!fieldValue,
858
1385
  description: field.description,
859
1386
  readOnly: isReadOnly,
1387
+ disabled: isDisabled,
860
1388
  inline: field.inline,
861
1389
  variant: field.variant,
862
1390
  onChange: fieldOnChange,
@@ -893,61 +1421,140 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
893
1421
  case "repeater": {
894
1422
  const rows = Array.isArray(fieldValue) ? fieldValue : [];
895
1423
  const subFields = field.fields || [];
896
- const minRows = field.min || 0;
897
- const maxRows = field.max || Infinity;
898
- const canAdd = rows.length < maxRows && !isReadOnly;
899
- const canRemove = rows.length > minRows && !isReadOnly;
1424
+ const minRows = typeof field.min === "number" ? field.min : 0;
1425
+ const maxRows = typeof field.max === "number" ? field.max : Infinity;
1426
+ const repeaterProps = field.repeaterProps || {};
1427
+ const renderAddControl = repeaterProps.renderAdd;
1428
+ const renderRemoveControl = repeaterProps.renderRemove;
1429
+ const renderMoveUpControl = repeaterProps.renderMoveUp;
1430
+ const renderMoveDownControl = repeaterProps.renderMoveDown;
1431
+ const addLabel = repeaterProps.addLabel || "Add";
1432
+ const removeLabel = repeaterProps.removeLabel || "Remove";
1433
+ const moveUpLabel = repeaterProps.moveUpLabel || "Up";
1434
+ const moveDownLabel = repeaterProps.moveDownLabel || "Down";
1435
+ const canEditRows = !isReadOnly && !isDisabled;
1436
+ const canAdd = rows.length < maxRows && canEditRows;
1437
+ const canRemove = rows.length > minRows && canEditRows;
1438
+ const canReorder = !!repeaterProps.reorderable && canEditRows;
1439
+ const repeaterHasNestedErrors = Object.keys(formErrors).some(
1440
+ (k) => k.startsWith(`${field.name}[`)
1441
+ );
1442
+ const firstNestedErrorKey = Object.keys(formErrors).find(
1443
+ (k) => k.startsWith(`${field.name}[`)
1444
+ );
1445
+ const repeaterErrorMessage = fieldError || (firstNestedErrorKey ? formErrors[firstNestedErrorKey] : null);
1446
+ const repeaterHasError = !!fieldError || repeaterHasNestedErrors;
900
1447
  const addRow = () => {
901
1448
  const emptyRow = {};
902
1449
  for (const sf of subFields) {
903
- emptyRow[sf.name] = sf.defaultValue !== void 0 ? sf.defaultValue : getEmptyValue(sf);
1450
+ emptyRow[sf.name] = sf.defaultValue !== void 0 ? sf.defaultValue : getFieldEmptyValue(sf);
904
1451
  }
905
1452
  handleFieldChange(field.name, [...rows, emptyRow]);
906
1453
  };
907
1454
  const removeRow = (idx) => {
908
1455
  handleFieldChange(field.name, rows.filter((_, i) => i !== idx));
909
1456
  };
910
- const updateRow = (idx, subName, subValue) => {
1457
+ const moveRow = (fromIndex, toIndex) => {
1458
+ if (toIndex < 0 || toIndex >= rows.length || toIndex === fromIndex) return;
1459
+ const updated = [...rows];
1460
+ const [moved] = updated.splice(fromIndex, 1);
1461
+ updated.splice(toIndex, 0, moved);
1462
+ handleFieldChange(field.name, updated);
1463
+ };
1464
+ const validateSubField = (rowIdx, subField, subValue, nextRows) => {
1465
+ const rowValues = { ...formValues, [field.name]: nextRows };
1466
+ const err = runValidators(subValue, subField, rowValues, fieldTypes);
1467
+ setRepeaterSubFieldError(field.name, rowIdx, subField.name, err);
1468
+ };
1469
+ const handleSubFieldChange = (rowIdx, subField, subValue) => {
911
1470
  const updated = rows.map(
912
- (row, i) => i === idx ? { ...row, [subName]: subValue } : row
1471
+ (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
913
1472
  );
914
1473
  handleFieldChange(field.name, updated);
1474
+ if (validateOnChange) {
1475
+ validateSubField(rowIdx, subField, subValue, updated);
1476
+ }
1477
+ };
1478
+ const handleSubFieldBlur = (rowIdx, subField, subValue) => {
1479
+ if (!validateOnBlur) return;
1480
+ const nextRows = rows.map(
1481
+ (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
1482
+ );
1483
+ validateSubField(rowIdx, subField, subValue, nextRows);
915
1484
  };
916
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, field.label, isRequired ? " *" : ""), field.description && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, field.description), rows.map((row, rowIdx) => /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { key: rowIdx, direction: "row", gap: "xs", align: "end" }, subFields.map((sf) => {
1485
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, field.label, isRequired ? " *" : ""), field.description && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, field.description), rows.map((row, rowIdx) => /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { key: getRowKey(field.name, row, rowIdx), direction: "row", gap: "xs", align: "end" }, subFields.map((sf) => {
917
1486
  const sfValue = row[sf.name];
918
1487
  const sfLabel = rowIdx === 0 ? sf.label : void 0;
919
- const sfOptions = resolveOptions(sf, formValues);
1488
+ const sfOptions = resolveOptions(sf, { ...formValues, [field.name]: rows });
1489
+ const sfError = formErrors[getRepeaterErrorKey(field.name, rowIdx, sf.name)] || null;
920
1490
  const sfProps = {
921
1491
  name: `${field.name}-${rowIdx}-${sf.name}`,
922
1492
  label: sfLabel,
923
1493
  placeholder: sf.placeholder,
924
- readOnly: isReadOnly,
1494
+ readOnly: sf.readOnly || isReadOnly,
1495
+ disabled: sf.disabled || isDisabled,
1496
+ error: !!sfError,
1497
+ validationMessage: sfError || void 0,
925
1498
  ...sf.fieldProps || {}
926
1499
  };
927
1500
  let sfElement;
928
1501
  switch (sf.type) {
929
1502
  case "select":
930
- sfElement = /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Select, { ...sfProps, value: sfValue, options: sfOptions, onChange: (v) => updateRow(rowIdx, sf.name, v) });
1503
+ sfElement = /* @__PURE__ */ import_react.default.createElement(
1504
+ import_ui_extensions.Select,
1505
+ {
1506
+ ...sfProps,
1507
+ value: sfValue,
1508
+ options: sfOptions,
1509
+ onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
1510
+ onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
1511
+ }
1512
+ );
931
1513
  break;
932
1514
  case "number":
933
- sfElement = /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.NumberInput, { ...sfProps, value: sfValue, onChange: (v) => updateRow(rowIdx, sf.name, v) });
1515
+ sfElement = /* @__PURE__ */ import_react.default.createElement(
1516
+ import_ui_extensions.NumberInput,
1517
+ {
1518
+ ...sfProps,
1519
+ value: sfValue,
1520
+ onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
1521
+ onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
1522
+ }
1523
+ );
934
1524
  break;
935
1525
  case "checkbox":
936
- sfElement = /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Checkbox, { ...sfProps, checked: !!sfValue, onChange: (v) => updateRow(rowIdx, sf.name, v) }, sf.label);
1526
+ sfElement = /* @__PURE__ */ import_react.default.createElement(
1527
+ import_ui_extensions.Checkbox,
1528
+ {
1529
+ ...sfProps,
1530
+ checked: !!sfValue,
1531
+ onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
1532
+ onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
1533
+ },
1534
+ sf.label
1535
+ );
937
1536
  break;
938
1537
  default:
939
- sfElement = /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Input, { ...sfProps, value: sfValue || "", onChange: (v) => updateRow(rowIdx, sf.name, v) });
1538
+ sfElement = /* @__PURE__ */ import_react.default.createElement(
1539
+ import_ui_extensions.Input,
1540
+ {
1541
+ ...sfProps,
1542
+ value: sfValue || "",
1543
+ onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
1544
+ onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
1545
+ }
1546
+ );
940
1547
  }
941
1548
  return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { key: sf.name, flex: 1 }, sfElement);
942
- }), canRemove && /* @__PURE__ */ import_react.default.createElement(
1549
+ }), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Inline, { gap: "xs" }, canReorder && rowIdx > 0 && (renderMoveUpControl ? renderMoveUpControl({ index: rowIdx, onClick: () => moveRow(rowIdx, rowIdx - 1) }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", size: "sm", onClick: () => moveRow(rowIdx, rowIdx - 1) }, moveUpLabel)), canReorder && rowIdx < rows.length - 1 && (renderMoveDownControl ? renderMoveDownControl({ index: rowIdx, onClick: () => moveRow(rowIdx, rowIdx + 1) }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", size: "sm", onClick: () => moveRow(rowIdx, rowIdx + 1) }, moveDownLabel)), canRemove && (renderRemoveControl ? renderRemoveControl({ index: rowIdx, onClick: () => removeRow(rowIdx) }) : /* @__PURE__ */ import_react.default.createElement(
943
1550
  import_ui_extensions.Button,
944
1551
  {
945
1552
  variant: "secondary",
946
- size: "xs",
1553
+ size: "md",
947
1554
  onClick: () => removeRow(rowIdx)
948
1555
  },
949
- "Remove"
950
- ))), canAdd && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", size: "sm", onClick: addRow }, "+ Add"), hasError && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, fieldError));
1556
+ removeLabel
1557
+ ))))), canAdd && (renderAddControl ? renderAddControl({ onClick: addRow, count: rows.length }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { onClick: addRow }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "flush" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "add" }), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, addLabel)))), repeaterHasError && repeaterErrorMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, repeaterErrorMessage));
951
1558
  }
952
1559
  default:
953
1560
  return /* @__PURE__ */ import_react.default.createElement(
@@ -967,15 +1574,17 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
967
1574
  if (field.width === "full" && columns > 1) return columns;
968
1575
  return 1;
969
1576
  };
970
- const getDependents = (parentField) => visibleFields.filter((f) => f.dependsOn === parentField.name && f.name !== parentField.name);
971
- const isDependent = (field) => field.dependsOn && visibleFields.some((f) => f.name === field.dependsOn && f.name !== field.name);
1577
+ const getDependents = (parentField) => visibleFields.filter(
1578
+ (f) => getDependsOnName(f) === parentField.name && f.name !== parentField.name && getDependsOnDisplay(f) === "grouped"
1579
+ );
1580
+ const isDependent = (field) => getDependsOnName(field) && getDependsOnDisplay(field) === "grouped" && visibleFields.some((f) => f.name === getDependsOnName(field) && f.name !== field.name);
972
1581
  const renderDependentGroup = (parentField, dependents) => {
973
- const firstWithLabel = dependents.find((f) => f.dependsOnLabel) || dependents[0];
974
- const firstWithMessage = dependents.find((f) => f.dependsOnMessage) || dependents[0];
975
- const groupLabel = firstWithLabel.dependsOnLabel || "Dependent properties";
976
- const rawMessage = firstWithMessage.dependsOnMessage;
1582
+ const firstWithLabel = dependents.find((f) => getDependsOnLabel(f)) || dependents[0];
1583
+ const firstWithMessage = dependents.find((f) => getDependsOnMessage(f)) || dependents[0];
1584
+ const groupLabel = getDependsOnLabel(firstWithLabel) || "Dependent properties";
1585
+ const rawMessage = getDependsOnMessage(firstWithMessage);
977
1586
  const tooltipMessage = typeof rawMessage === "function" ? rawMessage(parentField.label) : rawMessage || "";
978
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tile, { key: `dep-${parentField.name}`, compact: true }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, groupLabel, " ", tooltipMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { inline: true, variant: "dark", overlay: /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tooltip, null, tooltipMessage) }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "info" })))), dependents.map((dep) => /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, { key: dep.name }, renderField(dep)))));
1587
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tile, { key: `dep-${parentField.name}`, compact: true }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, groupLabel, " ", tooltipMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { inline: true, variant: "dark", overlay: /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tooltip, null, tooltipMessage) }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "info" })))), renderFieldSubset(dependents)));
979
1588
  };
980
1589
  const renderGridLayout = (fieldSubset) => {
981
1590
  const fieldList = fieldSubset || visibleFields;
@@ -1068,7 +1677,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1068
1677
  let i = 0;
1069
1678
  while (i < fieldList.length) {
1070
1679
  const field = fieldList[i];
1071
- if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !field.dependsOn) {
1680
+ if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !getDependsOnName(field)) {
1072
1681
  rows.push({ type: "pair", fields: [fieldList[i], fieldList[i + 1]] });
1073
1682
  i += 2;
1074
1683
  } else {
@@ -1104,9 +1713,12 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1104
1713
  let batch = [];
1105
1714
  const flushBatch = () => {
1106
1715
  if (batch.length === 0) return;
1107
- elements.push(
1108
- /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.AutoGrid, { key: `ag-${batch[0].name}`, columnWidth, flexible: true, gap }, batch.map((f) => /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, { key: f.name }, renderField(f))))
1109
- );
1716
+ const chunks = maxColumns ? Array.from({ length: Math.ceil(batch.length / maxColumns) }, (_, i) => batch.slice(i * maxColumns, i * maxColumns + maxColumns)) : [batch];
1717
+ for (const chunk of chunks) {
1718
+ elements.push(
1719
+ /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.AutoGrid, { key: `ag-${chunk[0].name}`, columnWidth, flexible: true, gap }, chunk.map((f) => /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, { key: f.name }, renderField(f))))
1720
+ );
1721
+ }
1110
1722
  batch = [];
1111
1723
  };
1112
1724
  for (const field of fieldList) {
@@ -1189,7 +1801,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1189
1801
  );
1190
1802
  if (sec.info) {
1191
1803
  elements.push(
1192
- /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { key: sec.id, direction: "row", align: "start", gap: "flush" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1 }, accordion), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { overlay: /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tooltip, null, sec.info) }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "info", size: "sm", screenReaderText: sec.info })))
1804
+ /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { key: sec.id, direction: "row", align: "start", justify: "start", gap: "flush" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1 }, accordion), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", overlay: /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tooltip, null, sec.info) }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "info", size: "sm", screenReaderText: sec.info })))
1193
1805
  );
1194
1806
  } else {
1195
1807
  elements.push(accordion);
@@ -1217,8 +1829,30 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1217
1829
  if (submitPosition === "none" || formReadOnly) return null;
1218
1830
  const isLastStep = !isMultiStep || currentStep === steps.length - 1;
1219
1831
  const isFirstStep = !isMultiStep || currentStep === 0;
1832
+ const buttonContext = {
1833
+ isMultiStep,
1834
+ isFirstStep,
1835
+ isLastStep,
1836
+ currentStep,
1837
+ totalSteps: isMultiStep ? steps.length : 1,
1838
+ disabled,
1839
+ loading: isLoading,
1840
+ labels: {
1841
+ submit: submitButtonLabel,
1842
+ cancel: cancelButtonLabel,
1843
+ back: backButtonLabel,
1844
+ next: nextButtonLabel
1845
+ },
1846
+ onBack: handleBack,
1847
+ onNext: handleNext,
1848
+ onCancel,
1849
+ onSubmit: handleSubmit
1850
+ };
1851
+ if (renderButtonsProp) {
1852
+ return renderButtonsProp(buttonContext);
1853
+ }
1220
1854
  if (isMultiStep) {
1221
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: handleBack, disabled }, "Back") : showCancel ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelLabel) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, " "), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Inline, { gap: "small" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, "Step ", currentStep + 1, " of ", steps.length), isLastStep ? /* @__PURE__ */ import_react.default.createElement(
1855
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: handleBack, disabled }, backButtonLabel) : showCancel ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, " "), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Inline, { gap: "small" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, "Step ", currentStep + 1, " of ", steps.length), isLastStep ? /* @__PURE__ */ import_react.default.createElement(
1222
1856
  import_ui_extensions.LoadingButton,
1223
1857
  {
1224
1858
  variant: submitVariant,
@@ -1226,10 +1860,10 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1226
1860
  onClick: handleSubmit,
1227
1861
  disabled
1228
1862
  },
1229
- submitLabel
1230
- ) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "primary", onClick: handleNext, disabled }, "Next")));
1863
+ submitButtonLabel
1864
+ ) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "primary", onClick: handleNext, disabled }, nextButtonLabel)));
1231
1865
  }
1232
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelLabel), /* @__PURE__ */ import_react.default.createElement(
1866
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ import_react.default.createElement(
1233
1867
  import_ui_extensions.LoadingButton,
1234
1868
  {
1235
1869
  variant: submitVariant,
@@ -1238,7 +1872,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1238
1872
  onClick: noFormWrapper ? handleSubmit : void 0,
1239
1873
  disabled
1240
1874
  },
1241
- submitLabel
1875
+ submitButtonLabel
1242
1876
  ));
1243
1877
  };
1244
1878
  const formContent = /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap }, isMultiStep && showStepIndicator && /* @__PURE__ */ import_react.default.createElement(
@@ -1247,7 +1881,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1247
1881
  currentStep,
1248
1882
  stepNames: steps.map((s) => s.title)
1249
1883
  }
1250
- ), formReadOnly && readOnlyMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title: "Read Only", variant: "warning" }, readOnlyMessage), formError && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title: "Error", variant: "danger" }, typeof formError === "string" ? formError : void 0), formSuccess && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title: "Success", variant: "success" }, formSuccess), isMultiStep && steps[currentStep] && steps[currentStep].render ? steps[currentStep].render({
1884
+ ), formReadOnly && readOnlyMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title: readOnlyTitle, variant: "warning" }, readOnlyMessage), !addAlert && formError && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title: errorTitle, variant: "danger" }, typeof formError === "string" ? formError : void 0), !addAlert && formSuccess && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title: successTitle, variant: "success" }, formSuccess), isMultiStep && steps[currentStep] && steps[currentStep].render ? steps[currentStep].render({
1251
1885
  values: formValues,
1252
1886
  goNext: handleNext,
1253
1887
  goBack: handleBack,
@@ -1259,7 +1893,15 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1259
1893
  if (noFormWrapper) {
1260
1894
  return formContent;
1261
1895
  }
1262
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Form, { onSubmit: handleSubmit, autoComplete: props.autoComplete }, formContent);
1896
+ return /* @__PURE__ */ import_react.default.createElement(
1897
+ import_ui_extensions.Form,
1898
+ {
1899
+ ...formProps || {},
1900
+ onSubmit: handleSubmit,
1901
+ autoComplete
1902
+ },
1903
+ formContent
1904
+ );
1263
1905
  });
1264
1906
  // Annotate the CommonJS export names for ESM import in node:
1265
1907
  0 && (module.exports = {