hs-uix 1.0.0 → 1.0.2

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.js CHANGED
@@ -1075,7 +1075,132 @@ var isValueEmpty = (value, field) => {
1075
1075
  if ((field.type === "toggle" || field.type === "checkbox") && value === false) return true;
1076
1076
  return false;
1077
1077
  };
1078
- var runValidators = (value, field, allValues, fieldTypes) => {
1078
+ var isPromise = (value) => value && typeof value.then === "function";
1079
+ var isAsyncFunction = (fn) => fn && fn.constructor && fn.constructor.name === "AsyncFunction";
1080
+ var normalizeValidatorResult = (result) => {
1081
+ if (result === true || result === void 0 || result === null || result === false) return null;
1082
+ return String(result);
1083
+ };
1084
+ var isDateValueObject = (value) => isPlainObject(value) && Number.isInteger(value.year) && Number.isInteger(value.month) && Number.isInteger(value.date);
1085
+ var isTimeValueObject = (value) => isPlainObject(value) && Number.isInteger(value.hours) && Number.isInteger(value.minutes);
1086
+ var compareDateValues = (a, b) => {
1087
+ if (a.year !== b.year) return a.year - b.year;
1088
+ if (a.month !== b.month) return a.month - b.month;
1089
+ return a.date - b.date;
1090
+ };
1091
+ var compareTimeValues = (a, b) => {
1092
+ if (a.hours !== b.hours) return a.hours - b.hours;
1093
+ return a.minutes - b.minutes;
1094
+ };
1095
+ var runDefaultFieldValidator = (value, field, allValues) => {
1096
+ const errorPrefix = field.label || field.name;
1097
+ switch (field.type) {
1098
+ case "text":
1099
+ case "password":
1100
+ case "textarea":
1101
+ if (typeof value !== "string") return `${errorPrefix} must be text`;
1102
+ break;
1103
+ case "number":
1104
+ case "stepper":
1105
+ case "currency":
1106
+ if (typeof value !== "number" || Number.isNaN(value)) return `${errorPrefix} must be a number`;
1107
+ break;
1108
+ case "toggle":
1109
+ case "checkbox":
1110
+ if (typeof value !== "boolean") return `${errorPrefix} must be true or false`;
1111
+ break;
1112
+ case "select":
1113
+ case "radioGroup": {
1114
+ const options = resolveOptions(field, allValues);
1115
+ if (options.length > 0 && !options.some((o) => Object.is(o.value, value))) {
1116
+ return `${errorPrefix} has an invalid selection`;
1117
+ }
1118
+ break;
1119
+ }
1120
+ case "multiselect":
1121
+ case "checkboxGroup": {
1122
+ if (!Array.isArray(value)) return `${errorPrefix} must be a list`;
1123
+ const options = resolveOptions(field, allValues);
1124
+ if (options.length > 0) {
1125
+ const validValues = new Set(options.map((o) => o.value));
1126
+ const hasInvalid = value.some((item) => !validValues.has(item));
1127
+ if (hasInvalid) return `${errorPrefix} has an invalid selection`;
1128
+ }
1129
+ break;
1130
+ }
1131
+ case "date":
1132
+ if (!isDateValueObject(value)) return `${errorPrefix} has an invalid date`;
1133
+ break;
1134
+ case "time":
1135
+ if (!isTimeValueObject(value)) return `${errorPrefix} has an invalid time`;
1136
+ break;
1137
+ case "datetime": {
1138
+ if (isDateValueObject(value)) break;
1139
+ if (!isPlainObject(value)) return `${errorPrefix} has an invalid date/time`;
1140
+ const hasDate = value.date !== void 0;
1141
+ const hasTime = value.time !== void 0;
1142
+ if (!hasDate && !hasTime) return `${errorPrefix} has an invalid date/time`;
1143
+ if (hasDate && !isDateValueObject(value.date)) return `${errorPrefix} has an invalid date`;
1144
+ if (hasTime && !isTimeValueObject(value.time)) return `${errorPrefix} has an invalid time`;
1145
+ break;
1146
+ }
1147
+ case "repeater":
1148
+ if (!Array.isArray(value)) return `${errorPrefix} has invalid rows`;
1149
+ break;
1150
+ default:
1151
+ break;
1152
+ }
1153
+ return null;
1154
+ };
1155
+ var runCustomSyncValidators = (value, field, allValues) => {
1156
+ const validators = Array.isArray(field.validators) ? field.validators : [];
1157
+ for (const validator of validators) {
1158
+ if (isAsyncFunction(validator)) continue;
1159
+ try {
1160
+ const result = validator(value, allValues);
1161
+ if (isPromise(result)) continue;
1162
+ const err = normalizeValidatorResult(result);
1163
+ if (err) return err;
1164
+ } catch (err) {
1165
+ return (err == null ? void 0 : err.message) || "Validation failed";
1166
+ }
1167
+ }
1168
+ if (field.validate && !isAsyncFunction(field.validate)) {
1169
+ try {
1170
+ const result = field.validate(value, allValues);
1171
+ if (!isPromise(result)) {
1172
+ const err = normalizeValidatorResult(result);
1173
+ if (err) return err;
1174
+ }
1175
+ } catch (err) {
1176
+ return (err == null ? void 0 : err.message) || "Validation failed";
1177
+ }
1178
+ }
1179
+ return null;
1180
+ };
1181
+ var collectAsyncValidatorPromises = (value, field, allValues, context) => {
1182
+ const promises = [];
1183
+ const validators = Array.isArray(field.validators) ? field.validators : [];
1184
+ for (const validator of validators) {
1185
+ try {
1186
+ const result = validator(value, allValues, context);
1187
+ if (isPromise(result)) promises.push(result);
1188
+ } catch (err) {
1189
+ promises.push(Promise.reject(err));
1190
+ }
1191
+ }
1192
+ if (field.validate) {
1193
+ try {
1194
+ const result = field.validate(value, allValues, context);
1195
+ if (isPromise(result)) promises.push(result);
1196
+ } catch (err) {
1197
+ promises.push(Promise.reject(err));
1198
+ }
1199
+ }
1200
+ return promises;
1201
+ };
1202
+ var runValidators = (value, field, allValues, fieldTypes, options = {}) => {
1203
+ const includeCustomValidators = options.includeCustomValidators !== false;
1079
1204
  if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList") return null;
1080
1205
  const isRequired = resolveRequired(field, allValues);
1081
1206
  const plugin = fieldTypes && fieldTypes[field.type];
@@ -1084,6 +1209,10 @@ var runValidators = (value, field, allValues, fieldTypes) => {
1084
1209
  return `${field.label} is required`;
1085
1210
  }
1086
1211
  if (empty) return null;
1212
+ if (field.useDefaultValidators !== false) {
1213
+ const typeError = runDefaultFieldValidator(value, field, allValues);
1214
+ if (typeError) return typeError;
1215
+ }
1087
1216
  if (field.pattern && typeof value === "string") {
1088
1217
  if (!field.pattern.test(value)) {
1089
1218
  return field.patternMessage || "Invalid format";
@@ -1105,10 +1234,25 @@ var runValidators = (value, field, allValues, fieldTypes) => {
1105
1234
  return `Must be no more than ${field.max}`;
1106
1235
  }
1107
1236
  }
1108
- if (field.validate) {
1109
- const result = field.validate(value, allValues);
1110
- if (result && typeof result.then === "function") return null;
1111
- if (result !== true && result) return result;
1237
+ if (field.type === "date" && isDateValueObject(value)) {
1238
+ if (field.min && isDateValueObject(field.min) && compareDateValues(value, field.min) < 0) {
1239
+ return field.minValidationMessage || "Date is too early";
1240
+ }
1241
+ if (field.max && isDateValueObject(field.max) && compareDateValues(value, field.max) > 0) {
1242
+ return field.maxValidationMessage || "Date is too late";
1243
+ }
1244
+ }
1245
+ if (field.type === "time" && isTimeValueObject(value)) {
1246
+ if (field.min && isTimeValueObject(field.min) && compareTimeValues(value, field.min) < 0) {
1247
+ return field.minValidationMessage || "Time is too early";
1248
+ }
1249
+ if (field.max && isTimeValueObject(field.max) && compareTimeValues(value, field.max) > 0) {
1250
+ return field.maxValidationMessage || "Time is too late";
1251
+ }
1252
+ }
1253
+ if (includeCustomValidators) {
1254
+ const customError = runCustomSyncValidators(value, field, allValues);
1255
+ if (customError) return customError;
1112
1256
  }
1113
1257
  return null;
1114
1258
  };
@@ -1120,6 +1264,58 @@ var resolveOptions = (field, allValues) => {
1120
1264
  if (typeof field.options === "function") return field.options(allValues);
1121
1265
  return field.options || [];
1122
1266
  };
1267
+ var getDependsOnName = (field) => field.dependsOnConfig && field.dependsOnConfig.field;
1268
+ var getDependsOnDisplay = (field) => field.dependsOnConfig && field.dependsOnConfig.display || "grouped";
1269
+ var getDependsOnLabel = (field) => field.dependsOnConfig && field.dependsOnConfig.label;
1270
+ var getDependsOnMessage = (field) => field.dependsOnConfig && field.dependsOnConfig.message;
1271
+ var getRepeaterErrorKey = (fieldName, rowIdx, subFieldName) => `${fieldName}[${rowIdx}].${subFieldName}`;
1272
+ var isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
1273
+ var deepEqual = (a, b) => {
1274
+ if (Object.is(a, b)) return true;
1275
+ if (typeof a !== typeof b) return false;
1276
+ if (a == null || b == null) return false;
1277
+ if (Array.isArray(a)) {
1278
+ if (!Array.isArray(b) || a.length !== b.length) return false;
1279
+ for (let i = 0; i < a.length; i++) {
1280
+ if (!deepEqual(a[i], b[i])) return false;
1281
+ }
1282
+ return true;
1283
+ }
1284
+ if (a instanceof Date && b instanceof Date) {
1285
+ return a.getTime() === b.getTime();
1286
+ }
1287
+ if (isPlainObject(a) && isPlainObject(b)) {
1288
+ const aKeys = Object.keys(a);
1289
+ const bKeys = Object.keys(b);
1290
+ if (aKeys.length !== bKeys.length) return false;
1291
+ for (const key of aKeys) {
1292
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
1293
+ if (!deepEqual(a[key], b[key])) return false;
1294
+ }
1295
+ return true;
1296
+ }
1297
+ return false;
1298
+ };
1299
+ var fieldSetHasErrors = (errors, fields) => {
1300
+ if (!errors || !fields || fields.length === 0) return false;
1301
+ const names = new Set(fields.map((field) => field.name));
1302
+ return Object.keys(errors).some((errorKey) => {
1303
+ const base = errorKey.split("[")[0];
1304
+ return names.has(base);
1305
+ });
1306
+ };
1307
+ var deepClone = (value) => {
1308
+ if (Array.isArray(value)) return value.map(deepClone);
1309
+ if (value instanceof Date) return new Date(value.getTime());
1310
+ if (isPlainObject(value)) {
1311
+ const next = {};
1312
+ for (const key of Object.keys(value)) {
1313
+ next[key] = deepClone(value[key]);
1314
+ }
1315
+ return next;
1316
+ }
1317
+ return value;
1318
+ };
1123
1319
  var useFormPrefill = (properties, mapping) => {
1124
1320
  return (0, import_react2.useMemo)(() => {
1125
1321
  if (!properties) return {};
@@ -1189,28 +1385,30 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1189
1385
  // validate current step fields before Next
1190
1386
  } = props;
1191
1387
  const {
1192
- submitLabel = "Submit",
1193
- // submit button text
1388
+ labels,
1389
+ // { submit, cancel, back, next } — i18n label object
1194
1390
  submitVariant = "primary",
1195
1391
  // submit button variant
1196
1392
  showCancel = false,
1197
1393
  // show cancel button
1198
- cancelLabel = "Cancel",
1199
- // cancel button text
1200
1394
  onCancel,
1201
1395
  // () => void
1202
1396
  submitPosition = "bottom",
1203
1397
  // "bottom" | "none"
1204
1398
  loading: controlledLoading,
1205
1399
  // controlled loading state
1206
- disabled = false
1400
+ disabled = false,
1207
1401
  // disable entire form
1402
+ renderButtons: renderButtonsProp
1403
+ // custom action row renderer
1208
1404
  } = props;
1209
1405
  const {
1210
1406
  columns = 1,
1211
1407
  // number of grid columns (1 = full-width stack)
1212
1408
  columnWidth,
1213
1409
  // AutoGrid columnWidth — responsive layout (overrides columns)
1410
+ maxColumns,
1411
+ // cap number of columns per row in AutoGrid mode
1214
1412
  layout,
1215
1413
  // explicit row layout array (overrides columns + columnWidth)
1216
1414
  sections,
@@ -1221,6 +1419,10 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1221
1419
  // show * on required fields
1222
1420
  noFormWrapper = false,
1223
1421
  // skip HubSpot <Form> wrapper
1422
+ autoComplete,
1423
+ // form autoComplete attribute
1424
+ formProps,
1425
+ // pass-through props for Form wrapper
1224
1426
  fieldTypes
1225
1427
  // Record<string, FieldTypePlugin> — custom field type registry
1226
1428
  } = props;
@@ -1231,8 +1433,12 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1231
1433
  // string — form-level success alert
1232
1434
  readOnly: formReadOnly = false,
1233
1435
  // boolean — lock all fields
1234
- readOnlyMessage
1436
+ readOnlyMessage,
1235
1437
  // string — warning alert when readOnly
1438
+ alerts,
1439
+ // { addAlert, readOnlyTitle, errorTitle, successTitle }
1440
+ errors: controlledErrors
1441
+ // controlled validation errors
1236
1442
  } = props;
1237
1443
  const {
1238
1444
  onDirtyChange,
@@ -1240,6 +1446,38 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1240
1446
  autoSave
1241
1447
  // { debounce: number, onAutoSave: (values) => void }
1242
1448
  } = props;
1449
+ const submitButtonLabel = (labels == null ? void 0 : labels.submit) || "Submit";
1450
+ const cancelButtonLabel = (labels == null ? void 0 : labels.cancel) || "Cancel";
1451
+ const backButtonLabel = (labels == null ? void 0 : labels.back) || "Back";
1452
+ const nextButtonLabel = (labels == null ? void 0 : labels.next) || "Next";
1453
+ const addAlert = alerts == null ? void 0 : alerts.addAlert;
1454
+ const readOnlyTitle = (alerts == null ? void 0 : alerts.readOnlyTitle) || "Read Only";
1455
+ const errorTitle = (alerts == null ? void 0 : alerts.errorTitle) || "Error";
1456
+ const successTitle = (alerts == null ? void 0 : alerts.successTitle) || "Success";
1457
+ const prevErrorRef = (0, import_react2.useRef)(formError);
1458
+ const prevSuccessRef = (0, import_react2.useRef)(formSuccess);
1459
+ (0, import_react2.useEffect)(() => {
1460
+ if (!addAlert) return;
1461
+ if (formError && formError !== prevErrorRef.current) {
1462
+ addAlert({
1463
+ type: "danger",
1464
+ title: errorTitle,
1465
+ message: typeof formError === "string" ? formError : void 0
1466
+ });
1467
+ }
1468
+ prevErrorRef.current = formError;
1469
+ }, [addAlert, formError, errorTitle]);
1470
+ (0, import_react2.useEffect)(() => {
1471
+ if (!addAlert) return;
1472
+ if (formSuccess && formSuccess !== prevSuccessRef.current) {
1473
+ addAlert({
1474
+ type: "success",
1475
+ title: successTitle,
1476
+ message: formSuccess
1477
+ });
1478
+ }
1479
+ prevSuccessRef.current = formSuccess;
1480
+ }, [addAlert, formSuccess, successTitle]);
1243
1481
  const computeInitialValues = () => {
1244
1482
  const vals = {};
1245
1483
  for (const field of fields) {
@@ -1255,25 +1493,99 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1255
1493
  const [internalErrors, setInternalErrors] = (0, import_react2.useState)({});
1256
1494
  const [internalStep, setInternalStep] = (0, import_react2.useState)(0);
1257
1495
  const [internalLoading, setInternalLoading] = (0, import_react2.useState)(false);
1258
- const [touchedFields, setTouchedFields] = (0, import_react2.useState)({});
1259
1496
  const [validatingFields, setValidatingFields] = (0, import_react2.useState)({});
1260
1497
  const asyncValidationRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1498
+ const asyncAbortRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1499
+ const asyncValidationVersionRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1261
1500
  const debounceTimersRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1501
+ const inputDebounceRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1502
+ const rowKeyRef = (0, import_react2.useRef)(/* @__PURE__ */ new WeakMap());
1503
+ const rowKeyCounterRef = (0, import_react2.useRef)(0);
1262
1504
  const initialSnapshot = (0, import_react2.useRef)(null);
1263
1505
  if (initialSnapshot.current === null) {
1264
- initialSnapshot.current = JSON.stringify(computeInitialValues());
1506
+ initialSnapshot.current = deepClone(computeInitialValues());
1265
1507
  }
1266
1508
  const formValues = values != null ? values : internalValues;
1509
+ const formErrors = controlledErrors != null ? controlledErrors : internalErrors;
1267
1510
  const currentStep = controlledStep != null ? controlledStep : internalStep;
1268
1511
  const isLoading = controlledLoading != null ? controlledLoading : internalLoading;
1269
1512
  const isMultiStep = Array.isArray(steps) && steps.length > 0;
1513
+ const formValuesRef = (0, import_react2.useRef)(formValues);
1514
+ const formErrorsRef = (0, import_react2.useRef)(formErrors);
1515
+ const draftValuesRef = (0, import_react2.useRef)(null);
1516
+ formValuesRef.current = formValues;
1517
+ formErrorsRef.current = formErrors;
1518
+ const fieldByName = (0, import_react2.useMemo)(() => {
1519
+ const map = /* @__PURE__ */ new Map();
1520
+ for (const field of fields) map.set(field.name, field);
1521
+ return map;
1522
+ }, [fields]);
1523
+ const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
1524
+ const configWarningsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
1525
+ const warnConfig = (0, import_react2.useCallback)((message) => {
1526
+ if (!isDev) return;
1527
+ if (configWarningsRef.current.has(message)) return;
1528
+ configWarningsRef.current.add(message);
1529
+ if (typeof console !== "undefined" && console.warn) {
1530
+ console.warn(`[FormBuilder] ${message}`);
1531
+ }
1532
+ }, [isDev]);
1533
+ const replaceErrors = (0, import_react2.useCallback)(
1534
+ (nextErrors) => {
1535
+ if (controlledErrors == null) setInternalErrors(nextErrors);
1536
+ if (onValidationChange) onValidationChange(nextErrors);
1537
+ },
1538
+ [controlledErrors, onValidationChange]
1539
+ );
1540
+ const updateErrors = (0, import_react2.useCallback)(
1541
+ (newErrors) => {
1542
+ const mergeErrors = (base) => {
1543
+ const merged = { ...base, ...newErrors };
1544
+ for (const key of Object.keys(newErrors)) {
1545
+ if (newErrors[key] === null || newErrors[key] === void 0) {
1546
+ delete merged[key];
1547
+ }
1548
+ }
1549
+ return merged;
1550
+ };
1551
+ if (controlledErrors != null) {
1552
+ const merged = mergeErrors(formErrorsRef.current || {});
1553
+ if (onValidationChange) onValidationChange(merged);
1554
+ return;
1555
+ }
1556
+ setInternalErrors((prev) => {
1557
+ const merged = mergeErrors(prev);
1558
+ if (onValidationChange) onValidationChange(merged);
1559
+ return merged;
1560
+ });
1561
+ },
1562
+ [controlledErrors, onValidationChange]
1563
+ );
1564
+ const getFieldEmptyValue = (0, import_react2.useCallback)(
1565
+ (field) => {
1566
+ const plugin = fieldTypes && fieldTypes[field.type];
1567
+ return plugin && plugin.getEmptyValue ? plugin.getEmptyValue() : getEmptyValue(field);
1568
+ },
1569
+ [fieldTypes]
1570
+ );
1571
+ const getRowKey = (0, import_react2.useCallback)((fieldName, row, index) => {
1572
+ if (!row || typeof row !== "object") return `${fieldName}-idx-${index}`;
1573
+ if (!rowKeyRef.current.has(row)) {
1574
+ rowKeyCounterRef.current += 1;
1575
+ rowKeyRef.current.set(row, `${fieldName}-row-${rowKeyCounterRef.current}`);
1576
+ }
1577
+ return rowKeyRef.current.get(row);
1578
+ }, []);
1270
1579
  (0, import_react2.useEffect)(() => {
1271
1580
  return () => {
1272
1581
  for (const timer of debounceTimersRef.current.values()) clearTimeout(timer);
1582
+ for (const timer of inputDebounceRef.current.values()) clearTimeout(timer);
1583
+ for (const controller of asyncAbortRef.current.values()) controller.abort();
1584
+ if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1273
1585
  };
1274
1586
  }, []);
1275
1587
  const isDirty = (0, import_react2.useMemo)(() => {
1276
- return JSON.stringify(formValues) !== initialSnapshot.current;
1588
+ return !deepEqual(formValues, initialSnapshot.current);
1277
1589
  }, [formValues]);
1278
1590
  const prevDirtyRef = (0, import_react2.useRef)(false);
1279
1591
  (0, import_react2.useEffect)(() => {
@@ -1283,36 +1595,129 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1283
1595
  }
1284
1596
  }, [isDirty, onDirtyChange]);
1285
1597
  const autoSaveTimerRef = (0, import_react2.useRef)(null);
1598
+ const autoSaveRef = (0, import_react2.useRef)(autoSave);
1599
+ autoSaveRef.current = autoSave;
1600
+ const prevAutoSaveValues = (0, import_react2.useRef)(deepClone(formValues));
1286
1601
  (0, import_react2.useEffect)(() => {
1287
- if (!autoSave || !autoSave.onAutoSave || !isDirty) return;
1602
+ const cfg = autoSaveRef.current;
1603
+ if (!cfg || !cfg.onAutoSave || !isDirty) return;
1604
+ if (deepEqual(prevAutoSaveValues.current, formValues)) return;
1605
+ prevAutoSaveValues.current = deepClone(formValues);
1288
1606
  if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1289
1607
  autoSaveTimerRef.current = setTimeout(() => {
1290
1608
  autoSaveTimerRef.current = null;
1291
- autoSave.onAutoSave(formValues);
1292
- }, autoSave.debounce || 1e3);
1609
+ autoSaveRef.current.onAutoSave(formValues);
1610
+ }, cfg.debounce || 1e3);
1293
1611
  return () => {
1294
1612
  if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1295
1613
  };
1296
- }, [formValues, isDirty, autoSave]);
1297
- const visibleFields = (0, import_react2.useMemo)(() => {
1298
- let filtered = fields.filter((f) => {
1614
+ }, [formValues, isDirty]);
1615
+ const allVisibleFields = (0, import_react2.useMemo)(() => {
1616
+ return fields.filter((f) => {
1299
1617
  if (f.visible && !f.visible(formValues)) return false;
1300
1618
  return true;
1301
1619
  });
1620
+ }, [fields, formValues]);
1621
+ const visibleFields = (0, import_react2.useMemo)(() => {
1622
+ let filtered = allVisibleFields;
1302
1623
  if (isMultiStep && steps[currentStep] && steps[currentStep].fields) {
1303
1624
  const stepFieldNames = new Set(steps[currentStep].fields);
1304
1625
  filtered = filtered.filter((f) => stepFieldNames.has(f.name));
1305
1626
  }
1306
1627
  return filtered;
1307
- }, [fields, formValues, isMultiStep, steps, currentStep]);
1628
+ }, [allVisibleFields, isMultiStep, steps, currentStep]);
1629
+ (0, import_react2.useEffect)(() => {
1630
+ const nameSet = new Set(fields.map((f) => f.name));
1631
+ if (nameSet.size !== fields.length) {
1632
+ warnConfig("Duplicate field names detected. Field names must be unique.");
1633
+ }
1634
+ for (const field of fields) {
1635
+ const parentName = getDependsOnName(field);
1636
+ if (parentName && !nameSet.has(parentName)) {
1637
+ warnConfig(`Field "${field.name}" depends on missing field "${parentName}".`);
1638
+ }
1639
+ }
1640
+ if (steps) {
1641
+ for (let i = 0; i < steps.length; i++) {
1642
+ const step = steps[i];
1643
+ if (!step.fields) continue;
1644
+ for (const fieldName of step.fields) {
1645
+ if (!nameSet.has(fieldName)) {
1646
+ warnConfig(`Step ${i + 1} references missing field "${fieldName}".`);
1647
+ }
1648
+ }
1649
+ }
1650
+ }
1651
+ if (layout) {
1652
+ for (const row of layout) {
1653
+ for (const entry of row) {
1654
+ const fieldName = typeof entry === "string" ? entry : entry.field;
1655
+ if (!nameSet.has(fieldName)) {
1656
+ warnConfig(`Layout references missing field "${fieldName}".`);
1657
+ }
1658
+ }
1659
+ }
1660
+ }
1661
+ if (sections) {
1662
+ for (const section of sections) {
1663
+ for (const fieldName of section.fields || []) {
1664
+ if (!nameSet.has(fieldName)) {
1665
+ warnConfig(`Section "${section.id}" references missing field "${fieldName}".`);
1666
+ }
1667
+ }
1668
+ }
1669
+ }
1670
+ }, [fields, steps, layout, sections, warnConfig]);
1671
+ const validateRepeaterField = (0, import_react2.useCallback)(
1672
+ (field, value, allValues) => {
1673
+ const errors = {};
1674
+ const rows = Array.isArray(value) ? value : [];
1675
+ const subFields = field.fields || [];
1676
+ let firstSubError = null;
1677
+ if (resolveRequired(field, allValues) && rows.length === 0) {
1678
+ const requiredError = `${field.label} is required`;
1679
+ errors[field.name] = requiredError;
1680
+ return { errors, hasErrors: true };
1681
+ }
1682
+ if (typeof field.min === "number" && rows.length < field.min) {
1683
+ errors[field.name] = `Must have at least ${field.min} ${field.min === 1 ? "row" : "rows"}`;
1684
+ } else if (typeof field.max === "number" && rows.length > field.max) {
1685
+ errors[field.name] = `Must have no more than ${field.max} ${field.max === 1 ? "row" : "rows"}`;
1686
+ }
1687
+ rows.forEach((row, rowIdx) => {
1688
+ const rowValues = { ...allValues, [field.name]: rows };
1689
+ subFields.forEach((subField) => {
1690
+ if (subField.visible && !subField.visible(rowValues)) return;
1691
+ const err = runValidators(row == null ? void 0 : row[subField.name], subField, rowValues, fieldTypes);
1692
+ if (!err) return;
1693
+ const key = getRepeaterErrorKey(field.name, rowIdx, subField.name);
1694
+ errors[key] = err;
1695
+ if (!firstSubError) firstSubError = { row: rowIdx + 1, message: err };
1696
+ });
1697
+ });
1698
+ if (!errors[field.name] && firstSubError) {
1699
+ errors[field.name] = `Row ${firstSubError.row}: ${firstSubError.message}`;
1700
+ }
1701
+ return { errors, hasErrors: Object.keys(errors).length > 0 };
1702
+ },
1703
+ [fieldTypes]
1704
+ );
1308
1705
  const validateField = (0, import_react2.useCallback)(
1309
1706
  (name, value) => {
1310
- const field = fields.find((f) => f.name === name);
1707
+ const field = fieldByName.get(name);
1311
1708
  if (!field) return null;
1312
1709
  if (field.visible && !field.visible(formValues)) return null;
1710
+ if (field.type === "repeater") {
1711
+ const repeaterResult = validateRepeaterField(
1712
+ field,
1713
+ value != null ? value : formValues[name],
1714
+ formValues
1715
+ );
1716
+ return repeaterResult.errors[name] || null;
1717
+ }
1313
1718
  return runValidators(value != null ? value : formValues[name], field, formValues, fieldTypes);
1314
1719
  },
1315
- [fields, formValues, fieldTypes]
1720
+ [fieldByName, formValues, validateRepeaterField, fieldTypes]
1316
1721
  );
1317
1722
  const validateVisibleFields = (0, import_react2.useCallback)(
1318
1723
  (fieldSubset) => {
@@ -1320,6 +1725,14 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1320
1725
  const errors = {};
1321
1726
  let hasErrors = false;
1322
1727
  for (const field of toValidate) {
1728
+ if (field.type === "repeater") {
1729
+ const repeaterResult = validateRepeaterField(field, formValues[field.name], formValues);
1730
+ if (repeaterResult.hasErrors) {
1731
+ Object.assign(errors, repeaterResult.errors);
1732
+ hasErrors = true;
1733
+ }
1734
+ continue;
1735
+ }
1323
1736
  const err = runValidators(formValues[field.name], field, formValues, fieldTypes);
1324
1737
  if (err) {
1325
1738
  errors[field.name] = err;
@@ -1328,64 +1741,87 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1328
1741
  }
1329
1742
  return { errors, hasErrors };
1330
1743
  },
1331
- [visibleFields, formValues]
1332
- );
1333
- const updateErrors = (0, import_react2.useCallback)(
1334
- (newErrors) => {
1335
- setInternalErrors((prev) => {
1336
- const merged = { ...prev, ...newErrors };
1337
- for (const key of Object.keys(merged)) {
1338
- if (newErrors[key] === null || newErrors[key] === void 0) {
1339
- delete merged[key];
1340
- }
1341
- }
1342
- if (onValidationChange) onValidationChange(merged);
1343
- return merged;
1344
- });
1345
- },
1346
- [onValidationChange]
1744
+ [visibleFields, formValues, validateRepeaterField, fieldTypes]
1347
1745
  );
1348
1746
  const runAsyncValidation = (0, import_react2.useCallback)(
1349
1747
  (name, value) => {
1350
- const field = fields.find((f) => f.name === name);
1351
- if (!field || !field.validate) return;
1748
+ const field = fieldByName.get(name);
1749
+ if (!field || field.type === "repeater") return null;
1352
1750
  const val = value != null ? value : formValues[name];
1353
- const syncError = runValidators(val, field, formValues, fieldTypes);
1354
- if (syncError) return;
1355
- const result = field.validate(val, formValues);
1356
- if (!result || typeof result.then !== "function") return;
1357
- const validationPromise = result.then(
1358
- (asyncResult) => {
1359
- if (asyncValidationRef.current.get(name) !== validationPromise) return;
1751
+ const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false });
1752
+ const prevController = asyncAbortRef.current.get(name);
1753
+ if (prevController) prevController.abort();
1754
+ asyncAbortRef.current.delete(name);
1755
+ setValidatingFields((prev) => {
1756
+ if (!prev[name]) return prev;
1757
+ const next = { ...prev };
1758
+ delete next[name];
1759
+ return next;
1760
+ });
1761
+ if (syncError) return null;
1762
+ const version = (asyncValidationVersionRef.current.get(name) || 0) + 1;
1763
+ asyncValidationVersionRef.current.set(name, version);
1764
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
1765
+ if (controller) asyncAbortRef.current.set(name, controller);
1766
+ let asyncPromises;
1767
+ try {
1768
+ asyncPromises = collectAsyncValidatorPromises(
1769
+ val,
1770
+ field,
1771
+ formValues,
1772
+ controller ? { signal: controller.signal } : void 0
1773
+ );
1774
+ } catch (err) {
1775
+ updateErrors({ [name]: (err == null ? void 0 : err.message) || "Validation failed" });
1776
+ return null;
1777
+ }
1778
+ if (asyncPromises.length === 0) {
1779
+ asyncAbortRef.current.delete(name);
1780
+ return null;
1781
+ }
1782
+ const validationPromise = Promise.all(asyncPromises).then(
1783
+ (results) => {
1784
+ if (asyncValidationVersionRef.current.get(name) !== version) return;
1360
1785
  asyncValidationRef.current.delete(name);
1786
+ asyncAbortRef.current.delete(name);
1361
1787
  setValidatingFields((prev) => {
1362
1788
  const next = { ...prev };
1363
1789
  delete next[name];
1364
1790
  return next;
1365
1791
  });
1366
- const err = asyncResult !== true && asyncResult ? asyncResult : null;
1792
+ let err = null;
1793
+ for (const result of results) {
1794
+ const normalized = normalizeValidatorResult(result);
1795
+ if (normalized) {
1796
+ err = normalized;
1797
+ break;
1798
+ }
1799
+ }
1367
1800
  updateErrors({ [name]: err });
1368
1801
  },
1369
1802
  (rejection) => {
1370
- if (asyncValidationRef.current.get(name) !== validationPromise) return;
1803
+ if (asyncValidationVersionRef.current.get(name) !== version) return;
1371
1804
  asyncValidationRef.current.delete(name);
1805
+ asyncAbortRef.current.delete(name);
1372
1806
  setValidatingFields((prev) => {
1373
1807
  const next = { ...prev };
1374
1808
  delete next[name];
1375
1809
  return next;
1376
1810
  });
1811
+ if (rejection && rejection.name === "AbortError") return;
1377
1812
  updateErrors({ [name]: (rejection == null ? void 0 : rejection.message) || "Validation failed" });
1378
1813
  }
1379
1814
  );
1380
1815
  asyncValidationRef.current.set(name, validationPromise);
1381
1816
  setValidatingFields((prev) => ({ ...prev, [name]: true }));
1817
+ return validationPromise;
1382
1818
  },
1383
- [fields, formValues, updateErrors]
1819
+ [fieldByName, formValues, fieldTypes, updateErrors]
1384
1820
  );
1385
1821
  const triggerAsyncValidation = (0, import_react2.useCallback)(
1386
1822
  (name, value) => {
1387
- const field = fields.find((f) => f.name === name);
1388
- if (!field || !field.validate) return;
1823
+ const field = fieldByName.get(name);
1824
+ if (!field || field.type === "repeater") return;
1389
1825
  const debounceMs = field.validateDebounce;
1390
1826
  if (debounceMs && debounceMs > 0) {
1391
1827
  const existing = debounceTimersRef.current.get(name);
@@ -1399,44 +1835,93 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1399
1835
  runAsyncValidation(name, value);
1400
1836
  }
1401
1837
  },
1402
- [fields, runAsyncValidation]
1838
+ [fieldByName, runAsyncValidation]
1403
1839
  );
1404
- const setFieldValueSilent = (0, import_react2.useCallback)(
1405
- (name, value) => {
1840
+ const commitValues = (0, import_react2.useCallback)(
1841
+ (nextValues) => {
1842
+ formValuesRef.current = nextValues;
1406
1843
  if (values != null) {
1407
- if (onChange) onChange({ ...formValues, [name]: value });
1844
+ if (onChange) onChange(nextValues);
1408
1845
  } else {
1409
- setInternalValues((prev) => ({ ...prev, [name]: value }));
1846
+ setInternalValues(nextValues);
1410
1847
  }
1411
1848
  },
1412
- [values, onChange, formValues]
1849
+ [values, onChange]
1850
+ );
1851
+ const setFieldValueSilent = (0, import_react2.useCallback)(
1852
+ (name, value) => {
1853
+ const base = draftValuesRef.current || formValuesRef.current || {};
1854
+ const nextValues = { ...base, [name]: value };
1855
+ draftValuesRef.current = nextValues;
1856
+ commitValues(nextValues);
1857
+ },
1858
+ [commitValues]
1413
1859
  );
1414
1860
  const handleFieldChange = (0, import_react2.useCallback)(
1415
1861
  (name, value) => {
1416
- const newValues = { ...formValues, [name]: value };
1417
- if (values != null) {
1418
- if (onChange) onChange(newValues);
1419
- } else {
1420
- setInternalValues(newValues);
1862
+ const newValues = { ...formValuesRef.current, [name]: value };
1863
+ const queue = [name];
1864
+ const visited = /* @__PURE__ */ new Set();
1865
+ const clearedErrors = {};
1866
+ while (queue.length > 0) {
1867
+ const current = queue.shift();
1868
+ if (!current || visited.has(current)) continue;
1869
+ visited.add(current);
1870
+ fields.forEach((dep) => {
1871
+ const parentName = getDependsOnName(dep);
1872
+ if (parentName !== current || dep.name === current) return;
1873
+ if (!dep.options) return;
1874
+ const depOptions = resolveOptions(dep, newValues);
1875
+ const depValue = newValues[dep.name];
1876
+ if (depValue == null || depValue === "") return;
1877
+ const validValues = new Set(depOptions.map((o) => o.value));
1878
+ let nextDepValue = depValue;
1879
+ let changed = false;
1880
+ if (Array.isArray(depValue)) {
1881
+ const filtered = depValue.filter((v) => validValues.has(v));
1882
+ if (filtered.length !== depValue.length) {
1883
+ nextDepValue = filtered;
1884
+ changed = true;
1885
+ }
1886
+ } else if (!validValues.has(depValue)) {
1887
+ nextDepValue = getFieldEmptyValue(dep);
1888
+ changed = true;
1889
+ }
1890
+ if (changed) {
1891
+ newValues[dep.name] = nextDepValue;
1892
+ queue.push(dep.name);
1893
+ if (formErrorsRef.current[dep.name] != null) {
1894
+ clearedErrors[dep.name] = null;
1895
+ }
1896
+ }
1897
+ });
1421
1898
  }
1422
- if (onFieldChange) onFieldChange(name, value, newValues);
1423
- if (internalErrors[name]) {
1424
- updateErrors({ [name]: null });
1899
+ if (formErrorsRef.current[name] != null) {
1900
+ clearedErrors[name] = null;
1901
+ }
1902
+ for (const key of Object.keys(formErrorsRef.current)) {
1903
+ if (key.startsWith(`${name}[`)) {
1904
+ clearedErrors[key] = null;
1905
+ }
1425
1906
  }
1426
- const field = fields.find((f) => f.name === name);
1907
+ draftValuesRef.current = newValues;
1908
+ commitValues(newValues);
1909
+ if (onFieldChange) onFieldChange(name, value, newValues);
1910
+ if (Object.keys(clearedErrors).length > 0) updateErrors(clearedErrors);
1911
+ const field = fieldByName.get(name);
1427
1912
  if (field && field.onFieldChange) {
1428
1913
  field.onFieldChange(value, newValues, {
1429
1914
  setFieldValue: setFieldValueSilent,
1430
1915
  setFieldError: (fieldName, message) => updateErrors({ [fieldName]: message })
1431
1916
  });
1432
1917
  }
1918
+ draftValuesRef.current = null;
1433
1919
  },
1434
- [formValues, values, onChange, onFieldChange, internalErrors, updateErrors, fields, setFieldValueSilent]
1920
+ [fields, getFieldEmptyValue, commitValues, onFieldChange, updateErrors, fieldByName, setFieldValueSilent]
1435
1921
  );
1436
- const inputDebounceRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1437
1922
  const handleDebouncedFieldChange = (0, import_react2.useCallback)(
1438
1923
  (name, value) => {
1439
- const field = fields.find((f) => f.name === name);
1924
+ const field = fieldByName.get(name);
1440
1925
  const debounceMs = field && field.debounce;
1441
1926
  if (debounceMs && debounceMs > 0) {
1442
1927
  const existing = inputDebounceRef.current.get(name);
@@ -1450,58 +1935,59 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1450
1935
  handleFieldChange(name, value);
1451
1936
  }
1452
1937
  },
1453
- [fields, handleFieldChange]
1938
+ [fieldByName, handleFieldChange]
1454
1939
  );
1455
1940
  const handleFieldInput = (0, import_react2.useCallback)(
1456
1941
  (name, value) => {
1457
- if (validateOnChange) {
1458
- const err = validateField(name, value);
1459
- updateErrors({ [name]: err });
1460
- }
1942
+ if (!validateOnChange) return;
1943
+ const err = validateField(name, value);
1944
+ updateErrors({ [name]: err });
1461
1945
  },
1462
1946
  [validateOnChange, validateField, updateErrors]
1463
1947
  );
1464
1948
  const handleFieldBlur = (0, import_react2.useCallback)(
1465
1949
  (name, value) => {
1466
- setTouchedFields((prev) => ({ ...prev, [name]: true }));
1467
- if (validateOnBlur) {
1468
- const err = validateField(name, value != null ? value : formValues[name]);
1469
- updateErrors({ [name]: err });
1470
- if (!err) {
1471
- triggerAsyncValidation(name, value != null ? value : formValues[name]);
1472
- }
1950
+ if (!validateOnBlur) return;
1951
+ const resolvedValue = value != null ? value : formValuesRef.current[name];
1952
+ const err = validateField(name, resolvedValue);
1953
+ updateErrors({ [name]: err });
1954
+ if (!err) {
1955
+ triggerAsyncValidation(name, resolvedValue);
1473
1956
  }
1474
1957
  },
1475
- [validateOnBlur, validateField, updateErrors, formValues, triggerAsyncValidation]
1958
+ [validateOnBlur, validateField, updateErrors, triggerAsyncValidation]
1476
1959
  );
1477
1960
  const handleSubmit = (0, import_react2.useCallback)(
1478
1961
  async (e) => {
1479
1962
  if (e && e.preventDefault) e.preventDefault();
1480
1963
  if (validateOnSubmit) {
1481
- const allVisible = fields.filter((f) => !f.visible || f.visible(formValues));
1482
- const { errors, hasErrors } = validateVisibleFields(allVisible);
1964
+ const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
1483
1965
  if (hasErrors) {
1484
- setInternalErrors(errors);
1485
- if (onValidationChange) onValidationChange(errors);
1966
+ replaceErrors(errors);
1486
1967
  return;
1487
1968
  }
1488
- if (asyncValidationRef.current.size > 0) {
1489
- await Promise.all(asyncValidationRef.current.values());
1490
- const currentErrors = { ...internalErrors };
1491
- const hasAsyncErrors = Object.keys(currentErrors).length > 0;
1492
- if (hasAsyncErrors) return;
1969
+ const asyncSubmitValidations = allVisibleFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
1970
+ if (asyncSubmitValidations.length > 0 || asyncValidationRef.current.size > 0) {
1971
+ const pendingValidations = [
1972
+ .../* @__PURE__ */ new Set([
1973
+ ...asyncSubmitValidations,
1974
+ ...Array.from(asyncValidationRef.current.values())
1975
+ ])
1976
+ ];
1977
+ await Promise.all(pendingValidations);
1978
+ if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) return;
1493
1979
  }
1494
1980
  }
1495
1981
  const reset = () => {
1496
1982
  const fresh = computeInitialValues();
1497
1983
  if (values == null) setInternalValues(fresh);
1498
- setInternalErrors({});
1499
- setTouchedFields({});
1500
- initialSnapshot.current = JSON.stringify(fresh);
1984
+ replaceErrors({});
1985
+ initialSnapshot.current = deepClone(fresh);
1986
+ prevAutoSaveValues.current = deepClone(fresh);
1501
1987
  };
1502
1988
  const rawValues = {};
1503
1989
  for (const key of Object.keys(formValues)) {
1504
- const f = fields.find((fd) => fd.name === key);
1990
+ const f = fieldByName.get(key);
1505
1991
  if (f && (f.type === "display" || f.type === "crmPropertyList" || f.type === "crmAssociationPropertyList")) continue;
1506
1992
  rawValues[key] = formValues[key];
1507
1993
  }
@@ -1525,25 +2011,34 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1525
2011
  if (controlledLoading == null) setInternalLoading(false);
1526
2012
  }
1527
2013
  },
1528
- [validateOnSubmit, fields, formValues, validateVisibleFields, onValidationChange, onSubmit, values, controlledLoading, internalErrors, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess]
2014
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, runAsyncValidation]
1529
2015
  );
1530
- const handleNext = (0, import_react2.useCallback)(() => {
2016
+ const handleNext = (0, import_react2.useCallback)(async () => {
1531
2017
  if (!isMultiStep) return;
1532
2018
  if (validateStepOnNext && steps[currentStep] && steps[currentStep].fields) {
1533
- const stepFields = fields.filter(
1534
- (f) => steps[currentStep].fields.includes(f.name) && (!f.visible || f.visible(formValues))
1535
- );
2019
+ const stepFieldNames = new Set(steps[currentStep].fields);
2020
+ const stepFields = allVisibleFields.filter((f) => stepFieldNames.has(f.name));
1536
2021
  const { errors, hasErrors } = validateVisibleFields(stepFields);
1537
2022
  if (hasErrors) {
1538
- setInternalErrors((prev) => ({ ...prev, ...errors }));
1539
- if (onValidationChange) onValidationChange({ ...internalErrors, ...errors });
2023
+ replaceErrors({ ...formErrorsRef.current, ...errors });
1540
2024
  return;
1541
2025
  }
2026
+ const asyncStepValidations = stepFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
2027
+ if (asyncStepValidations.length > 0 || asyncValidationRef.current.size > 0) {
2028
+ const pendingValidations = [
2029
+ .../* @__PURE__ */ new Set([
2030
+ ...asyncStepValidations,
2031
+ ...Array.from(asyncValidationRef.current.values())
2032
+ ])
2033
+ ];
2034
+ await Promise.all(pendingValidations);
2035
+ if (fieldSetHasErrors(formErrorsRef.current, stepFields)) return;
2036
+ }
1542
2037
  }
1543
2038
  if (steps[currentStep] && steps[currentStep].validate) {
1544
2039
  const result = steps[currentStep].validate(formValues);
1545
2040
  if (result !== true && result) {
1546
- setInternalErrors((prev) => ({ ...prev, ...result }));
2041
+ replaceErrors({ ...formErrorsRef.current, ...result });
1547
2042
  return;
1548
2043
  }
1549
2044
  }
@@ -1553,7 +2048,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1553
2048
  } else {
1554
2049
  setInternalStep(nextStep);
1555
2050
  }
1556
- }, [isMultiStep, validateStepOnNext, steps, currentStep, fields, formValues, validateVisibleFields, onValidationChange, internalErrors, controlledStep, onStepChange]);
2051
+ }, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, runAsyncValidation]);
1557
2052
  const handleBack = (0, import_react2.useCallback)(() => {
1558
2053
  if (!isMultiStep) return;
1559
2054
  const prevStep = Math.max(currentStep - 1, 0);
@@ -1578,33 +2073,56 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1578
2073
  (0, import_react2.useImperativeHandle)(ref, () => ({
1579
2074
  submit: handleSubmit,
1580
2075
  validate: () => {
1581
- const allVisible = fields.filter((f) => !f.visible || f.visible(formValues));
1582
- const { errors, hasErrors } = validateVisibleFields(allVisible);
1583
- setInternalErrors(errors);
2076
+ const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
2077
+ replaceErrors(errors);
1584
2078
  return { valid: !hasErrors, errors };
1585
2079
  },
1586
2080
  reset: () => {
1587
2081
  const fresh = computeInitialValues();
1588
2082
  if (values == null) setInternalValues(fresh);
1589
- setInternalErrors({});
1590
- setTouchedFields({});
1591
- initialSnapshot.current = JSON.stringify(fresh);
2083
+ replaceErrors({});
2084
+ initialSnapshot.current = deepClone(fresh);
2085
+ prevAutoSaveValues.current = deepClone(fresh);
1592
2086
  },
1593
2087
  getValues: () => formValues,
1594
2088
  isDirty: () => isDirty,
1595
2089
  setFieldValue: (name, value) => handleFieldChange(name, value),
1596
2090
  setFieldError: (name, message) => updateErrors({ [name]: message }),
1597
2091
  setErrors: (errors) => {
1598
- setInternalErrors(errors);
1599
- if (onValidationChange) onValidationChange(errors);
2092
+ replaceErrors(errors);
1600
2093
  }
1601
2094
  }));
2095
+ const setRepeaterSubFieldError = (0, import_react2.useCallback)(
2096
+ (fieldName, rowIdx, subFieldName, errorMessage) => {
2097
+ const key = getRepeaterErrorKey(fieldName, rowIdx, subFieldName);
2098
+ const merged = { ...formErrorsRef.current };
2099
+ if (errorMessage) {
2100
+ merged[key] = errorMessage;
2101
+ } else {
2102
+ delete merged[key];
2103
+ }
2104
+ const subErrors = Object.keys(merged).filter((k) => k.startsWith(`${fieldName}[`)).map((k) => {
2105
+ const match = k.match(/\[(\d+)\]\./);
2106
+ const row = match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
2107
+ return { key: k, row };
2108
+ }).sort((a, b) => a.row - b.row);
2109
+ if (subErrors.length > 0) {
2110
+ const first = subErrors[0];
2111
+ merged[fieldName] = `Row ${first.row + 1}: ${merged[first.key]}`;
2112
+ } else if (!merged[fieldName] || merged[fieldName].startsWith("Row ")) {
2113
+ delete merged[fieldName];
2114
+ }
2115
+ replaceErrors(merged);
2116
+ },
2117
+ [replaceErrors]
2118
+ );
1602
2119
  const renderField = (field) => {
1603
2120
  const fieldValue = formValues[field.name];
1604
- const fieldError = internalErrors[field.name] || null;
2121
+ const fieldError = formErrors[field.name] || null;
1605
2122
  const hasError = !!fieldError;
1606
2123
  const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
1607
- const isReadOnly = field.readOnly || disabled || formReadOnly;
2124
+ const isReadOnly = field.readOnly || formReadOnly;
2125
+ const isDisabled = disabled || field.disabled || formReadOnly;
1608
2126
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
1609
2127
  if (field.type === "display") {
1610
2128
  if (field.render) {
@@ -1663,6 +2181,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1663
2181
  tooltip: field.tooltip,
1664
2182
  required: isRequired,
1665
2183
  readOnly: isReadOnly,
2184
+ disabled: isDisabled,
1666
2185
  error: hasError,
1667
2186
  validationMessage: fieldError || void 0,
1668
2187
  ...field.loading || validatingFields[field.name] ? { loading: true } : {},
@@ -1795,6 +2314,9 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1795
2314
  maxValidationMessage: field.maxValidationMessage,
1796
2315
  onChange: (v) => {
1797
2316
  handleFieldChange(field.name, { ...fieldValue, date: v, time: timeVal });
2317
+ },
2318
+ onBlur: (v) => {
2319
+ handleFieldBlur(field.name, { ...fieldValue, date: v, time: timeVal });
1798
2320
  }
1799
2321
  }
1800
2322
  )), /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { flex: 1 }, /* @__PURE__ */ import_react2.default.createElement(
@@ -1805,12 +2327,16 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1805
2327
  description: field.description,
1806
2328
  tooltip: field.tooltip,
1807
2329
  readOnly: isReadOnly,
2330
+ disabled: isDisabled,
1808
2331
  error: hasError,
1809
2332
  value: timeVal,
1810
2333
  interval: field.interval,
1811
2334
  timezone: field.timezone,
1812
2335
  onChange: (v) => {
1813
2336
  handleFieldChange(field.name, { ...fieldValue, date: dateVal, time: v });
2337
+ },
2338
+ onBlur: (v) => {
2339
+ handleFieldBlur(field.name, { ...fieldValue, date: dateVal, time: v });
1814
2340
  }
1815
2341
  }
1816
2342
  )));
@@ -1848,6 +2374,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1848
2374
  textChecked: field.textChecked,
1849
2375
  textUnchecked: field.textUnchecked,
1850
2376
  readonly: isReadOnly,
2377
+ disabled: isDisabled,
1851
2378
  onChange: fieldOnChange,
1852
2379
  ...field.fieldProps || {}
1853
2380
  }
@@ -1860,6 +2387,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1860
2387
  checked: !!fieldValue,
1861
2388
  description: field.description,
1862
2389
  readOnly: isReadOnly,
2390
+ disabled: isDisabled,
1863
2391
  inline: field.inline,
1864
2392
  variant: field.variant,
1865
2393
  onChange: fieldOnChange,
@@ -1896,61 +2424,140 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1896
2424
  case "repeater": {
1897
2425
  const rows = Array.isArray(fieldValue) ? fieldValue : [];
1898
2426
  const subFields = field.fields || [];
1899
- const minRows = field.min || 0;
1900
- const maxRows = field.max || Infinity;
1901
- const canAdd = rows.length < maxRows && !isReadOnly;
1902
- const canRemove = rows.length > minRows && !isReadOnly;
2427
+ const minRows = typeof field.min === "number" ? field.min : 0;
2428
+ const maxRows = typeof field.max === "number" ? field.max : Infinity;
2429
+ const repeaterProps = field.repeaterProps || {};
2430
+ const renderAddControl = repeaterProps.renderAdd;
2431
+ const renderRemoveControl = repeaterProps.renderRemove;
2432
+ const renderMoveUpControl = repeaterProps.renderMoveUp;
2433
+ const renderMoveDownControl = repeaterProps.renderMoveDown;
2434
+ const addLabel = repeaterProps.addLabel || "Add";
2435
+ const removeLabel = repeaterProps.removeLabel || "Remove";
2436
+ const moveUpLabel = repeaterProps.moveUpLabel || "Up";
2437
+ const moveDownLabel = repeaterProps.moveDownLabel || "Down";
2438
+ const canEditRows = !isReadOnly && !isDisabled;
2439
+ const canAdd = rows.length < maxRows && canEditRows;
2440
+ const canRemove = rows.length > minRows && canEditRows;
2441
+ const canReorder = !!repeaterProps.reorderable && canEditRows;
2442
+ const repeaterHasNestedErrors = Object.keys(formErrors).some(
2443
+ (k) => k.startsWith(`${field.name}[`)
2444
+ );
2445
+ const firstNestedErrorKey = Object.keys(formErrors).find(
2446
+ (k) => k.startsWith(`${field.name}[`)
2447
+ );
2448
+ const repeaterErrorMessage = fieldError || (firstNestedErrorKey ? formErrors[firstNestedErrorKey] : null);
2449
+ const repeaterHasError = !!fieldError || repeaterHasNestedErrors;
1903
2450
  const addRow = () => {
1904
2451
  const emptyRow = {};
1905
2452
  for (const sf of subFields) {
1906
- emptyRow[sf.name] = sf.defaultValue !== void 0 ? sf.defaultValue : getEmptyValue(sf);
2453
+ emptyRow[sf.name] = sf.defaultValue !== void 0 ? sf.defaultValue : getFieldEmptyValue(sf);
1907
2454
  }
1908
2455
  handleFieldChange(field.name, [...rows, emptyRow]);
1909
2456
  };
1910
2457
  const removeRow = (idx) => {
1911
2458
  handleFieldChange(field.name, rows.filter((_, i) => i !== idx));
1912
2459
  };
1913
- const updateRow = (idx, subName, subValue) => {
2460
+ const moveRow = (fromIndex, toIndex) => {
2461
+ if (toIndex < 0 || toIndex >= rows.length || toIndex === fromIndex) return;
2462
+ const updated = [...rows];
2463
+ const [moved] = updated.splice(fromIndex, 1);
2464
+ updated.splice(toIndex, 0, moved);
2465
+ handleFieldChange(field.name, updated);
2466
+ };
2467
+ const validateSubField = (rowIdx, subField, subValue, nextRows) => {
2468
+ const rowValues = { ...formValues, [field.name]: nextRows };
2469
+ const err = runValidators(subValue, subField, rowValues, fieldTypes);
2470
+ setRepeaterSubFieldError(field.name, rowIdx, subField.name, err);
2471
+ };
2472
+ const handleSubFieldChange = (rowIdx, subField, subValue) => {
1914
2473
  const updated = rows.map(
1915
- (row, i) => i === idx ? { ...row, [subName]: subValue } : row
2474
+ (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
1916
2475
  );
1917
2476
  handleFieldChange(field.name, updated);
2477
+ if (validateOnChange) {
2478
+ validateSubField(rowIdx, subField, subValue, updated);
2479
+ }
2480
+ };
2481
+ const handleSubFieldBlur = (rowIdx, subField, subValue) => {
2482
+ if (!validateOnBlur) return;
2483
+ const nextRows = rows.map(
2484
+ (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
2485
+ );
2486
+ validateSubField(rowIdx, subField, subValue, nextRows);
1918
2487
  };
1919
- return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { format: { fontWeight: "demibold" } }, field.label, isRequired ? " *" : ""), field.description && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { variant: "microcopy" }, field.description), rows.map((row, rowIdx) => /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { key: rowIdx, direction: "row", gap: "xs", align: "end" }, subFields.map((sf) => {
2488
+ return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { format: { fontWeight: "demibold" } }, field.label, isRequired ? " *" : ""), field.description && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { variant: "microcopy" }, field.description), rows.map((row, rowIdx) => /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { key: getRowKey(field.name, row, rowIdx), direction: "row", gap: "xs", align: "end" }, subFields.map((sf) => {
1920
2489
  const sfValue = row[sf.name];
1921
2490
  const sfLabel = rowIdx === 0 ? sf.label : void 0;
1922
- const sfOptions = resolveOptions(sf, formValues);
2491
+ const sfOptions = resolveOptions(sf, { ...formValues, [field.name]: rows });
2492
+ const sfError = formErrors[getRepeaterErrorKey(field.name, rowIdx, sf.name)] || null;
1923
2493
  const sfProps = {
1924
2494
  name: `${field.name}-${rowIdx}-${sf.name}`,
1925
2495
  label: sfLabel,
1926
2496
  placeholder: sf.placeholder,
1927
- readOnly: isReadOnly,
2497
+ readOnly: sf.readOnly || isReadOnly,
2498
+ disabled: sf.disabled || isDisabled,
2499
+ error: !!sfError,
2500
+ validationMessage: sfError || void 0,
1928
2501
  ...sf.fieldProps || {}
1929
2502
  };
1930
2503
  let sfElement;
1931
2504
  switch (sf.type) {
1932
2505
  case "select":
1933
- sfElement = /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Select, { ...sfProps, value: sfValue, options: sfOptions, onChange: (v) => updateRow(rowIdx, sf.name, v) });
2506
+ sfElement = /* @__PURE__ */ import_react2.default.createElement(
2507
+ import_ui_extensions2.Select,
2508
+ {
2509
+ ...sfProps,
2510
+ value: sfValue,
2511
+ options: sfOptions,
2512
+ onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
2513
+ onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
2514
+ }
2515
+ );
1934
2516
  break;
1935
2517
  case "number":
1936
- sfElement = /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.NumberInput, { ...sfProps, value: sfValue, onChange: (v) => updateRow(rowIdx, sf.name, v) });
2518
+ sfElement = /* @__PURE__ */ import_react2.default.createElement(
2519
+ import_ui_extensions2.NumberInput,
2520
+ {
2521
+ ...sfProps,
2522
+ value: sfValue,
2523
+ onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
2524
+ onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
2525
+ }
2526
+ );
1937
2527
  break;
1938
2528
  case "checkbox":
1939
- sfElement = /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Checkbox, { ...sfProps, checked: !!sfValue, onChange: (v) => updateRow(rowIdx, sf.name, v) }, sf.label);
2529
+ sfElement = /* @__PURE__ */ import_react2.default.createElement(
2530
+ import_ui_extensions2.Checkbox,
2531
+ {
2532
+ ...sfProps,
2533
+ checked: !!sfValue,
2534
+ onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
2535
+ onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
2536
+ },
2537
+ sf.label
2538
+ );
1940
2539
  break;
1941
2540
  default:
1942
- sfElement = /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Input, { ...sfProps, value: sfValue || "", onChange: (v) => updateRow(rowIdx, sf.name, v) });
2541
+ sfElement = /* @__PURE__ */ import_react2.default.createElement(
2542
+ import_ui_extensions2.Input,
2543
+ {
2544
+ ...sfProps,
2545
+ value: sfValue || "",
2546
+ onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
2547
+ onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
2548
+ }
2549
+ );
1943
2550
  }
1944
2551
  return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { key: sf.name, flex: 1 }, sfElement);
1945
- }), canRemove && /* @__PURE__ */ import_react2.default.createElement(
2552
+ }), /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Inline, { gap: "xs" }, canReorder && rowIdx > 0 && (renderMoveUpControl ? renderMoveUpControl({ index: rowIdx, onClick: () => moveRow(rowIdx, rowIdx - 1) }) : /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.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_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", size: "sm", onClick: () => moveRow(rowIdx, rowIdx + 1) }, moveDownLabel)), canRemove && (renderRemoveControl ? renderRemoveControl({ index: rowIdx, onClick: () => removeRow(rowIdx) }) : /* @__PURE__ */ import_react2.default.createElement(
1946
2553
  import_ui_extensions2.Button,
1947
2554
  {
1948
2555
  variant: "secondary",
1949
- size: "xs",
2556
+ size: "md",
1950
2557
  onClick: () => removeRow(rowIdx)
1951
2558
  },
1952
- "Remove"
1953
- ))), canAdd && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", size: "sm", onClick: addRow }, "+ Add"), hasError && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { variant: "microcopy" }, fieldError));
2559
+ removeLabel
2560
+ ))))), canAdd && (renderAddControl ? renderAddControl({ onClick: addRow, count: rows.length }) : /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Link, { onClick: addRow }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "row", align: "center", gap: "flush" }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Icon, { name: "add" }), /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { format: { fontWeight: "demibold" } }, addLabel)))), repeaterHasError && repeaterErrorMessage && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { variant: "microcopy" }, repeaterErrorMessage));
1954
2561
  }
