hs-uix 1.0.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -84,12 +84,19 @@ var computeAutoWidths = (columns, data) => {
84
84
  columns.forEach((col) => {
85
85
  if (col.width && col.cellWidth) return;
86
86
  const values = sample.map((row) => row[col.field]).filter((v) => v != null);
87
- const strings = values.map((v) => String(v));
87
+ const strings = values.map((v) => {
88
+ const s = String(v);
89
+ const truncLen = typeof col.truncate === "number" ? col.truncate : col.truncate && typeof col.truncate === "object" ? col.truncate.maxLength : null;
90
+ return truncLen && s.length > truncLen ? s.slice(0, truncLen) : s;
91
+ });
88
92
  let widthHint = null;
89
93
  let cellWidthHint = null;
90
94
  if (col.editable && col.editType && NARROW_EDIT_TYPES.has(col.editType)) {
91
95
  cellWidthHint = "min";
92
96
  }
97
+ if (col.truncate === true) {
98
+ cellWidthHint = cellWidthHint || "min";
99
+ }
93
100
  if (strings.length > 0) {
94
101
  const lengths = strings.map((s) => s.length);
95
102
  const maxLen = Math.max(...lengths);
@@ -155,12 +162,16 @@ var DataTable = ({
155
162
  // enable fuzzy matching via Fuse.js
156
163
  fuzzyOptions,
157
164
  // custom Fuse.js options (threshold, distance, etc.)
165
+ showSearch = true,
166
+ // show the SearchInput in the toolbar
158
167
  // Filters
159
168
  filters = [],
160
169
  showFilterBadges = true,
161
170
  // show active filter chips/badges
162
171
  showClearFiltersButton = true,
163
172
  // show "Clear all" filters reset button
173
+ filterInlineLimit = 2,
174
+ // number of filters shown inline before overflow
164
175
  // Pagination
165
176
  pageSize = 10,
166
177
  maxVisiblePageButtons,
@@ -242,6 +253,8 @@ var DataTable = ({
242
253
  // optional key to force clear uncontrolled selection memory
243
254
  resetSelectionOnQueryChange = true,
244
255
  // clear uncontrolled selection on search/filter/sort changes
256
+ showSelectionBar = true,
257
+ // show the selection action bar when rows are selected
245
258
  recordLabel,
246
259
  // { singular: "Contact", plural: "Contacts" } — defaults to Record/Records
247
260
  // -----------------------------------------------------------------------
@@ -262,11 +275,31 @@ var DataTable = ({
262
275
  // (row, field, newValue) => void
263
276
  onRowEditInput,
264
277
  // optional live-input callback: (row, field, inputValue) => void
278
+ onEditStart,
279
+ // (row, field, currentValue) => void — fires when editing begins
280
+ onEditCancel,
281
+ // (row, field) => void — fires when editing is cancelled without commit
265
282
  // -----------------------------------------------------------------------
266
283
  // Auto-width
267
284
  // -----------------------------------------------------------------------
268
- autoWidth = true
285
+ autoWidth = true,
269
286
  // auto-compute column widths from content analysis
287
+ // -----------------------------------------------------------------------
288
+ // Labels / i18n
289
+ // -----------------------------------------------------------------------
290
+ labels,
291
+ // override hardcoded UI strings for i18n
292
+ // -----------------------------------------------------------------------
293
+ // Render overrides (Phase 3 — full replacement escape hatches)
294
+ // -----------------------------------------------------------------------
295
+ renderSelectionBar,
296
+ // ({ selectedIds, selectedCount, displayCount, countLabel, onSelectAll, onDeselectAll, selectionActions }) => ReactNode
297
+ renderEmptyState,
298
+ // ({ title, message }) => ReactNode
299
+ renderLoadingState,
300
+ // ({ label }) => ReactNode
301
+ renderErrorState
302
+ // ({ error, title, message }) => ReactNode
270
303
  }) => {
271
304
  const initialSortState = useMemo(() => {
272
305
  return normalizeSortState(columns, defaultSort);
@@ -500,11 +533,11 @@ var DataTable = ({
500
533
  const type = filter.type || "select";
501
534
  const prefix = filter.chipLabel || filter.placeholder || filter.name;
502
535
  if (type === "multiselect") {
503
- const labels = value.map((v) => {
536
+ const labels2 = value.map((v) => {
504
537
  var _a;
505
538
  return ((_a = filter.options.find((o) => o.value === v)) == null ? void 0 : _a.label) || v;
506
539
  }).join(", ");
507
- chips.push({ key: filter.name, label: `${prefix}: ${labels}` });
540
+ chips.push({ key: filter.name, label: `${prefix}: ${labels2}` });
508
541
  } else if (type === "dateRange") {
509
542
  const parts = [];
510
543
  if (value.from) parts.push(`from ${formatDateChip(value.from)}`);
@@ -545,7 +578,17 @@ var DataTable = ({
545
578
  const countLabel = (n) => n === 1 ? singularLabel : pluralLabel;
546
579
  const resolvedEmptyTitle = emptyTitle || "No results found";
547
580
  const resolvedEmptyMessage = emptyMessage || `No ${pluralLabel} match your search or filter criteria.`;
548
- const resolvedLoadingLabel = `Loading ${pluralLabel}...`;
581
+ const resolvedSelectedLabel = (labels == null ? void 0 : labels.selected) || ((count, label) => `${count}\xA0${label}\xA0selected`);
582
+ const resolvedSelectAllLabel = (labels == null ? void 0 : labels.selectAll) || ((count, label) => `Select all ${count} ${label}`);
583
+ const resolvedDeselectAllLabel = (labels == null ? void 0 : labels.deselectAll) || "Deselect all";
584
+ const resolvedFiltersButtonLabel = (labels == null ? void 0 : labels.filtersButton) || "Filters";
585
+ const resolvedClearAllLabel = (labels == null ? void 0 : labels.clearAll) || "Clear all";
586
+ const resolvedDateFromLabel = (labels == null ? void 0 : labels.dateFrom) || "From";
587
+ const resolvedDateToLabel = (labels == null ? void 0 : labels.dateTo) || "To";
588
+ const resolvedLoadingLabel = (labels == null ? void 0 : labels.loading) || `Loading ${pluralLabel}...`;
589
+ const resolvedErrorTitle = (labels == null ? void 0 : labels.errorTitle) || "Something went wrong.";
590
+ const resolvedErrorMessage = (labels == null ? void 0 : labels.errorMessage) || "An error occurred while loading data.";
591
+ const resolvedRetryMessage = (labels == null ? void 0 : labels.retryMessage) || "Please try again.";
549
592
  const recordCountLabel = rowCountText ? rowCountText(shownOnPageCount, displayCount) : displayCount === totalDataCount ? `${totalDataCount} ${countLabel(totalDataCount)}` : `${displayCount} of ${totalDataCount} ${countLabel(totalDataCount)}`;
550
593
  const [internalSelectedIds, setInternalSelectedIds] = useState(/* @__PURE__ */ new Set());
551
594
  const selectionResetRef = useRef("");
@@ -634,7 +677,11 @@ var DataTable = ({
634
677
  setEditingCell({ rowId, field });
635
678
  setEditValue(currentValue);
636
679
  setEditError(null);
637
- }, []);
680
+ if (onEditStart) {
681
+ const row = data.find((r) => r[rowIdField] === rowId);
682
+ if (row) onEditStart(row, field, currentValue);
683
+ }
684
+ }, [onEditStart, data, rowIdField]);
638
685
  const commitEdit = useCallback((row, field, value) => {
639
686
  const col = columns.find((c) => c.field === field);
640
687
  if (col == null ? void 0 : col.editValidate) {
@@ -656,6 +703,7 @@ var DataTable = ({
656
703
  const commit = (val) => commitEdit(row, col.field, val);
657
704
  const exitEdit = () => {
658
705
  if (editError) return;
706
+ if (onEditCancel) onEditCancel(row, col.field);
659
707
  setEditingCell(null);
660
708
  setEditValue(null);
661
709
  };
@@ -822,20 +870,26 @@ var DataTable = ({
822
870
  const rawStr = String(rawValue ?? "");
823
871
  if (col.truncate && rawStr.length > 0) {
824
872
  if (col.truncate === true) {
825
- const content2 = col.renderCell ? col.renderCell(rawValue, row) : rawStr;
873
+ if (col.renderCell) {
874
+ const content2 = col.renderCell(rawValue, row);
875
+ if (col.editable) {
876
+ return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
877
+ }
878
+ return content2;
879
+ }
826
880
  if (col.editable) {
827
- return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--"));
881
+ return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, rawStr || "--"));
828
882
  }
829
- return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, content2);
883
+ return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, rawStr);
830
884
  }
831
- const maxLen = col.truncate.maxLength || 100;
885
+ const maxLen = typeof col.truncate === "number" ? col.truncate : col.truncate.maxLength || 100;
832
886
  if (rawStr.length > maxLen) {
833
887
  const truncatedStr = rawStr.slice(0, maxLen) + "\u2026";
834
- const truncatedContent = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
888
+ const content2 = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
835
889
  if (col.editable) {
836
- return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, truncatedContent || "--");
890
+ return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
837
891
  }
838
- return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, truncatedContent || "--");
892
+ return col.renderCell ? content2 : /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, content2 || "--");
839
893
  }
840
894
  }
841
895
  const content = col.renderCell ? col.renderCell(rawValue, row) : rawValue;
@@ -875,7 +929,7 @@ var DataTable = ({
875
929
  {
876
930
  name: `filter-${filter.name}-from`,
877
931
  label: "",
878
- placeholder: "From",
932
+ placeholder: resolvedDateFromLabel,
879
933
  format: "medium",
880
934
  value: rangeVal.from,
881
935
  onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, from: val })
@@ -886,7 +940,7 @@ var DataTable = ({
886
940
  size: "sm",
887
941
  name: `filter-${filter.name}-to`,
888
942
  label: "",
889
- placeholder: "To",
943
+ placeholder: resolvedDateToLabel,
890
944
  format: "medium",
891
945
  value: rangeVal.to,
892
946
  onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, to: val })
@@ -909,7 +963,7 @@ var DataTable = ({
909
963
  }
910
964
  );
911
965
  };
912
- return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, searchFields.length > 0 && /* @__PURE__ */ React.createElement(
966
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch && searchFields.length > 0 && /* @__PURE__ */ React.createElement(
913
967
  SearchInput,
914
968
  {
915
969
  name: "datatable-search",
@@ -917,7 +971,7 @@ var DataTable = ({
917
971
  value: searchTerm,
918
972
  onChange: handleSearchChange
919
973
  }
920
- ), filters.slice(0, 2).map(renderFilterControl), filters.length > 2 && /* @__PURE__ */ React.createElement(
974
+ ), filters.slice(0, filterInlineLimit).map(renderFilterControl), filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(
921
975
  Button,
922
976
  {
923
977
  variant: "transparent",
@@ -925,16 +979,25 @@ var DataTable = ({
925
979
  onClick: () => setShowMoreFilters((prev) => !prev)
926
980
  },
927
981
  /* @__PURE__ */ React.createElement(Icon, { name: "filter", size: "sm" }),
928
- " Filters"
929
- )), showMoreFilters && filters.length > 2 && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, filters.slice(2).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ React.createElement(Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), showClearFiltersButton && /* @__PURE__ */ React.createElement(
982
+ " ",
983
+ resolvedFiltersButtonLabel
984
+ )), showMoreFilters && filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, filters.slice(filterInlineLimit).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ React.createElement(Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), showClearFiltersButton && /* @__PURE__ */ React.createElement(
930
985
  Button,
931
986
  {
932
987
  variant: "transparent",
933
988
  size: "extra-small",
934
989
  onClick: () => handleFilterRemove("all")
935
990
  },
936
- "Clear all"
937
- )))), showRowCount && displayCount > 0 && !(selectable && selectedIds.size > 0) && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), selectable && selectedIds.size > 0 && /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(Text, { inline: true, format: { fontWeight: "demibold" } }, selectedIds.size, "\xA0", countLabel(selectedIds.size), "\xA0selected"), /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, "Select all ", displayCount, " ", countLabel(displayCount)), /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, "Deselect all"), selectionActions.map((action, i) => /* @__PURE__ */ React.createElement(
991
+ resolvedClearAllLabel
992
+ )))), showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0) && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), showSelectionBar && selectable && selectedIds.size > 0 && (renderSelectionBar ? renderSelectionBar({
993
+ selectedIds,
994
+ selectedCount: selectedIds.size,
995
+ displayCount,
996
+ countLabel,
997
+ onSelectAll: handleSelectAllRows,
998
+ onDeselectAll: handleDeselectAll,
999
+ selectionActions
1000
+ }) : /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(Text, { inline: true, format: { fontWeight: "demibold" } }, typeof resolvedSelectedLabel === "function" ? resolvedSelectedLabel(selectedIds.size, countLabel(selectedIds.size)) : resolvedSelectedLabel), /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, typeof resolvedSelectAllLabel === "function" ? resolvedSelectAllLabel(displayCount, countLabel(displayCount)) : resolvedSelectAllLabel), /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, resolvedDeselectAllLabel), selectionActions.map((action, i) => /* @__PURE__ */ React.createElement(
938
1001
  Button,
939
1002
  {
940
1003
  key: i,
@@ -945,7 +1008,11 @@ var DataTable = ({
945
1008
  action.icon && /* @__PURE__ */ React.createElement(Icon, { name: action.icon, size: "sm" }),
946
1009
  " ",
947
1010
  action.label
948
- )))), showRowCount && displayCount > 0 && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), loading ? /* @__PURE__ */ React.createElement(LoadingSpinner, { label: resolvedLoadingLabel, layout: "centered" }) : error ? /* @__PURE__ */ React.createElement(ErrorState, { title: typeof error === "string" ? error : "Something went wrong." }, /* @__PURE__ */ React.createElement(Text, null, typeof error === "string" ? "Please try again." : "An error occurred while loading data.")) : displayRows.length === 0 ? /* @__PURE__ */ React.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedEmptyMessage))) : /* @__PURE__ */ React.createElement(
1011
+ )))), showRowCount && displayCount > 0 && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel))))), loading ? renderLoadingState ? renderLoadingState({ label: resolvedLoadingLabel }) : /* @__PURE__ */ React.createElement(LoadingSpinner, { label: resolvedLoadingLabel, layout: "centered" }) : error ? renderErrorState ? renderErrorState({
1012
+ error,
1013
+ title: typeof error === "string" ? error : resolvedErrorTitle,
1014
+ message: typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage
1015
+ }) : /* @__PURE__ */ React.createElement(ErrorState, { title: typeof error === "string" ? error : resolvedErrorTitle }, /* @__PURE__ */ React.createElement(Text, null, typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage)) : displayRows.length === 0 ? renderEmptyState ? renderEmptyState({ title: resolvedEmptyTitle, message: resolvedEmptyMessage }) : /* @__PURE__ */ React.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedEmptyMessage))) : /* @__PURE__ */ React.createElement(
949
1016
  Table,
950
1017
  {
951
1018
  bordered,
@@ -1168,7 +1235,6 @@ var runDefaultFieldValidator = (value, field, allValues) => {
1168
1235
  if (!isTimeValueObject(value)) return `${errorPrefix} has an invalid time`;
1169
1236
  break;
1170
1237
  case "datetime": {
1171
- if (isDateValueObject(value)) break;
1172
1238
  if (!isPlainObject(value)) return `${errorPrefix} has an invalid date/time`;
1173
1239
  const hasDate = value.date !== void 0;
1174
1240
  const hasTime = value.time !== void 0;
@@ -1234,12 +1300,14 @@ var collectAsyncValidatorPromises = (value, field, allValues, context) => {
1234
1300
  };
1235
1301
  var runValidators = (value, field, allValues, fieldTypes, options = {}) => {
1236
1302
  const includeCustomValidators = options.includeCustomValidators !== false;
1237
- if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList") return null;
1303
+ const msg = options.messages || {};
1304
+ if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList" || field.type === "fieldGroup") return null;
1238
1305
  const isRequired = resolveRequired(field, allValues);
1239
1306
  const plugin = fieldTypes && fieldTypes[field.type];
1240
1307
  const empty = plugin && plugin.isEmpty ? plugin.isEmpty(value) : isValueEmpty(value, field);
1241
1308
  if (isRequired && empty) {
1242
- return `${field.label} is required`;
1309
+ const fn = msg.required || ((label) => `${label} is required`);
1310
+ return typeof fn === "function" ? fn(field.label) : fn;
1243
1311
  }
1244
1312
  if (empty) return null;
1245
1313
  if (field.useDefaultValidators !== false) {
@@ -1248,23 +1316,27 @@ var runValidators = (value, field, allValues, fieldTypes, options = {}) => {
1248
1316
  }
1249
1317
  if (field.pattern && typeof value === "string") {
1250
1318
  if (!field.pattern.test(value)) {
1251
- return field.patternMessage || "Invalid format";
1319
+ return field.patternMessage || msg.invalidFormat || "Invalid format";
1252
1320
  }
1253
1321
  }
1254
1322
  if (typeof value === "string") {
1255
1323
  if (field.minLength != null && value.length < field.minLength) {
1256
- return `Must be at least ${field.minLength} characters`;
1324
+ const fn = msg.minLength || ((min) => `Must be at least ${min} characters`);
1325
+ return typeof fn === "function" ? fn(field.minLength) : fn;
1257
1326
  }
1258
1327
  if (field.maxLength != null && value.length > field.maxLength) {
1259
- return `Must be no more than ${field.maxLength} characters`;
1328
+ const fn = msg.maxLength || ((max) => `Must be no more than ${max} characters`);
1329
+ return typeof fn === "function" ? fn(field.maxLength) : fn;
1260
1330
  }
1261
1331
  }
1262
1332
  if (typeof value === "number") {
1263
1333
  if (field.min != null && value < field.min) {
1264
- return `Must be at least ${field.min}`;
1334
+ const fn = msg.minValue || ((min) => `Must be at least ${min}`);
1335
+ return typeof fn === "function" ? fn(field.min) : fn;
1265
1336
  }
1266
1337
  if (field.max != null && value > field.max) {
1267
- return `Must be no more than ${field.max}`;
1338
+ const fn = msg.maxValue || ((max) => `Must be no more than ${max}`);
1339
+ return typeof fn === "function" ? fn(field.max) : fn;
1268
1340
  }
1269
1341
  }
1270
1342
  if (field.type === "date" && isDateValueObject(value)) {
@@ -1376,6 +1448,8 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1376
1448
  // (values, { reset, rawValues }) => void | Promise
1377
1449
  transformValues,
1378
1450
  // (values) => values — reshape before submit
1451
+ transformInitialValues,
1452
+ // (rawInitialValues) => values — reshape raw data on load
1379
1453
  onBeforeSubmit,
1380
1454
  // (values) => boolean | Promise<boolean> — intercept submit
1381
1455
  onSubmitSuccess,
@@ -1470,8 +1544,18 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1470
1544
  // string — warning alert when readOnly
1471
1545
  alerts,
1472
1546
  // { addAlert, readOnlyTitle, errorTitle, successTitle }
1473
- errors: controlledErrors
1547
+ errors: controlledErrors,
1474
1548
  // controlled validation errors
1549
+ showReadOnlyAlert = true,
1550
+ // show warning Alert when readOnly is true
1551
+ showInlineAlerts = true,
1552
+ // show inline form-level error/success Alerts
1553
+ renderReadOnlyAlert,
1554
+ // (context: { title, message }) => ReactNode — custom readOnly alert renderer
1555
+ renderFieldError,
1556
+ // (error: string, field: object) => ReactNode — custom field error renderer
1557
+ defaultCurrency = "USD"
1558
+ // form-level default ISO 4217 currency code for currency fields
1475
1559
  } = props;
1476
1560
  const {
1477
1561
  onDirtyChange,
@@ -1483,6 +1567,23 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1483
1567
  const cancelButtonLabel = (labels == null ? void 0 : labels.cancel) || "Cancel";
1484
1568
  const backButtonLabel = (labels == null ? void 0 : labels.back) || "Back";
1485
1569
  const nextButtonLabel = (labels == null ? void 0 : labels.next) || "Next";
1570
+ const requiredMessage = (labels == null ? void 0 : labels.required) || ((label) => `${label} is required`);
1571
+ const invalidFormatMessage = (labels == null ? void 0 : labels.invalidFormat) || "Invalid format";
1572
+ const minLengthMessage = (labels == null ? void 0 : labels.minLength) || ((min) => `Must be at least ${min} characters`);
1573
+ const maxLengthMessage = (labels == null ? void 0 : labels.maxLength) || ((max) => `Must be no more than ${max} characters`);
1574
+ const minValueMessage = (labels == null ? void 0 : labels.minValue) || ((min) => `Must be at least ${min}`);
1575
+ const maxValueMessage = (labels == null ? void 0 : labels.maxValue) || ((max) => `Must be no more than ${max}`);
1576
+ const dependentPropertiesLabel = (labels == null ? void 0 : labels.dependentProperties) || "Dependent properties";
1577
+ const repeaterAddLabel = (labels == null ? void 0 : labels.repeaterAdd) || "Add";
1578
+ const repeaterRemoveLabel = (labels == null ? void 0 : labels.repeaterRemove) || "Remove";
1579
+ const validationMessages = labels ? {
1580
+ required: requiredMessage,
1581
+ invalidFormat: invalidFormatMessage,
1582
+ minLength: minLengthMessage,
1583
+ maxLength: maxLengthMessage,
1584
+ minValue: minValueMessage,
1585
+ maxValue: maxValueMessage
1586
+ } : void 0;
1486
1587
  const addAlert = alerts == null ? void 0 : alerts.addAlert;
1487
1588
  const readOnlyTitle = (alerts == null ? void 0 : alerts.readOnlyTitle) || "Read Only";
1488
1589
  const errorTitle = (alerts == null ? void 0 : alerts.errorTitle) || "Error";
@@ -1512,12 +1613,27 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1512
1613
  prevSuccessRef.current = formSuccess;
1513
1614
  }, [addAlert, formSuccess, successTitle]);
1514
1615
  const computeInitialValues = () => {
1616
+ const resolved = transformInitialValues && initialValues ? transformInitialValues(initialValues) : initialValues;
1515
1617
  const vals = {};
1516
1618
  for (const field of fields) {
1517
1619
  if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList") continue;
1620
+ if (field.type === "fieldGroup" && field.items && field.fields) {
1621
+ for (const item of field.items) {
1622
+ const subFields = field.fields(item);
1623
+ for (const sf of subFields) {
1624
+ const plugin2 = fieldTypes && fieldTypes[sf.type];
1625
+ const emptyValue2 = plugin2 && plugin2.getEmptyValue ? plugin2.getEmptyValue() : getEmptyValue(sf);
1626
+ let init2 = resolved && resolved[sf.name] !== void 0 ? resolved[sf.name] : sf.defaultValue !== void 0 ? sf.defaultValue : emptyValue2;
1627
+ if (sf.transformIn) init2 = sf.transformIn(init2);
1628
+ vals[sf.name] = init2;
1629
+ }
1630
+ }
1631
+ continue;
1632
+ }
1518
1633
  const plugin = fieldTypes && fieldTypes[field.type];
1519
1634
  const emptyValue = plugin && plugin.getEmptyValue ? plugin.getEmptyValue() : getEmptyValue(field);
1520
- const init = initialValues && initialValues[field.name] !== void 0 ? initialValues[field.name] : field.defaultValue !== void 0 ? field.defaultValue : emptyValue;
1635
+ let init = resolved && resolved[field.name] !== void 0 ? resolved[field.name] : field.defaultValue !== void 0 ? field.defaultValue : emptyValue;
1636
+ if (field.transformIn) init = field.transformIn(init);
1521
1637
  vals[field.name] = init;
1522
1638
  }
1523
1639
  return vals;
@@ -1550,7 +1666,14 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1550
1666
  formErrorsRef.current = formErrors;
1551
1667
  const fieldByName = useMemo2(() => {
1552
1668
  const map = /* @__PURE__ */ new Map();
1553
- for (const field of fields) map.set(field.name, field);
1669
+ for (const field of fields) {
1670
+ map.set(field.name, field);
1671
+ if (field.type === "fieldGroup" && field.items && field.fields) {
1672
+ for (const item of field.items) {
1673
+ for (const sf of field.fields(item)) map.set(sf.name, sf);
1674
+ }
1675
+ }
1676
+ }
1554
1677
  return map;
1555
1678
  }, [fields]);
1556
1679
  const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
@@ -1721,7 +1844,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1721
1844
  const rowValues = { ...allValues, [field.name]: rows };
1722
1845
  subFields.forEach((subField) => {
1723
1846
  if (subField.visible && !subField.visible(rowValues)) return;
1724
- const err = runValidators(row == null ? void 0 : row[subField.name], subField, rowValues, fieldTypes);
1847
+ const err = runValidators(row == null ? void 0 : row[subField.name], subField, rowValues, fieldTypes, { messages: validationMessages });
1725
1848
  if (!err) return;
1726
1849
  const key = getRepeaterErrorKey(field.name, rowIdx, subField.name);
1727
1850
  errors[key] = err;
@@ -1748,9 +1871,9 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1748
1871
  );
1749
1872
  return repeaterResult.errors[name] || null;
1750
1873
  }
1751
- return runValidators(value != null ? value : formValues[name], field, formValues, fieldTypes);
1874
+ return runValidators(value != null ? value : formValues[name], field, formValues, fieldTypes, { messages: validationMessages });
1752
1875
  },
1753
- [fieldByName, formValues, validateRepeaterField, fieldTypes]
1876
+ [fieldByName, formValues, validateRepeaterField, fieldTypes, validationMessages]
1754
1877
  );
1755
1878
  const validateVisibleFields = useCallback2(
1756
1879
  (fieldSubset) => {
@@ -1766,7 +1889,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1766
1889
  }
1767
1890
  continue;
1768
1891
  }
1769
- const err = runValidators(formValues[field.name], field, formValues, fieldTypes);
1892
+ const err = runValidators(formValues[field.name], field, formValues, fieldTypes, { messages: validationMessages });
1770
1893
  if (err) {
1771
1894
  errors[field.name] = err;
1772
1895
  hasErrors = true;
@@ -1774,14 +1897,14 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1774
1897
  }
1775
1898
  return { errors, hasErrors };
1776
1899
  },
1777
- [visibleFields, formValues, validateRepeaterField, fieldTypes]
1900
+ [visibleFields, formValues, validateRepeaterField, fieldTypes, validationMessages]
1778
1901
  );
1779
1902
  const runAsyncValidation = useCallback2(
1780
1903
  (name, value) => {
1781
1904
  const field = fieldByName.get(name);
1782
1905
  if (!field || field.type === "repeater") return null;
1783
1906
  const val = value != null ? value : formValues[name];
1784
- const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false });
1907
+ const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false, messages: validationMessages });
1785
1908
  const prevController = asyncAbortRef.current.get(name);
