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.js CHANGED
@@ -91,12 +91,19 @@ var computeAutoWidths = (columns, data) => {
91
91
  columns.forEach((col) => {
92
92
  if (col.width && col.cellWidth) return;
93
93
  const values = sample.map((row) => row[col.field]).filter((v) => v != null);
94
- const strings = values.map((v) => String(v));
94
+ const strings = values.map((v) => {
95
+ const s = String(v);
96
+ const truncLen = typeof col.truncate === "number" ? col.truncate : col.truncate && typeof col.truncate === "object" ? col.truncate.maxLength : null;
97
+ return truncLen && s.length > truncLen ? s.slice(0, truncLen) : s;
98
+ });
95
99
  let widthHint = null;
96
100
  let cellWidthHint = null;
97
101
  if (col.editable && col.editType && NARROW_EDIT_TYPES.has(col.editType)) {
98
102
  cellWidthHint = "min";
99
103
  }
104
+ if (col.truncate === true) {
105
+ cellWidthHint = cellWidthHint || "min";
106
+ }
100
107
  if (strings.length > 0) {
101
108
  const lengths = strings.map((s) => s.length);
102
109
  const maxLen = Math.max(...lengths);
@@ -162,12 +169,16 @@ var DataTable = ({
162
169
  // enable fuzzy matching via Fuse.js
163
170
  fuzzyOptions,
164
171
  // custom Fuse.js options (threshold, distance, etc.)
172
+ showSearch = true,
173
+ // show the SearchInput in the toolbar
165
174
  // Filters
166
175
  filters = [],
167
176
  showFilterBadges = true,
168
177
  // show active filter chips/badges
169
178
  showClearFiltersButton = true,
170
179
  // show "Clear all" filters reset button
180
+ filterInlineLimit = 2,
181
+ // number of filters shown inline before overflow
171
182
  // Pagination
172
183
  pageSize = 10,
173
184
  maxVisiblePageButtons,
@@ -249,6 +260,8 @@ var DataTable = ({
249
260
  // optional key to force clear uncontrolled selection memory
250
261
  resetSelectionOnQueryChange = true,
251
262
  // clear uncontrolled selection on search/filter/sort changes
263
+ showSelectionBar = true,
264
+ // show the selection action bar when rows are selected
252
265
  recordLabel,
253
266
  // { singular: "Contact", plural: "Contacts" } — defaults to Record/Records
254
267
  // -----------------------------------------------------------------------
@@ -269,11 +282,31 @@ var DataTable = ({
269
282
  // (row, field, newValue) => void
270
283
  onRowEditInput,
271
284
  // optional live-input callback: (row, field, inputValue) => void
285
+ onEditStart,
286
+ // (row, field, currentValue) => void — fires when editing begins
287
+ onEditCancel,
288
+ // (row, field) => void — fires when editing is cancelled without commit
272
289
  // -----------------------------------------------------------------------
273
290
  // Auto-width
274
291
  // -----------------------------------------------------------------------
275
- autoWidth = true
292
+ autoWidth = true,
276
293
  // auto-compute column widths from content analysis
294
+ // -----------------------------------------------------------------------
295
+ // Labels / i18n
296
+ // -----------------------------------------------------------------------
297
+ labels,
298
+ // override hardcoded UI strings for i18n
299
+ // -----------------------------------------------------------------------
300
+ // Render overrides (Phase 3 — full replacement escape hatches)
301
+ // -----------------------------------------------------------------------
302
+ renderSelectionBar,
303
+ // ({ selectedIds, selectedCount, displayCount, countLabel, onSelectAll, onDeselectAll, selectionActions }) => ReactNode
304
+ renderEmptyState,
305
+ // ({ title, message }) => ReactNode
306
+ renderLoadingState,
307
+ // ({ label }) => ReactNode
308
+ renderErrorState
309
+ // ({ error, title, message }) => ReactNode
277
310
  }) => {
278
311
  const initialSortState = (0, import_react.useMemo)(() => {
279
312
  return normalizeSortState(columns, defaultSort);
@@ -507,11 +540,11 @@ var DataTable = ({
507
540
  const type = filter.type || "select";
508
541
  const prefix = filter.chipLabel || filter.placeholder || filter.name;
509
542
  if (type === "multiselect") {
510
- const labels = value.map((v) => {
543
+ const labels2 = value.map((v) => {
511
544
  var _a;
512
545
  return ((_a = filter.options.find((o) => o.value === v)) == null ? void 0 : _a.label) || v;
513
546
  }).join(", ");
514
- chips.push({ key: filter.name, label: `${prefix}: ${labels}` });
547
+ chips.push({ key: filter.name, label: `${prefix}: ${labels2}` });
515
548
  } else if (type === "dateRange") {
516
549
  const parts = [];
517
550
  if (value.from) parts.push(`from ${formatDateChip(value.from)}`);
@@ -552,7 +585,17 @@ var DataTable = ({
552
585
  const countLabel = (n) => n === 1 ? singularLabel : pluralLabel;
553
586
  const resolvedEmptyTitle = emptyTitle || "No results found";
554
587
  const resolvedEmptyMessage = emptyMessage || `No ${pluralLabel} match your search or filter criteria.`;
555
- const resolvedLoadingLabel = `Loading ${pluralLabel}...`;
588
+ const resolvedSelectedLabel = (labels == null ? void 0 : labels.selected) || ((count, label) => `${count}\xA0${label}\xA0selected`);
589
+ const resolvedSelectAllLabel = (labels == null ? void 0 : labels.selectAll) || ((count, label) => `Select all ${count} ${label}`);
590
+ const resolvedDeselectAllLabel = (labels == null ? void 0 : labels.deselectAll) || "Deselect all";
591
+ const resolvedFiltersButtonLabel = (labels == null ? void 0 : labels.filtersButton) || "Filters";
592
+ const resolvedClearAllLabel = (labels == null ? void 0 : labels.clearAll) || "Clear all";
593
+ const resolvedDateFromLabel = (labels == null ? void 0 : labels.dateFrom) || "From";
594
+ const resolvedDateToLabel = (labels == null ? void 0 : labels.dateTo) || "To";
595
+ const resolvedLoadingLabel = (labels == null ? void 0 : labels.loading) || `Loading ${pluralLabel}...`;
596
+ const resolvedErrorTitle = (labels == null ? void 0 : labels.errorTitle) || "Something went wrong.";
597
+ const resolvedErrorMessage = (labels == null ? void 0 : labels.errorMessage) || "An error occurred while loading data.";
598
+ const resolvedRetryMessage = (labels == null ? void 0 : labels.retryMessage) || "Please try again.";
556
599
  const recordCountLabel = rowCountText ? rowCountText(shownOnPageCount, displayCount) : displayCount === totalDataCount ? `${totalDataCount} ${countLabel(totalDataCount)}` : `${displayCount} of ${totalDataCount} ${countLabel(totalDataCount)}`;
557
600
  const [internalSelectedIds, setInternalSelectedIds] = (0, import_react.useState)(/* @__PURE__ */ new Set());
558
601
  const selectionResetRef = (0, import_react.useRef)("");
@@ -641,7 +684,11 @@ var DataTable = ({
641
684
  setEditingCell({ rowId, field });
642
685
  setEditValue(currentValue);
643
686
  setEditError(null);
644
- }, []);
687
+ if (onEditStart) {
688
+ const row = data.find((r) => r[rowIdField] === rowId);
689
+ if (row) onEditStart(row, field, currentValue);
690
+ }
691
+ }, [onEditStart, data, rowIdField]);
645
692
  const commitEdit = (0, import_react.useCallback)((row, field, value) => {
646
693
  const col = columns.find((c) => c.field === field);
647
694
  if (col == null ? void 0 : col.editValidate) {
@@ -663,6 +710,7 @@ var DataTable = ({
663
710
  const commit = (val) => commitEdit(row, col.field, val);
664
711
  const exitEdit = () => {
665
712
  if (editError) return;
713
+ if (onEditCancel) onEditCancel(row, col.field);
666
714
  setEditingCell(null);
667
715
  setEditValue(null);
668
716
  };
@@ -829,20 +877,26 @@ var DataTable = ({
829
877
  const rawStr = String(rawValue ?? "");
830
878
  if (col.truncate && rawStr.length > 0) {
831
879
  if (col.truncate === true) {
832
- const content2 = col.renderCell ? col.renderCell(rawValue, row) : rawStr;
880
+ if (col.renderCell) {
881
+ const content2 = col.renderCell(rawValue, row);
882
+ if (col.editable) {
883
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
884
+ }
885
+ return content2;
886
+ }
833
887
  if (col.editable) {
834
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--"));
888
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, rawStr || "--"));
835
889
  }
836
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, content2);
890
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, rawStr);
837
891
  }
838
- const maxLen = col.truncate.maxLength || 100;
892
+ const maxLen = typeof col.truncate === "number" ? col.truncate : col.truncate.maxLength || 100;
839
893
  if (rawStr.length > maxLen) {
840
894
  const truncatedStr = rawStr.slice(0, maxLen) + "\u2026";
841
- const truncatedContent = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
895
+ const content2 = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
842
896
  if (col.editable) {
843
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, truncatedContent || "--");
897
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
844
898
  }
845
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, truncatedContent || "--");
899
+ return col.renderCell ? content2 : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, content2 || "--");
846
900
  }
847
901
  }
848
902
  const content = col.renderCell ? col.renderCell(rawValue, row) : rawValue;
@@ -882,7 +936,7 @@ var DataTable = ({
882
936
  {
883
937
  name: `filter-${filter.name}-from`,
884
938
  label: "",
885
- placeholder: "From",
939
+ placeholder: resolvedDateFromLabel,
886
940
  format: "medium",
887
941
  value: rangeVal.from,
888
942
  onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, from: val })
@@ -893,7 +947,7 @@ var DataTable = ({
893
947
  size: "sm",
894
948
  name: `filter-${filter.name}-to`,
895
949
  label: "",
896
- placeholder: "To",
950
+ placeholder: resolvedDateToLabel,
897
951
  format: "medium",
898
952
  value: rangeVal.to,
899
953
  onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, to: val })
@@ -916,7 +970,7 @@ var DataTable = ({
916
970
  }
917
971
  );
918
972
  };
919
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, searchFields.length > 0 && /* @__PURE__ */ import_react.default.createElement(
973
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch && searchFields.length > 0 && /* @__PURE__ */ import_react.default.createElement(
920
974
  import_ui_extensions.SearchInput,
921
975
  {
922
976
  name: "datatable-search",
@@ -924,7 +978,7 @@ var DataTable = ({
924
978
  value: searchTerm,
925
979
  onChange: handleSearchChange
926
980
  }
927
- ), filters.slice(0, 2).map(renderFilterControl), filters.length > 2 && /* @__PURE__ */ import_react.default.createElement(
981
+ ), filters.slice(0, filterInlineLimit).map(renderFilterControl), filters.length > filterInlineLimit && /* @__PURE__ */ import_react.default.createElement(
928
982
  import_ui_extensions.Button,
929
983
  {
930
984
  variant: "transparent",
@@ -932,16 +986,25 @@ var DataTable = ({
932
986
  onClick: () => setShowMoreFilters((prev) => !prev)
933
987
  },
934
988
  /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "filter", size: "sm" }),
935
- " Filters"
936
- )), showMoreFilters && filters.length > 2 && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, filters.slice(2).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), showClearFiltersButton && /* @__PURE__ */ import_react.default.createElement(
989
+ " ",
990
+ resolvedFiltersButtonLabel
991
+ )), showMoreFilters && filters.length > filterInlineLimit && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, filters.slice(filterInlineLimit).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), showClearFiltersButton && /* @__PURE__ */ import_react.default.createElement(
937
992
  import_ui_extensions.Button,
938
993
  {
939
994
  variant: "transparent",
940
995
  size: "extra-small",
941
996
  onClick: () => handleFilterRemove("all")
942
997
  },
943
- "Clear all"
944
- )))), showRowCount && displayCount > 0 && !(selectable && selectedIds.size > 0) && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), selectable && selectedIds.size > 0 && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { inline: true, format: { fontWeight: "demibold" } }, selectedIds.size, "\xA0", countLabel(selectedIds.size), "\xA0selected"), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, "Select all ", displayCount, " ", countLabel(displayCount)), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, "Deselect all"), selectionActions.map((action, i) => /* @__PURE__ */ import_react.default.createElement(
998
+ resolvedClearAllLabel
999
+ )))), showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0) && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), showSelectionBar && selectable && selectedIds.size > 0 && (renderSelectionBar ? renderSelectionBar({
1000
+ selectedIds,
1001
+ selectedCount: selectedIds.size,
1002
+ displayCount,
1003
+ countLabel,
1004
+ onSelectAll: handleSelectAllRows,
1005
+ onDeselectAll: handleDeselectAll,
1006
+ selectionActions
1007
+ }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { inline: true, format: { fontWeight: "demibold" } }, typeof resolvedSelectedLabel === "function" ? resolvedSelectedLabel(selectedIds.size, countLabel(selectedIds.size)) : resolvedSelectedLabel), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, typeof resolvedSelectAllLabel === "function" ? resolvedSelectAllLabel(displayCount, countLabel(displayCount)) : resolvedSelectAllLabel), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, resolvedDeselectAllLabel), selectionActions.map((action, i) => /* @__PURE__ */ import_react.default.createElement(
945
1008
  import_ui_extensions.Button,
946
1009
  {
947
1010
  key: i,
@@ -952,7 +1015,11 @@ var DataTable = ({
952
1015
  action.icon && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: action.icon, size: "sm" }),
953
1016
  " ",
954
1017
  action.label
955
- )))), showRowCount && displayCount > 0 && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), loading ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.LoadingSpinner, { label: resolvedLoadingLabel, layout: "centered" }) : error ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.ErrorState, { title: typeof error === "string" ? error : "Something went wrong." }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, typeof error === "string" ? "Please try again." : "An error occurred while loading data.")) : displayRows.length === 0 ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, resolvedEmptyMessage))) : /* @__PURE__ */ import_react.default.createElement(
1018
+ )))), showRowCount && displayCount > 0 && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel))))), loading ? renderLoadingState ? renderLoadingState({ label: resolvedLoadingLabel }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.LoadingSpinner, { label: resolvedLoadingLabel, layout: "centered" }) : error ? renderErrorState ? renderErrorState({
1019
+ error,
1020
+ title: typeof error === "string" ? error : resolvedErrorTitle,
1021
+ message: typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage
1022
+ }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.ErrorState, { title: typeof error === "string" ? error : resolvedErrorTitle }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage)) : displayRows.length === 0 ? renderEmptyState ? renderEmptyState({ title: resolvedEmptyTitle, message: resolvedEmptyMessage }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, resolvedEmptyMessage))) : /* @__PURE__ */ import_react.default.createElement(
956
1023
  import_ui_extensions.Table,
957
1024
  {
958
1025
  bordered,
@@ -1135,7 +1202,6 @@ var runDefaultFieldValidator = (value, field, allValues) => {
1135
1202
  if (!isTimeValueObject(value)) return `${errorPrefix} has an invalid time`;
1136
1203
  break;
1137
1204
  case "datetime": {
1138
- if (isDateValueObject(value)) break;
1139
1205
  if (!isPlainObject(value)) return `${errorPrefix} has an invalid date/time`;
1140
1206
  const hasDate = value.date !== void 0;
1141
1207
  const hasTime = value.time !== void 0;
@@ -1201,12 +1267,14 @@ var collectAsyncValidatorPromises = (value, field, allValues, context) => {
1201
1267
  };
1202
1268
  var runValidators = (value, field, allValues, fieldTypes, options = {}) => {
1203
1269
  const includeCustomValidators = options.includeCustomValidators !== false;
1204
- if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList") return null;
1270
+ const msg = options.messages || {};
1271
+ if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList" || field.type === "fieldGroup") return null;
1205
1272
  const isRequired = resolveRequired(field, allValues);
1206
1273
  const plugin = fieldTypes && fieldTypes[field.type];
1207
1274
  const empty = plugin && plugin.isEmpty ? plugin.isEmpty(value) : isValueEmpty(value, field);
1208
1275
  if (isRequired && empty) {
1209
- return `${field.label} is required`;
1276
+ const fn = msg.required || ((label) => `${label} is required`);
1277
+ return typeof fn === "function" ? fn(field.label) : fn;
1210
1278
  }
1211
1279
  if (empty) return null;
1212
1280
  if (field.useDefaultValidators !== false) {
@@ -1215,23 +1283,27 @@ var runValidators = (value, field, allValues, fieldTypes, options = {}) => {
1215
1283
  }
1216
1284
  if (field.pattern && typeof value === "string") {
1217
1285
  if (!field.pattern.test(value)) {
1218
- return field.patternMessage || "Invalid format";
1286
+ return field.patternMessage || msg.invalidFormat || "Invalid format";
1219
1287
  }
1220
1288
  }
1221
1289
  if (typeof value === "string") {
1222
1290
  if (field.minLength != null && value.length < field.minLength) {
1223
- return `Must be at least ${field.minLength} characters`;
1291
+ const fn = msg.minLength || ((min) => `Must be at least ${min} characters`);
1292
+ return typeof fn === "function" ? fn(field.minLength) : fn;
1224
1293
  }
1225
1294
  if (field.maxLength != null && value.length > field.maxLength) {
1226
- return `Must be no more than ${field.maxLength} characters`;
1295
+ const fn = msg.maxLength || ((max) => `Must be no more than ${max} characters`);
1296
+ return typeof fn === "function" ? fn(field.maxLength) : fn;
1227
1297
  }
1228
1298
  }
1229
1299
  if (typeof value === "number") {
1230
1300
  if (field.min != null && value < field.min) {
1231
- return `Must be at least ${field.min}`;
1301
+ const fn = msg.minValue || ((min) => `Must be at least ${min}`);
1302
+ return typeof fn === "function" ? fn(field.min) : fn;
1232
1303
  }
1233
1304
  if (field.max != null && value > field.max) {
1234
- return `Must be no more than ${field.max}`;
1305
+ const fn = msg.maxValue || ((max) => `Must be no more than ${max}`);
1306
+ return typeof fn === "function" ? fn(field.max) : fn;
1235
1307
  }
1236
1308
  }
1237
1309
  if (field.type === "date" && isDateValueObject(value)) {
@@ -1343,6 +1415,8 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1343
1415
  // (values, { reset, rawValues }) => void | Promise
1344
1416
  transformValues,
1345
1417
  // (values) => values — reshape before submit
1418
+ transformInitialValues,
1419
+ // (rawInitialValues) => values — reshape raw data on load
1346
1420
  onBeforeSubmit,
1347
1421
  // (values) => boolean | Promise<boolean> — intercept submit
1348
1422
  onSubmitSuccess,
@@ -1437,8 +1511,18 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1437
1511
  // string — warning alert when readOnly
1438
1512
  alerts,
1439
1513
  // { addAlert, readOnlyTitle, errorTitle, successTitle }
1440
- errors: controlledErrors
1514
+ errors: controlledErrors,
1441
1515
  // controlled validation errors
1516
+ showReadOnlyAlert = true,
1517
+ // show warning Alert when readOnly is true
1518
+ showInlineAlerts = true,
1519
+ // show inline form-level error/success Alerts
1520
+ renderReadOnlyAlert,
1521
+ // (context: { title, message }) => ReactNode — custom readOnly alert renderer
1522
+ renderFieldError,
1523
+ // (error: string, field: object) => ReactNode — custom field error renderer
1524
+ defaultCurrency = "USD"
1525
+ // form-level default ISO 4217 currency code for currency fields
1442
1526
  } = props;
1443
1527
  const {
1444
1528
  onDirtyChange,
@@ -1450,6 +1534,23 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1450
1534
  const cancelButtonLabel = (labels == null ? void 0 : labels.cancel) || "Cancel";
1451
1535
  const backButtonLabel = (labels == null ? void 0 : labels.back) || "Back";
1452
1536
  const nextButtonLabel = (labels == null ? void 0 : labels.next) || "Next";
1537
+ const requiredMessage = (labels == null ? void 0 : labels.required) || ((label) => `${label} is required`);
1538
+ const invalidFormatMessage = (labels == null ? void 0 : labels.invalidFormat) || "Invalid format";
1539
+ const minLengthMessage = (labels == null ? void 0 : labels.minLength) || ((min) => `Must be at least ${min} characters`);
1540
+ const maxLengthMessage = (labels == null ? void 0 : labels.maxLength) || ((max) => `Must be no more than ${max} characters`);
1541
+ const minValueMessage = (labels == null ? void 0 : labels.minValue) || ((min) => `Must be at least ${min}`);
1542
+ const maxValueMessage = (labels == null ? void 0 : labels.maxValue) || ((max) => `Must be no more than ${max}`);
1543
+ const dependentPropertiesLabel = (labels == null ? void 0 : labels.dependentProperties) || "Dependent properties";
1544
+ const repeaterAddLabel = (labels == null ? void 0 : labels.repeaterAdd) || "Add";
1545
+ const repeaterRemoveLabel = (labels == null ? void 0 : labels.repeaterRemove) || "Remove";
1546
+ const validationMessages = labels ? {
1547
+ required: requiredMessage,
1548
+ invalidFormat: invalidFormatMessage,
1549
+ minLength: minLengthMessage,
1550
+ maxLength: maxLengthMessage,
1551
+ minValue: minValueMessage,
1552
+ maxValue: maxValueMessage
1553
+ } : void 0;
1453
1554
  const addAlert = alerts == null ? void 0 : alerts.addAlert;
1454
1555
  const readOnlyTitle = (alerts == null ? void 0 : alerts.readOnlyTitle) || "Read Only";
1455
1556
  const errorTitle = (alerts == null ? void 0 : alerts.errorTitle) || "Error";
@@ -1479,12 +1580,27 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1479
1580
  prevSuccessRef.current = formSuccess;
1480
1581
  }, [addAlert, formSuccess, successTitle]);
1481
1582
  const computeInitialValues = () => {
1583
+ const resolved = transformInitialValues && initialValues ? transformInitialValues(initialValues) : initialValues;
1482
1584
  const vals = {};
1483
1585
  for (const field of fields) {
1484
1586
  if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList") continue;
1587
+ if (field.type === "fieldGroup" && field.items && field.fields) {
1588
+ for (const item of field.items) {
1589
+ const subFields = field.fields(item);
1590
+ for (const sf of subFields) {
1591
+ const plugin2 = fieldTypes && fieldTypes[sf.type];
1592
+ const emptyValue2 = plugin2 && plugin2.getEmptyValue ? plugin2.getEmptyValue() : getEmptyValue(sf);
1593
+ let init2 = resolved && resolved[sf.name] !== void 0 ? resolved[sf.name] : sf.defaultValue !== void 0 ? sf.defaultValue : emptyValue2;
1594
+ if (sf.transformIn) init2 = sf.transformIn(init2);
1595
+ vals[sf.name] = init2;
1596
+ }
1597
+ }
1598
+ continue;
1599
+ }
1485
1600
  const plugin = fieldTypes && fieldTypes[field.type];
1486
1601
  const emptyValue = plugin && plugin.getEmptyValue ? plugin.getEmptyValue() : getEmptyValue(field);
1487
- const init = initialValues && initialValues[field.name] !== void 0 ? initialValues[field.name] : field.defaultValue !== void 0 ? field.defaultValue : emptyValue;
1602
+ let init = resolved && resolved[field.name] !== void 0 ? resolved[field.name] : field.defaultValue !== void 0 ? field.defaultValue : emptyValue;
1603
+ if (field.transformIn) init = field.transformIn(init);
1488
1604
  vals[field.name] = init;
1489
1605
  }
1490
1606
  return vals;
@@ -1517,7 +1633,14 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1517
1633
  formErrorsRef.current = formErrors;
1518
1634
  const fieldByName = (0, import_react2.useMemo)(() => {
1519
1635
  const map = /* @__PURE__ */ new Map();
1520
- for (const field of fields) map.set(field.name, field);
1636
+ for (const field of fields) {
1637
+ map.set(field.name, field);
1638
+ if (field.type === "fieldGroup" && field.items && field.fields) {
1639
+ for (const item of field.items) {
1640
+ for (const sf of field.fields(item)) map.set(sf.name, sf);
1641
+ }
1642
+ }
1643
+ }
1521
1644
  return map;
1522
1645
  }, [fields]);
1523
1646
  const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
@@ -1688,7 +1811,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1688
1811
  const rowValues = { ...allValues, [field.name]: rows };
1689
1812
  subFields.forEach((subField) => {
1690
1813
  if (subField.visible && !subField.visible(rowValues)) return;
1691
- const err = runValidators(row == null ? void 0 : row[subField.name], subField, rowValues, fieldTypes);
1814
+ const err = runValidators(row == null ? void 0 : row[subField.name], subField, rowValues, fieldTypes, { messages: validationMessages });
1692
1815
  if (!err) return;
1693
1816
  const key = getRepeaterErrorKey(field.name, rowIdx, subField.name);
1694
1817
  errors[key] = err;
@@ -1715,9 +1838,9 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1715
1838
  );
1716
1839
  return repeaterResult.errors[name] || null;
1717
1840
  }
1718
- return runValidators(value != null ? value : formValues[name], field, formValues, fieldTypes);
1841
+ return runValidators(value != null ? value : formValues[name], field, formValues, fieldTypes, { messages: validationMessages });
1719
1842
  },
1720
- [fieldByName, formValues, validateRepeaterField, fieldTypes]
1843
+ [fieldByName, formValues, validateRepeaterField, fieldTypes, validationMessages]
1721
1844
  );
1722
1845
  const validateVisibleFields = (0, import_react2.useCallback)(
1723
1846
  (fieldSubset) => {
@@ -1733,7 +1856,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1733
1856
  }
1734
1857
  continue;
1735
1858
  }
1736
- const err = runValidators(formValues[field.name], field, formValues, fieldTypes);
1859
+ const err = runValidators(formValues[field.name], field, formValues, fieldTypes, { messages: validationMessages });
1737
1860
  if (err) {
1738
1861
  errors[field.name] = err;
1739
1862
  hasErrors = true;
@@ -1741,14 +1864,14 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1741
1864
  }
1742
1865
  return { errors, hasErrors };
1743
1866
  },
1744
- [visibleFields, formValues, validateRepeaterField, fieldTypes]
1867
+ [visibleFields, formValues, validateRepeaterField, fieldTypes, validationMessages]
1745
1868
  );
1746
1869
  const runAsyncValidation = (0, import_react2.useCallback)(
1747
1870
  (name, value) => {
1748
1871
  const field = fieldByName.get(name);
1749
1872
  if (!field || field.type === "repeater") return null;
1750
1873
  const val = value != null ? value : formValues[name];
1751
- const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false });
1874
+ const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false, messages: validationMessages });
1752
1875
  const prevController = asyncAbortRef.current.get(name);