1955
2562
  default:
1956
2563
  return /* @__PURE__ */ import_react2.default.createElement(
@@ -1970,15 +2577,17 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1970
2577
  if (field.width === "full" && columns > 1) return columns;
1971
2578
  return 1;
1972
2579
  };
1973
- const getDependents = (parentField) => visibleFields.filter((f) => f.dependsOn === parentField.name && f.name !== parentField.name);
1974
- const isDependent = (field) => field.dependsOn && visibleFields.some((f) => f.name === field.dependsOn && f.name !== field.name);
2580
+ const getDependents = (parentField) => visibleFields.filter(
2581
+ (f) => getDependsOnName(f) === parentField.name && f.name !== parentField.name && getDependsOnDisplay(f) === "grouped"
2582
+ );
2583
+ const isDependent = (field) => getDependsOnName(field) && getDependsOnDisplay(field) === "grouped" && visibleFields.some((f) => f.name === getDependsOnName(field) && f.name !== field.name);
1975
2584
  const renderDependentGroup = (parentField, dependents) => {
1976
- const firstWithLabel = dependents.find((f) => f.dependsOnLabel) || dependents[0];
1977
- const firstWithMessage = dependents.find((f) => f.dependsOnMessage) || dependents[0];
1978
- const groupLabel = firstWithLabel.dependsOnLabel || "Dependent properties";
1979
- const rawMessage = firstWithMessage.dependsOnMessage;
2585
+ const firstWithLabel = dependents.find((f) => getDependsOnLabel(f)) || dependents[0];
2586
+ const firstWithMessage = dependents.find((f) => getDependsOnMessage(f)) || dependents[0];
2587
+ const groupLabel = getDependsOnLabel(firstWithLabel) || "Dependent properties";
2588
+ const rawMessage = getDependsOnMessage(firstWithMessage);
1980
2589
  const tooltipMessage = typeof rawMessage === "function" ? rawMessage(parentField.label) : rawMessage || "";
1981
- return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Tile, { key: `dep-${parentField.name}`, compact: true }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "column", gap }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { format: { fontWeight: "demibold" } }, groupLabel, " ", tooltipMessage && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Link, { inline: true, variant: "dark", overlay: /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Tooltip, null, tooltipMessage) }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Icon, { name: "info" })))), dependents.map((dep) => /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, { key: dep.name }, renderField(dep)))));
2590
+ return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Tile, { key: `dep-${parentField.name}`, compact: true }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "column", gap }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { format: { fontWeight: "demibold" } }, groupLabel, " ", tooltipMessage && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Link, { inline: true, variant: "dark", overlay: /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Tooltip, null, tooltipMessage) }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Icon, { name: "info" })))), renderFieldSubset(dependents)));
1982
2591
  };