1786
1909
  if (prevController) prevController.abort();
1787
1910
  asyncAbortRef.current.delete(name);
@@ -1972,23 +2095,27 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1972
2095
  );
1973
2096
  const handleFieldInput = useCallback2(
1974
2097
  (name, value) => {
2098
+ handleFieldChange(name, value);
1975
2099
  if (!validateOnChange) return;
1976
2100
  const err = validateField(name, value);
1977
2101
  updateErrors({ [name]: err });
1978
2102
  },
1979
- [validateOnChange, validateField, updateErrors]
2103
+ [validateOnChange, validateField, updateErrors, handleFieldChange]
1980
2104
  );
1981
2105
  const handleFieldBlur = useCallback2(
1982
2106
  (name, value) => {
1983
- if (!validateOnBlur) return;
1984
2107
  const resolvedValue = value != null ? value : formValuesRef.current[name];
2108
+ if (value != null && value !== formValuesRef.current[name]) {
2109
+ handleFieldChange(name, value);
2110
+ }
2111
+ if (!validateOnBlur) return;
1985
2112
  const err = validateField(name, resolvedValue);
1986
2113
  updateErrors({ [name]: err });
1987
2114
  if (!err) {
1988
2115
  triggerAsyncValidation(name, resolvedValue);
1989
2116
  }
1990
2117
  },
1991
- [validateOnBlur, validateField, updateErrors, triggerAsyncValidation]
2118
+ [validateOnBlur, validateField, updateErrors, triggerAsyncValidation, handleFieldChange]
1992
2119
  );