1753
1876
  if (prevController) prevController.abort();
1754
1877
  asyncAbortRef.current.delete(name);
@@ -1939,23 +2062,27 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1939
2062
  );
1940
2063
  const handleFieldInput = (0, import_react2.useCallback)(
1941
2064
  (name, value) => {
2065
+ handleFieldChange(name, value);
1942
2066
  if (!validateOnChange) return;
1943
2067
  const err = validateField(name, value);
1944
2068
  updateErrors({ [name]: err });
1945
2069
  },
1946
- [validateOnChange, validateField, updateErrors]
2070
+ [validateOnChange, validateField, updateErrors, handleFieldChange]
1947
2071
  );
1948
2072
  const handleFieldBlur = (0, import_react2.useCallback)(
1949
2073
  (name, value) => {
1950
- if (!validateOnBlur) return;
1951
2074
  const resolvedValue = value != null ? value : formValuesRef.current[name];
2075
+ if (value != null && value !== formValuesRef.current[name]) {
2076
+ handleFieldChange(name, value);
2077
+ }
2078
+ if (!validateOnBlur) return;
1952
2079
  const err = validateField(name, resolvedValue);
1953
2080
  updateErrors({ [name]: err });
1954
2081
  if (!err) {
1955
2082
  triggerAsyncValidation(name, resolvedValue);
1956
2083
  }
1957
2084
  },
1958
- [validateOnBlur, validateField, updateErrors, triggerAsyncValidation]
2085
+ [validateOnBlur, validateField, updateErrors, triggerAsyncValidation, handleFieldChange]
1959
2086
  );