1983
2592
  const renderGridLayout = (fieldSubset) => {
1984
2593
  const fieldList = fieldSubset || visibleFields;
@@ -2071,7 +2680,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2071
2680
  let i = 0;
2072
2681
  while (i < fieldList.length) {
2073
2682
  const field = fieldList[i];
2074
- if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !field.dependsOn) {
2683
+ if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !getDependsOnName(field)) {
2075
2684
  rows.push({ type: "pair", fields: [fieldList[i], fieldList[i + 1]] });
2076
2685
  i += 2;
2077
2686
  } else {
@@ -2107,9 +2716,12 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2107
2716
  let batch = [];
2108
2717
  const flushBatch = () => {
2109
2718
  if (batch.length === 0) return;
2110
- elements.push(
2111
- /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.AutoGrid, { key: `ag-${batch[0].name}`, columnWidth, flexible: true, gap }, batch.map((f) => /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, { key: f.name }, renderField(f))))
2112
- );
2719
+ const chunks = maxColumns ? Array.from({ length: Math.ceil(batch.length / maxColumns) }, (_, i) => batch.slice(i * maxColumns, i * maxColumns + maxColumns)) : [batch];
2720
+ for (const chunk of chunks) {
2721
+ elements.push(
2722
+ /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.AutoGrid, { key: `ag-${chunk[0].name}`, columnWidth, flexible: true, gap }, chunk.map((f) => /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, { key: f.name }, renderField(f))))
2723
+ );
2724
+ }
2113
2725
  batch = [];