1993
2120
  const handleSubmit = useCallback2(
1994
2121
  async (e) => {
@@ -2021,8 +2148,17 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2021
2148
  const rawValues = {};
2022
2149
  for (const key of Object.keys(formValues)) {
2023
2150
  const f = fieldByName.get(key);
2024
- if (f && (f.type === "display" || f.type === "crmPropertyList" || f.type === "crmAssociationPropertyList")) continue;
2025
- rawValues[key] = formValues[key];
2151
+ if (f && (f.type === "display" || f.type === "crmPropertyList" || f.type === "crmAssociationPropertyList" || f.type === "fieldGroup")) continue;
2152
+ rawValues[key] = f && f.transformOut ? f.transformOut(formValues[key]) : formValues[key];
2153
+ }
2154
+ for (const f of fields) {
2155
+ if (f.type !== "fieldGroup" || !f.items || !f.fields) continue;
2156
+ for (const item of f.items) {
2157
+ for (const sf of f.fields(item)) {
2158
+ if (formValues[sf.name] === void 0) continue;
2159
+ rawValues[sf.name] = sf.transformOut ? sf.transformOut(formValues[sf.name]) : formValues[sf.name];
2160
+ }
2161
+ }
2026
2162
  }
2027
2163
  const submitValues = transformValues ? transformValues(rawValues) : rawValues;
2028
2164
  if (onBeforeSubmit) {
@@ -2150,6 +2286,12 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2150
2286
  [replaceErrors]
2151
2287
  );
2152
2288
  const renderField = (field) => {
2289
+ const fieldError = formErrors[field.name] || null;
2290
+ const rendered = renderFieldInner(field);
2291
+ if (!renderFieldError || !fieldError) return rendered;
2292
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, rendered, renderFieldError(fieldError, field));
2293
+ };
2294
+ const renderFieldInner = (field) => {
2153
2295
  const fieldValue = formValues[field.name];
2154
2296
  const fieldError = formErrors[field.name] || null;
2155
2297
  const hasError = !!fieldError;
@@ -2159,10 +2301,125 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2159
2301
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
2160
2302
  if (field.type === "display") {
2161
2303
  if (field.render) {
2162
- return field.render({ allValues: formValues });
2304
+ return field.render({
2305
+ allValues: formValues,
2306
+ setFieldValue: (name, value) => handleFieldChange(name, value),
2307
+ setFieldError: (name, message) => updateErrors({ [name]: message })
2308
+ });
2163
2309
  }
2164
2310
  return null;
2165
2311
  }
2312
+ if (field.type === "fieldGroup") {
2313
+ const items = field.items || [];
2314
+ const fieldsFn = field.fields;
2315
+ if (!fieldsFn) return null;
2316
+ const groupColumns = field.columns || 1;
2317
+ const showItemLabel = field.showItemLabel !== false;
2318
+ return /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ React2.createElement(Text2, { format: { fontWeight: "demibold" } }, field.label), field.description && /* @__PURE__ */ React2.createElement(Text2, { variant: "microcopy" }, field.description), items.map((item, itemIdx) => {
2319
+ const subFields = fieldsFn(item);
2320
+ return /* @__PURE__ */ React2.createElement(Flex2, { key: item.key || itemIdx, direction: "row", gap: "xs", align: "end" }, showItemLabel && item.label && /* @__PURE__ */ React2.createElement(Box2, { flex: 1 }, itemIdx === 0 ? /* @__PURE__ */ React2.createElement(
2321
+ Input2,
2322
+ {
2323
+ name: `_fieldGroup-label-${field.name}-${itemIdx}`,
2324
+ label: "\xA0",
2325
+ value: item.label,
2326
+ readOnly: true,
2327
+ disabled: true
2328
+ }
2329
+ ) : /* @__PURE__ */ React2.createElement(
2330
+ Input2,
2331
+ {
2332
+ name: `_fieldGroup-label-${field.name}-${itemIdx}`,
2333
+ value: item.label,
2334
+ readOnly: true,
2335
+ disabled: true
2336
+ }
2337
+ )), subFields.map((sf) => {
2338
+ const sfValue = formValues[sf.name];
2339
+ const sfError = formErrors[sf.name] || null;
2340
+ const sfLabel = itemIdx === 0 ? sf.label : void 0;
2341
+ const sfReadOnly = sf.readOnly || formReadOnly;
2342
+ const sfDisabled = disabled || sf.disabled || formReadOnly;
2343
+ const sfOnChange = sf.debounce ? (v) => handleDebouncedFieldChange(sf.name, v) : (v) => handleFieldChange(sf.name, v);
2344
+ const sfProps = {
2345
+ name: sf.name,
2346
+ label: sfLabel,
2347
+ placeholder: sf.placeholder,
2348
+ description: itemIdx === 0 ? sf.description : void 0,
2349
+ readOnly: sfReadOnly,
2350
+ disabled: sfDisabled,
2351
+ error: !!sfError,
2352
+ validationMessage: sfError || void 0,
2353
+ ...sf.fieldProps || {}
2354
+ };
2355
+ let sfElement;
2356
+ switch (sf.type) {
2357
+ case "select":
2358
+ sfElement = /* @__PURE__ */ React2.createElement(
2359
+ Select2,
2360
+ {
2361
+ ...sfProps,
2362
+ value: sfValue,
2363
+ options: resolveOptions(sf, formValues),
2364
+ onChange: sfOnChange
2365
+ }
2366
+ );
2367
+ break;
2368
+ case "number":
2369
+ sfElement = /* @__PURE__ */ React2.createElement(
2370
+ NumberInput2,
2371
+ {
2372
+ ...sfProps,
2373
+ value: sfValue,
2374
+ onChange: sfOnChange,
2375
+ onBlur: (v) => handleFieldBlur(sf.name, v)
2376
+ }
2377
+ );
2378
+ break;
2379
+ case "toggle":
2380
+ sfElement = /* @__PURE__ */ React2.createElement(
2381
+ Toggle2,
2382
+ {
2383
+ name: sf.name,
2384
+ label: sfLabel || sf.label,
2385
+ checked: !!sfValue,
2386
+ size: sf.size || "md",
2387
+ labelDisplay: sf.labelDisplay || "top",
2388
+ readonly: sfReadOnly,
2389
+ disabled: sfDisabled,
2390
+ onChange: sfOnChange,
2391
+ ...sf.fieldProps || {}
2392
+ }
2393
+ );
2394
+ break;
2395
+ case "time":
2396
+ sfElement = /* @__PURE__ */ React2.createElement(
2397
+ TimeInput2,
2398
+ {
2399
+ ...sfProps,
2400
+ value: sfValue,
2401
+ interval: sf.interval,
2402
+ onChange: sfOnChange,
2403
+ onBlur: (v) => handleFieldBlur(sf.name, v)
2404
+ }
2405
+ );
2406
+ break;
2407
+ default:
2408
+ sfElement = /* @__PURE__ */ React2.createElement(
2409
+ Input2,
2410
+ {
2411
+ ...sfProps,
2412
+ value: sfValue || "",
2413
+ onChange: sfOnChange,
2414
+ onInput: (v) => handleFieldInput(sf.name, v),
2415
+ onBlur: (v) => handleFieldBlur(sf.name, v)
2416
+ }
2417
+ );
2418
+ }
2419
+ return /* @__PURE__ */ React2.createElement(Box2, { key: sf.name, flex: 1 }, sfElement);
2420
+ }));
2421
+ }));
2422
+ }
2166
2423
  if (field.type === "crmPropertyList") {
2167
2424
  return /* @__PURE__ */ React2.createElement(
2168
2425
  CrmPropertyList,
@@ -2216,7 +2473,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2216
2473
  readOnly: isReadOnly,
2217
2474
  disabled: isDisabled,
2218
2475
  error: hasError,
2219
- validationMessage: fieldError || void 0,
2476
+ validationMessage: renderFieldError ? void 0 : fieldError || void 0,
2220
2477
  ...field.loading || validatingFields[field.name] ? { loading: true } : {},
2221
2478
  ...field.fieldProps || {}
2222
2479
  };
@@ -2286,7 +2543,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2286
2543
  CurrencyInput2,
2287
2544
  {
2288
2545
  ...commonProps,
2289
- currency: field.currency || "USD",
2546
+ currency: field.currency || defaultCurrency,
2290
2547
  value: fieldValue,
2291
2548
  min: field.min,
2292
2549
  max: field.max,
@@ -2464,8 +2721,8 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2464
2721
  const renderRemoveControl = repeaterProps.renderRemove;
2465
2722
  const renderMoveUpControl = repeaterProps.renderMoveUp;
2466
2723
  const renderMoveDownControl = repeaterProps.renderMoveDown;
2467
- const addLabel = repeaterProps.addLabel || "Add";
2468
- const removeLabel = repeaterProps.removeLabel || "Remove";
2724
+ const addLabel = repeaterProps.addLabel || repeaterAddLabel;
2725
+ const removeLabel = repeaterProps.removeLabel || repeaterRemoveLabel;
2469
2726
  const moveUpLabel = repeaterProps.moveUpLabel || "Up";
2470
2727
  const moveDownLabel = repeaterProps.moveDownLabel || "Down";
2471
2728
  const canEditRows = !isReadOnly && !isDisabled;
@@ -2499,7 +2756,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2499
2756
  };
2500
2757
  const validateSubField = (rowIdx, subField, subValue, nextRows) => {
2501
2758
  const rowValues = { ...formValues, [field.name]: nextRows };
2502
- const err = runValidators(subValue, subField, rowValues, fieldTypes);
2759
+ const err = runValidators(subValue, subField, rowValues, fieldTypes, { messages: validationMessages });
2503
2760
  setRepeaterSubFieldError(field.name, rowIdx, subField.name, err);
2504
2761
  };
2505
2762
  const handleSubFieldChange = (rowIdx, subField, subValue) => {
@@ -2617,7 +2874,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2617
2874
  const renderDependentGroup = (parentField, dependents) => {
2618
2875
  const firstWithLabel = dependents.find((f) => getDependsOnLabel(f)) || dependents[0];
2619
2876
  const firstWithMessage = dependents.find((f) => getDependsOnMessage(f)) || dependents[0];
2620
- const groupLabel = getDependsOnLabel(firstWithLabel) || "Dependent properties";
2877
+ const groupLabel = getDependsOnLabel(firstWithLabel) || dependentPropertiesLabel;
2621
2878
  const rawMessage = getDependsOnMessage(firstWithMessage);
2622
2879
  const tooltipMessage = typeof rawMessage === "function" ? rawMessage(parentField.label) : rawMessage || "";
2623
2880
  return /* @__PURE__ */ React2.createElement(Tile, { key: `dep-${parentField.name}`, compact: true }, /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap }, /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React2.createElement(Text2, { format: { fontWeight: "demibold" } }, groupLabel, " ", tooltipMessage && /* @__PURE__ */ React2.createElement(Link2, { inline: true, variant: "dark", overlay: /* @__PURE__ */ React2.createElement(Tooltip, null, tooltipMessage) }, /* @__PURE__ */ React2.createElement(Icon2, { name: "info" })))), renderFieldSubset(dependents)));
@@ -2707,38 +2964,19 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2707
2964
  }