1960
2087
  const handleSubmit = (0, import_react2.useCallback)(
1961
2088
  async (e) => {
@@ -1988,8 +2115,17 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1988
2115
  const rawValues = {};
1989
2116
  for (const key of Object.keys(formValues)) {
1990
2117
  const f = fieldByName.get(key);
1991
- if (f && (f.type === "display" || f.type === "crmPropertyList" || f.type === "crmAssociationPropertyList")) continue;
1992
- rawValues[key] = formValues[key];
2118
+ if (f && (f.type === "display" || f.type === "crmPropertyList" || f.type === "crmAssociationPropertyList" || f.type === "fieldGroup")) continue;
2119
+ rawValues[key] = f && f.transformOut ? f.transformOut(formValues[key]) : formValues[key];
2120
+ }
2121
+ for (const f of fields) {
2122
+ if (f.type !== "fieldGroup" || !f.items || !f.fields) continue;
2123
+ for (const item of f.items) {
2124
+ for (const sf of f.fields(item)) {
2125
+ if (formValues[sf.name] === void 0) continue;
2126
+ rawValues[sf.name] = sf.transformOut ? sf.transformOut(formValues[sf.name]) : formValues[sf.name];
2127
+ }
2128
+ }
1993
2129
  }
1994
2130
  const submitValues = transformValues ? transformValues(rawValues) : rawValues;
1995
2131
  if (onBeforeSubmit) {
@@ -2117,6 +2253,12 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2117
2253
  [replaceErrors]
2118
2254
  );
2119
2255
  const renderField = (field) => {
2256
+ const fieldError = formErrors[field.name] || null;
2257
+ const rendered = renderFieldInner(field);
2258
+ if (!renderFieldError || !fieldError) return rendered;
2259
+ return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, rendered, renderFieldError(fieldError, field));
2260
+ };
2261
+ const renderFieldInner = (field) => {
2120
2262
  const fieldValue = formValues[field.name];
2121
2263
  const fieldError = formErrors[field.name] || null;
2122
2264
  const hasError = !!fieldError;
@@ -2126,10 +2268,125 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2126
2268
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
2127
2269
  if (field.type === "display") {
2128
2270
  if (field.render) {
2129
- return field.render({ allValues: formValues });
2271
+ return field.render({
2272
+ allValues: formValues,
2273
+ setFieldValue: (name, value) => handleFieldChange(name, value),
2274
+ setFieldError: (name, message) => updateErrors({ [name]: message })
2275
+ });
2130
2276
  }
2131
2277
  return null;
2132
2278
  }
2279
+ if (field.type === "fieldGroup") {
2280
+ const items = field.items || [];
2281
+ const fieldsFn = field.fields;
2282
+ if (!fieldsFn) return null;
2283
+ const groupColumns = field.columns || 1;
2284
+ const showItemLabel = field.showItemLabel !== false;
2285
+ 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), field.description && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { variant: "microcopy" }, field.description), items.map((item, itemIdx) => {
2286
+ const subFields = fieldsFn(item);
2287
+ return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { key: item.key || itemIdx, direction: "row", gap: "xs", align: "end" }, showItemLabel && item.label && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { flex: 1 }, itemIdx === 0 ? /* @__PURE__ */ import_react2.default.createElement(
2288
+ import_ui_extensions2.Input,
2289
+ {
2290
+ name: `_fieldGroup-label-${field.name}-${itemIdx}`,
2291
+ label: "\xA0",
2292
+ value: item.label,
2293
+ readOnly: true,
2294
+ disabled: true
2295
+ }
2296
+ ) : /* @__PURE__ */ import_react2.default.createElement(
2297
+ import_ui_extensions2.Input,
2298
+ {
2299
+ name: `_fieldGroup-label-${field.name}-${itemIdx}`,
2300
+ value: item.label,
2301
+ readOnly: true,
2302
+ disabled: true
2303
+ }
2304
+ )), subFields.map((sf) => {
2305
+ const sfValue = formValues[sf.name];
2306
+ const sfError = formErrors[sf.name] || null;
2307
+ const sfLabel = itemIdx === 0 ? sf.label : void 0;
2308
+ const sfReadOnly = sf.readOnly || formReadOnly;
2309
+ const sfDisabled = disabled || sf.disabled || formReadOnly;
2310
+ const sfOnChange = sf.debounce ? (v) => handleDebouncedFieldChange(sf.name, v) : (v) => handleFieldChange(sf.name, v);
2311
+ const sfProps = {
2312
+ name: sf.name,
2313
+ label: sfLabel,
2314
+ placeholder: sf.placeholder,
2315
+ description: itemIdx === 0 ? sf.description : void 0,
2316
+ readOnly: sfReadOnly,
2317
+ disabled: sfDisabled,
2318
+ error: !!sfError,
2319
+ validationMessage: sfError || void 0,
2320
+ ...sf.fieldProps || {}
2321
+ };
2322
+ let sfElement;
2323
+ switch (sf.type) {
2324
+ case "select":
2325
+ sfElement = /* @__PURE__ */ import_react2.default.createElement(
2326
+ import_ui_extensions2.Select,
2327
+ {
2328
+ ...sfProps,
2329
+ value: sfValue,
2330
+ options: resolveOptions(sf, formValues),
2331
+ onChange: sfOnChange
2332
+ }
2333
+ );
2334
+ break;
2335
+ case "number":
2336
+ sfElement = /* @__PURE__ */ import_react2.default.createElement(
2337
+ import_ui_extensions2.NumberInput,
2338
+ {
2339
+ ...sfProps,
2340
+ value: sfValue,
2341
+ onChange: sfOnChange,
2342
+ onBlur: (v) => handleFieldBlur(sf.name, v)
2343
+ }
2344
+ );
2345
+ break;
2346
+ case "toggle":
2347
+ sfElement = /* @__PURE__ */ import_react2.default.createElement(
2348
+ import_ui_extensions2.Toggle,
2349
+ {
2350
+ name: sf.name,
2351
+ label: sfLabel || sf.label,
2352
+ checked: !!sfValue,
2353
+ size: sf.size || "md",
2354
+ labelDisplay: sf.labelDisplay || "top",
2355
+ readonly: sfReadOnly,
2356
+ disabled: sfDisabled,
2357
+ onChange: sfOnChange,
2358
+ ...sf.fieldProps || {}
2359
+ }
2360
+ );
2361
+ break;
2362
+ case "time":
2363
+ sfElement = /* @__PURE__ */ import_react2.default.createElement(
2364
+ import_ui_extensions2.TimeInput,
2365
+ {
2366
+ ...sfProps,
2367
+ value: sfValue,
2368
+ interval: sf.interval,
2369
+ onChange: sfOnChange,
2370
+ onBlur: (v) => handleFieldBlur(sf.name, v)
2371
+ }
2372
+ );
2373
+ break;
2374
+ default:
2375
+ sfElement = /* @__PURE__ */ import_react2.default.createElement(
2376
+ import_ui_extensions2.Input,
2377
+ {
2378
+ ...sfProps,
2379
+ value: sfValue || "",
2380
+ onChange: sfOnChange,
2381
+ onInput: (v) => handleFieldInput(sf.name, v),
2382
+ onBlur: (v) => handleFieldBlur(sf.name, v)
2383
+ }
2384
+ );
2385
+ }
2386
+ return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { key: sf.name, flex: 1 }, sfElement);
2387
+ }));
2388
+ }));
2389
+ }
2133
2390
  if (field.type === "crmPropertyList") {
2134
2391
  return /* @__PURE__ */ import_react2.default.createElement(
2135
2392
  import_crm.CrmPropertyList,
@@ -2183,7 +2440,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2183
2440
  readOnly: isReadOnly,
2184
2441
  disabled: isDisabled,
2185
2442
  error: hasError,
2186
- validationMessage: fieldError || void 0,
2443
+ validationMessage: renderFieldError ? void 0 : fieldError || void 0,
2187
2444
  ...field.loading || validatingFields[field.name] ? { loading: true } : {},
2188
2445
  ...field.fieldProps || {}
2189
2446
  };
@@ -2253,7 +2510,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2253
2510
  import_ui_extensions2.CurrencyInput,
2254
2511
  {
2255
2512
  ...commonProps,
2256
- currency: field.currency || "USD",
2513
+ currency: field.currency || defaultCurrency,
2257
2514
  value: fieldValue,
2258
2515
  min: field.min,
2259
2516
  max: field.max,
@@ -2431,8 +2688,8 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2431
2688
  const renderRemoveControl = repeaterProps.renderRemove;
2432
2689
  const renderMoveUpControl = repeaterProps.renderMoveUp;
2433
2690
  const renderMoveDownControl = repeaterProps.renderMoveDown;
2434
- const addLabel = repeaterProps.addLabel || "Add";
2435
- const removeLabel = repeaterProps.removeLabel || "Remove";
2691
+ const addLabel = repeaterProps.addLabel || repeaterAddLabel;
2692
+ const removeLabel = repeaterProps.removeLabel || repeaterRemoveLabel;
2436
2693
  const moveUpLabel = repeaterProps.moveUpLabel || "Up";
2437
2694
  const moveDownLabel = repeaterProps.moveDownLabel || "Down";
2438
2695
  const canEditRows = !isReadOnly && !isDisabled;
@@ -2466,7 +2723,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2466
2723
  };
2467
2724
  const validateSubField = (rowIdx, subField, subValue, nextRows) => {
2468
2725
  const rowValues = { ...formValues, [field.name]: nextRows };
2469
- const err = runValidators(subValue, subField, rowValues, fieldTypes);
2726
+ const err = runValidators(subValue, subField, rowValues, fieldTypes, { messages: validationMessages });
2470
2727
  setRepeaterSubFieldError(field.name, rowIdx, subField.name, err);
2471
2728
  };
2472
2729
  const handleSubFieldChange = (rowIdx, subField, subValue) => {
@@ -2584,7 +2841,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2584
2841
  const renderDependentGroup = (parentField, dependents) => {
2585
2842
  const firstWithLabel = dependents.find((f) => getDependsOnLabel(f)) || dependents[0];
2586
2843
  const firstWithMessage = dependents.find((f) => getDependsOnMessage(f)) || dependents[0];
2587
- const groupLabel = getDependsOnLabel(firstWithLabel) || "Dependent properties";
2844
+ const groupLabel = getDependsOnLabel(firstWithLabel) || dependentPropertiesLabel;
2588
2845
  const rawMessage = getDependsOnMessage(firstWithMessage);
2589
2846
  const tooltipMessage = typeof rawMessage === "function" ? rawMessage(parentField.label) : rawMessage || "";
2590
2847
  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)));