2114
2726
  };
2115
2727
  for (const field of fieldList) {
@@ -2192,7 +2804,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2192
2804
  );
2193
2805
  if (sec.info) {
2194
2806
  elements.push(
2195
- /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { key: sec.id, direction: "row", align: "start", gap: "flush" }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { flex: 1 }, accordion), /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Link, { overlay: /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Tooltip, null, sec.info) }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Icon, { name: "info", size: "sm", screenReaderText: sec.info })))
2807
+ /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { key: sec.id, direction: "row", align: "start", justify: "start", gap: "flush" }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { flex: 1 }, accordion), /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Link, { variant: "dark", overlay: /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Tooltip, null, sec.info) }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Icon, { name: "info", size: "sm", screenReaderText: sec.info })))
2196
2808
  );
2197
2809
  } else {
2198
2810
  elements.push(accordion);
@@ -2220,8 +2832,30 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2220
2832
  if (submitPosition === "none" || formReadOnly) return null;
2221
2833
  const isLastStep = !isMultiStep || currentStep === steps.length - 1;
2222
2834
  const isFirstStep = !isMultiStep || currentStep === 0;
2835
+ const buttonContext = {
2836
+ isMultiStep,
2837
+ isFirstStep,
2838
+ isLastStep,
2839
+ currentStep,
2840
+ totalSteps: isMultiStep ? steps.length : 1,
2841
+ disabled,
2842
+ loading: isLoading,
2843
+ labels: {
2844
+ submit: submitButtonLabel,
2845
+ cancel: cancelButtonLabel,
2846
+ back: backButtonLabel,
2847
+ next: nextButtonLabel
2848
+ },
2849
+ onBack: handleBack,
2850
+ onNext: handleNext,
2851
+ onCancel,
2852
+ onSubmit: handleSubmit
2853
+ };
2854
+ if (renderButtonsProp) {
2855
+ return renderButtonsProp(buttonContext);
2856
+ }
2223
2857
  if (isMultiStep) {
2224
- return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", onClick: handleBack, disabled }, "Back") : showCancel ? /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelLabel) : /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, null, " "), /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Inline, { gap: "small" }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { variant: "microcopy" }, "Step ", currentStep + 1, " of ", steps.length), isLastStep ? /* @__PURE__ */ import_react2.default.createElement(
2858
+ return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", onClick: handleBack, disabled }, backButtonLabel) : showCancel ? /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel) : /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, null, " "), /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Inline, { gap: "small" }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { variant: "microcopy" }, "Step ", currentStep + 1, " of ", steps.length), isLastStep ? /* @__PURE__ */ import_react2.default.createElement(
2225
2859
  import_ui_extensions2.LoadingButton,
2226
2860
  {
2227
2861
  variant: submitVariant,
@@ -2229,10 +2863,10 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2229
2863
  onClick: handleSubmit,
2230
2864
  disabled
2231
2865
  },
2232
- submitLabel
2233
- ) : /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "primary", onClick: handleNext, disabled }, "Next")));
2866
+ submitButtonLabel
2867
+ ) : /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "primary", onClick: handleNext, disabled }, nextButtonLabel)));
2234
2868
  }