2708
2965
  return elements;
2709
2966
  };
2710
- const renderLegacyLayout = (fieldSubset) => {
2967
+ const renderSingleColumnLayout = (fieldSubset) => {
2711
2968
  const fieldList = fieldSubset || visibleFields;
2712
- const rows = [];
2713
- let i = 0;
2714
- while (i < fieldList.length) {
2715
- const field = fieldList[i];
2716
- if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !getDependsOnName(field)) {
2717
- rows.push({ type: "pair", fields: [fieldList[i], fieldList[i + 1]] });
2718
- i += 2;
2719
- } else {
2720
- rows.push({ type: "single", field });
2721
- i++;
2722
- }
2723
- }
2724
2969
  const elements = [];
2725
2970
  const processedDeps = /* @__PURE__ */ new Set();
2726
- for (const row of rows) {
2727
- if (row.type === "pair") {
2728
- elements.push(
2729
- /* @__PURE__ */ React2.createElement(Flex2, { key: `pair-${row.fields[0].name}`, direction: "row", gap: "sm" }, /* @__PURE__ */ React2.createElement(Box2, { flex: 1 }, renderField(row.fields[0])), /* @__PURE__ */ React2.createElement(Box2, { flex: 1 }, renderField(row.fields[1])))
2730
- );
2731
- } else {
2732
- const field = row.field;
2733
- if (processedDeps.has(field.name)) continue;
2734
- elements.push(
2735
- /* @__PURE__ */ React2.createElement(React2.Fragment, { key: field.name }, renderField(field))
2736
- );
2737
- const dependents = getDependents(field);
2738
- if (dependents.length > 0) {
2739
- for (const dep of dependents) processedDeps.add(dep.name);
2740
- elements.push(renderDependentGroup(field, dependents));
2741
- }
2971
+ for (const field of fieldList) {
2972
+ if (processedDeps.has(field.name)) continue;
2973
+ elements.push(
2974
+ /* @__PURE__ */ React2.createElement(React2.Fragment, { key: field.name }, renderField(field))
2975
+ );
2976
+ const dependents = getDependents(field);
2977
+ if (dependents.length > 0) {
2978
+ for (const dep of dependents) processedDeps.add(dep.name);
2979
+ elements.push(renderDependentGroup(field, dependents));
2742
2980
  }
2743
2981
  }
2744
2982
  return elements;
@@ -2749,10 +2987,20 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2749
2987
  let batch = [];
2750
2988
  const flushBatch = () => {
2751
2989
  if (batch.length === 0) return;
2752
- const chunks = maxColumns ? Array.from({ length: Math.ceil(batch.length / maxColumns) }, (_, i) => batch.slice(i * maxColumns, i * maxColumns + maxColumns)) : [batch];
2753
- for (const chunk of chunks) {
2990
+ if (maxColumns) {
2991
+ const chunks = Array.from(
2992
+ { length: Math.ceil(batch.length / maxColumns) },
2993
+ (_, i) => batch.slice(i * maxColumns, i * maxColumns + maxColumns)
2994
+ );
2995
+ for (const chunk of chunks) {
2996
+ const remainder = maxColumns - chunk.length;
2997
+ elements.push(
2998
+ /* @__PURE__ */ React2.createElement(Flex2, { key: `ag-${chunk[0].name}`, direction: "row", gap }, chunk.map((f) => /* @__PURE__ */ React2.createElement(Box2, { key: f.name, flex: 1 }, renderField(f))), remainder > 0 && /* @__PURE__ */ React2.createElement(Box2, { flex: remainder }))
2999
+ );
3000
+ }
3001
+ } else {
2754
3002
  elements.push(
2755
- /* @__PURE__ */ React2.createElement(AutoGrid, { key: `ag-${chunk[0].name}`, columnWidth, flexible: true, gap }, chunk.map((f) => /* @__PURE__ */ React2.createElement(React2.Fragment, { key: f.name }, renderField(f))))
3003
+ /* @__PURE__ */ React2.createElement(AutoGrid, { key: `ag-${batch[0].name}`, columnWidth, flexible: true, gap }, batch.map((f) => /* @__PURE__ */ React2.createElement(React2.Fragment, { key: f.name }, renderField(f))))
2756
3004
  );
2757
3005
  }
2758
3006
  batch = [];
@@ -2811,7 +3059,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2811
3059
  if (layout && fieldSubset === visibleFields) return renderExplicitLayout();
2812
3060
  if (columnWidth) return renderAutoGridLayout(fieldSubset);
2813
3061
  if (columns > 1) return renderGridLayout(fieldSubset);
2814
- return renderLegacyLayout(fieldSubset);
3062
+ return renderSingleColumnLayout(fieldSubset);
2815
3063
  };
2816
3064
  const renderSections = () => {
2817
3065
  const hasSections = Array.isArray(sections) && sections.length > 0;
@@ -2824,7 +3072,8 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2824
3072
  for (const sec of sections) {
2825
3073
  const sectionFields = sec.fields ? visibleFields.filter((f) => sec.fields.includes(f.name)) : [];
2826
3074
  if (sectionFields.length === 0) continue;
2827
- const accordionContent = /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap }, renderFieldSubset(sectionFields));
3075
+ const sectionContext = { values: formValues, errors: formErrors };
3076
+ const accordionContent = /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap }, sec.renderBefore && sec.renderBefore(sectionContext), renderFieldSubset(sectionFields), sec.renderAfter && sec.renderAfter(sectionContext));
2828
3077
  const accordion = /* @__PURE__ */ React2.createElement(
2829
3078
  Accordion,
2830
3079
  {
@@ -2917,7 +3166,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2917
3166
  currentStep,
2918
3167
  stepNames: steps.map((s) => s.title)
2919
3168
  }
2920
- ), formReadOnly && readOnlyMessage && /* @__PURE__ */ React2.createElement(Alert, { title: readOnlyTitle, variant: "warning" }, readOnlyMessage), !addAlert && formError && /* @__PURE__ */ React2.createElement(Alert, { title: errorTitle, variant: "danger" }, typeof formError === "string" ? formError : void 0), !addAlert && formSuccess && /* @__PURE__ */ React2.createElement(Alert, { title: successTitle, variant: "success" }, formSuccess), isMultiStep && steps[currentStep] && steps[currentStep].render ? steps[currentStep].render({
3169
+ ), showReadOnlyAlert && formReadOnly && readOnlyMessage && (renderReadOnlyAlert ? renderReadOnlyAlert({ title: readOnlyTitle, message: readOnlyMessage }) : /* @__PURE__ */ React2.createElement(Alert, { title: readOnlyTitle, variant: "warning" }, readOnlyMessage)), showInlineAlerts && !addAlert && formError && /* @__PURE__ */ React2.createElement(Alert, { title: errorTitle, variant: "danger" }, typeof formError === "string" ? formError : void 0), showInlineAlerts && !addAlert && formSuccess && /* @__PURE__ */ React2.createElement(Alert, { title: successTitle, variant: "success" }, formSuccess), isMultiStep && steps[currentStep] && steps[currentStep].render ? steps[currentStep].render({
2921
3170
  values: formValues,
2922
3171
  goNext: handleNext,
2923
3172
  goBack: handleBack,