@@ -2674,38 +2931,19 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2674
2931
  }
2675
2932
  return elements;
2676
2933
  };
2677
- const renderLegacyLayout = (fieldSubset) => {
2934
+ const renderSingleColumnLayout = (fieldSubset) => {
2678
2935
  const fieldList = fieldSubset || visibleFields;
2679
- const rows = [];
2680
- let i = 0;
2681
- while (i < fieldList.length) {
2682
- const field = fieldList[i];
2683
- if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !getDependsOnName(field)) {
2684
- rows.push({ type: "pair", fields: [fieldList[i], fieldList[i + 1]] });
2685
- i += 2;
2686
- } else {
2687
- rows.push({ type: "single", field });
2688
- i++;
2689
- }
2690
- }
2691
2936
  const elements = [];
2692
2937
  const processedDeps = /* @__PURE__ */ new Set();
2693
- for (const row of rows) {
2694
- if (row.type === "pair") {
2695
- elements.push(
2696
- /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { key: `pair-${row.fields[0].name}`, direction: "row", gap: "sm" }, /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { flex: 1 }, renderField(row.fields[0])), /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { flex: 1 }, renderField(row.fields[1])))
2697
- );
2698
- } else {
2699
- const field = row.field;
2700
- if (processedDeps.has(field.name)) continue;
2701
- elements.push(
2702
- /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, { key: field.name }, renderField(field))
2703
- );
2704
- const dependents = getDependents(field);
2705
- if (dependents.length > 0) {
2706
- for (const dep of dependents) processedDeps.add(dep.name);
2707
- elements.push(renderDependentGroup(field, dependents));
2708
- }
2938
+ for (const field of fieldList) {
2939
+ if (processedDeps.has(field.name)) continue;
2940
+ elements.push(
2941
+ /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, { key: field.name }, renderField(field))
2942
+ );
2943
+ const dependents = getDependents(field);
2944
+ if (dependents.length > 0) {
2945
+ for (const dep of dependents) processedDeps.add(dep.name);
2946
+ elements.push(renderDependentGroup(field, dependents));
2709
2947
  }