2235
- return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelLabel), /* @__PURE__ */ import_react2.default.createElement(
2869
+ return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ import_react2.default.createElement(
2236
2870
  import_ui_extensions2.LoadingButton,
2237
2871
  {
2238
2872
  variant: submitVariant,
@@ -2241,7 +2875,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2241
2875
  onClick: noFormWrapper ? handleSubmit : void 0,
2242
2876
  disabled
2243
2877
  },
2244
- submitLabel
2878
+ submitButtonLabel
2245
2879
  ));
2246
2880
  };
2247
2881
  const formContent = /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "column", gap }, isMultiStep && showStepIndicator && /* @__PURE__ */ import_react2.default.createElement(
@@ -2250,7 +2884,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2250
2884
  currentStep,
2251
2885
  stepNames: steps.map((s) => s.title)
2252
2886
  }
2253
- ), formReadOnly && readOnlyMessage && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Alert, { title: "Read Only", variant: "warning" }, readOnlyMessage), formError && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Alert, { title: "Error", variant: "danger" }, typeof formError === "string" ? formError : void 0), formSuccess && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Alert, { title: "Success", variant: "success" }, formSuccess), isMultiStep && steps[currentStep] && steps[currentStep].render ? steps[currentStep].render({
2887
+ ), formReadOnly && readOnlyMessage && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Alert, { title: readOnlyTitle, variant: "warning" }, readOnlyMessage), !addAlert && formError && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Alert, { title: errorTitle, variant: "danger" }, typeof formError === "string" ? formError : void 0), !addAlert && formSuccess && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Alert, { title: successTitle, variant: "success" }, formSuccess), isMultiStep && steps[currentStep] && steps[currentStep].render ? steps[currentStep].render({
2254
2888
  values: formValues,
2255
2889
  goNext: handleNext,
2256
2890
  goBack: handleBack,
@@ -2262,7 +2896,15 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2262
2896
  if (noFormWrapper) {
2263
2897
  return formContent;
2264
2898
  }
2265
- return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Form, { onSubmit: handleSubmit, autoComplete: props.autoComplete }, formContent);
2899
+ return /* @__PURE__ */ import_react2.default.createElement(
2900
+ import_ui_extensions2.Form,
2901
+ {
2902
+ ...formProps || {},
2903
+ onSubmit: handleSubmit,
2904
+ autoComplete
2905
+ },
2906
+ formContent
2907
+ );
2266
2908
  });
2267
2909
  // Annotate the CommonJS export names for ESM import in node:
2268
2910
  0 && (module.exports = {