2710
2948
  }
2711
2949
  return elements;
@@ -2716,10 +2954,20 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2716
2954
  let batch = [];
2717
2955
  const flushBatch = () => {
2718
2956
  if (batch.length === 0) return;
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) {
2957
+ if (maxColumns) {
2958
+ const chunks = Array.from(
2959
+ { length: Math.ceil(batch.length / maxColumns) },
2960
+ (_, i) => batch.slice(i * maxColumns, i * maxColumns + maxColumns)
2961
+ );
2962
+ for (const chunk of chunks) {
2963
+ const remainder = maxColumns - chunk.length;
2964
+ elements.push(
2965
+ /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { key: `ag-${chunk[0].name}`, direction: "row", gap }, chunk.map((f) => /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { key: f.name, flex: 1 }, renderField(f))), remainder > 0 && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Box, { flex: remainder }))
2966
+ );
2967
+ }
2968
+ } else {
2721
2969
  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))))
2970
+ /* @__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))))
2723
2971
  );
2724
2972
  }
2725
2973
  batch = [];
@@ -2778,7 +3026,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2778
3026
  if (layout && fieldSubset === visibleFields) return renderExplicitLayout();
2779
3027
  if (columnWidth) return renderAutoGridLayout(fieldSubset);
2780
3028
  if (columns > 1) return renderGridLayout(fieldSubset);
2781
- return renderLegacyLayout(fieldSubset);
3029
+ return renderSingleColumnLayout(fieldSubset);
2782
3030
  };
2783
3031
  const renderSections = () => {
2784
3032
  const hasSections = Array.isArray(sections) && sections.length > 0;
@@ -2791,7 +3039,8 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2791
3039
  for (const sec of sections) {
2792
3040
  const sectionFields = sec.fields ? visibleFields.filter((f) => sec.fields.includes(f.name)) : [];
2793
3041
  if (sectionFields.length === 0) continue;
2794
- const accordionContent = /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "column", gap }, renderFieldSubset(sectionFields));
3042
+ const sectionContext = { values: formValues, errors: formErrors };
3043
+ const accordionContent = /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "column", gap }, sec.renderBefore && sec.renderBefore(sectionContext), renderFieldSubset(sectionFields), sec.renderAfter && sec.renderAfter(sectionContext));
2795
3044
  const accordion = /* @__PURE__ */ import_react2.default.createElement(
2796
3045
  import_ui_extensions2.Accordion,
2797
3046
  {
@@ -2884,7 +3133,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2884
3133
  currentStep,
2885
3134
  stepNames: steps.map((s) => s.title)
2886
3135
  }
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({
3136
+ ), showReadOnlyAlert && formReadOnly && readOnlyMessage && (renderReadOnlyAlert ? renderReadOnlyAlert({ title: readOnlyTitle, message: readOnlyMessage }) : /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Alert, { title: readOnlyTitle, variant: "warning" }, readOnlyMessage)), showInlineAlerts && !addAlert && formError && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Alert, { title: errorTitle, variant: "danger" }, typeof formError === "string" ? formError : void 0), showInlineAlerts && !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({
2888
3137
  values: formValues,
2889
3138
  goNext: handleNext,
2890
3139
  goBack: handleBack,