hs-uix 1.6.3 → 1.6.5

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/datatable.js CHANGED
@@ -186,6 +186,8 @@ var DataTable = ({
186
186
  showFirstLastButtons,
187
187
  // show First/Last page buttons (default: auto when pageCount > 5)
188
188
  // Row count
189
+ title,
190
+ // optional title shown as demibold text above the table toolbar
189
191
  showRowCount = true,
190
192
  // show "X records" / "X of Y records" text
191
193
  rowCountBold = false,
@@ -655,6 +657,8 @@ var DataTable = ({
655
657
  selectionResetRef.current = combinedSelectionResetKey;
656
658
  }, [combinedSelectionResetKey, selectable, externalSelectedIds]);
657
659
  const selectedIds = externalSelectedIds != null ? new Set(externalSelectedIds) : internalSelectedIds;
660
+ const showToolbarCount = showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
661
+ const hasToolbarContent = showSearch && searchFields.length > 0 || filters.length > 0 || activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) || showToolbarCount;
658
662
  const showRowActionsColumn = !!rowActions && !(hideRowActionsWhenSelectionActive && selectable && selectedIds.size > 0);
659
663
  const applySelection = (0, import_react.useCallback)((nextSet) => {
660
664
  if (externalSelectedIds == null) {
@@ -1004,7 +1008,7 @@ var DataTable = ({
1004
1008
  }
1005
1009
  );
1006
1010
  };
1007
- 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(
1011
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, title && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", justify: "between", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, title)), hasToolbarContent && /* @__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(
1008
1012
  import_ui_extensions.SearchInput,
1009
1013
  {
1010
1014
  name: "datatable-search",
@@ -1030,7 +1034,7 @@ var DataTable = ({
1030
1034
  onClick: () => handleFilterRemove("all")
1031
1035
  },
1032
1036
  resolvedClearAllLabel
1033
- )))), 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({
1037
+ )))), showToolbarCount && /* @__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({
1034
1038
  selectedIds,
1035
1039
  selectedCount: selectedIds.size,
1036
1040
  displayCount,
@@ -182,6 +182,8 @@ var DataTable = ({
182
182
  showFirstLastButtons,
183
183
  // show First/Last page buttons (default: auto when pageCount > 5)
184
184
  // Row count
185
+ title,
186
+ // optional title shown as demibold text above the table toolbar
185
187
  showRowCount = true,
186
188
  // show "X records" / "X of Y records" text
187
189
  rowCountBold = false,
@@ -651,6 +653,8 @@ var DataTable = ({
651
653
  selectionResetRef.current = combinedSelectionResetKey;
652
654
  }, [combinedSelectionResetKey, selectable, externalSelectedIds]);
653
655
  const selectedIds = externalSelectedIds != null ? new Set(externalSelectedIds) : internalSelectedIds;
656
+ const showToolbarCount = showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
657
+ const hasToolbarContent = showSearch && searchFields.length > 0 || filters.length > 0 || activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) || showToolbarCount;
654
658
  const showRowActionsColumn = !!rowActions && !(hideRowActionsWhenSelectionActive && selectable && selectedIds.size > 0);
655
659
  const applySelection = useCallback((nextSet) => {
656
660
  if (externalSelectedIds == null) {
@@ -1000,7 +1004,7 @@ var DataTable = ({
1000
1004
  }
1001
1005
  );
1002
1006
  };
1003
- 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(
1007
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, title && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", justify: "between", gap: "sm" }, /* @__PURE__ */ React.createElement(Text, { format: { fontWeight: "demibold" } }, title)), hasToolbarContent && /* @__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(
1004
1008
  SearchInput,
1005
1009
  {
1006
1010
  name: "datatable-search",
@@ -1026,7 +1030,7 @@ var DataTable = ({
1026
1030
  onClick: () => handleFilterRemove("all")
1027
1031
  },
1028
1032
  resolvedClearAllLabel
1029
- )))), 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({
1033
+ )))), showToolbarCount && /* @__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({
1030
1034
  selectedIds,
1031
1035
  selectedCount: selectedIds.size,
1032
1036
  displayCount,
package/dist/form.js CHANGED
@@ -378,8 +378,12 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
378
378
  // validate on blur
379
379
  validateOnSubmit = true,
380
380
  // validate all before onSubmit
381
- onValidationChange
381
+ onValidationChange,
382
382
  // (errors) => void
383
+ onValidationFail,
384
+ // ({ errors, fields, firstInvalidField }) => void — called when submit-time validation blocks submission
385
+ openSectionOnValidationFail = false
386
+ // auto-open accordion section containing first invalid field on submit failure
383
387
  } = props;
384
388
  const {
385
389
  steps,
@@ -404,6 +408,8 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
404
408
  // () => void
405
409
  submitPosition = "bottom",
406
410
  // "bottom" | "none"
411
+ submitAlign,
412
+ // default single-step action row alignment
407
413
  loading: controlledLoading,
408
414
  // controlled loading state
409
415
  disabled = false,
@@ -526,6 +532,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
526
532
  "tooltip",
527
533
  "required",
528
534
  "readOnly",
535
+ "alwaysEditable",
529
536
  "disabled",
530
537
  "defaultValue",
531
538
  "fieldProps",
@@ -698,6 +705,17 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
698
705
  }
699
706
  return map;
700
707
  }, [fields]);
708
+ const sectionIdByFieldName = (0, import_react.useMemo)(() => {
709
+ const map = /* @__PURE__ */ new Map();
710
+ if (Array.isArray(sections)) {
711
+ for (const sec of sections) {
712
+ if (!sec || !Array.isArray(sec.fields)) continue;
713
+ for (const name of sec.fields) map.set(name, sec.id);
714
+ }
715
+ }
716
+ return map;
717
+ }, [sections]);
718
+ const [validationOpenSection, setValidationOpenSection] = (0, import_react.useState)(null);
701
719
  const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
702
720
  const configWarningsRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
703
721
  const warnConfig = (0, import_react.useCallback)((message) => {
@@ -708,6 +726,10 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
708
726
  console.warn(`[FormBuilder] ${message}`);
709
727
  }
710
728
  }, [isDev]);
729
+ (0, import_react.useEffect)(() => {
730
+ if (!isMultiStep || !submitAlign) return;
731
+ warnConfig("submitAlign is ignored when steps are provided. Use renderButtons for custom multi-step button layout.");
732
+ }, [isMultiStep, submitAlign, warnConfig]);
711
733
  const replaceErrors = (0, import_react.useCallback)(
712
734
  (nextErrors) => {
713
735
  if (controlledErrors == null) setInternalErrors(nextErrors);
@@ -1274,10 +1296,35 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1274
1296
  const handleSubmit = (0, import_react.useCallback)(
1275
1297
  async (e) => {
1276
1298
  if (e && e.preventDefault) e.preventDefault();
1299
+ const reportValidationFailure = (errors) => {
1300
+ const errorNames = Object.keys(errors).filter((n) => !!errors[n]);
1301
+ if (errorNames.length === 0) return;
1302
+ const orderedNames = allVisibleFields.map((f) => f.name).filter((n) => errorNames.includes(n));
1303
+ for (const n of errorNames) if (!orderedNames.includes(n)) orderedNames.push(n);
1304
+ const fieldInfos = orderedNames.map((name) => {
1305
+ const f = fieldByName.get(name);
1306
+ return {
1307
+ name,
1308
+ label: f == null ? void 0 : f.label,
1309
+ sectionId: sectionIdByFieldName.get(name)
1310
+ };
1311
+ });
1312
+ const firstInvalidField = fieldInfos[0];
1313
+ if (openSectionOnValidationFail && (firstInvalidField == null ? void 0 : firstInvalidField.sectionId)) {
1314
+ setValidationOpenSection({
1315
+ id: firstInvalidField.sectionId,
1316
+ nonce: ((validationOpenSection == null ? void 0 : validationOpenSection.nonce) || 0) + 1
1317
+ });
1318
+ }
1319
+ if (onValidationFail) {
1320
+ onValidationFail({ errors, fields: fieldInfos, firstInvalidField });
1321
+ }
1322
+ };
1277
1323
  if (validateOnSubmit) {
1278
1324
  const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
1279
1325
  if (hasErrors) {
1280
1326
  replaceErrors(errors);
1327
+ reportValidationFailure(errors);
1281
1328
  return;
1282
1329
  }
1283
1330
  const asyncSubmitValidations = getAsyncValidationTargets(allVisibleFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
@@ -1289,7 +1336,10 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1289
1336
  ])
1290
1337
  ];
1291
1338
  await Promise.all(pendingValidations);
1292
- if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) return;
1339
+ if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) {
1340
+ reportValidationFailure(formErrorsRef.current);
1341
+ return;
1342
+ }
1293
1343
  }
1294
1344
  }
1295
1345
  const reset = () => {
@@ -1335,7 +1385,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1335
1385
  if (controlledLoading == null) setInternalLoading(false);
1336
1386
  }
1337
1387
  },
1338
- [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget]
1388
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget, onValidationFail, openSectionOnValidationFail, sectionIdByFieldName, validationOpenSection]
1339
1389
  );
1340
1390
  const handleNext = (0, import_react.useCallback)(async () => {
1341
1391
  if (!isMultiStep) return;
@@ -1428,8 +1478,9 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1428
1478
  const fieldError = formErrors[field.name] || null;
1429
1479
  const hasError = !!fieldError;
1430
1480
  const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
1431
- const isReadOnly = field.readOnly || formReadOnly;
1432
- const isDisabled = disabled || resolveDisabled(field, formValues) || formReadOnly;
1481
+ const fieldFormReadOnly = field.alwaysEditable ? false : formReadOnly;
1482
+ const isReadOnly = field.readOnly || fieldFormReadOnly;
1483
+ const isDisabled = disabled || resolveDisabled(field, formValues) || fieldFormReadOnly;
1433
1484
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
1434
1485
  if (field.type === "display" || field.type === "slot") {
1435
1486
  if (field.render) {
@@ -1472,8 +1523,9 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
1472
1523
  const sfValue = formValues[sf.name];
1473
1524
  const sfError = formErrors[sf.name] || null;
1474
1525
  const sfLabel = itemIdx === 0 ? sf.label : void 0;
1475
- const sfReadOnly = sf.readOnly || formReadOnly;
1476
- const sfDisabled = disabled || resolveDisabled(sf, formValues) || formReadOnly;
1526
+ const sfFormReadOnly = sf.alwaysEditable ? false : formReadOnly;
1527
+ const sfReadOnly = sf.readOnly || sfFormReadOnly;
1528
+ const sfDisabled = disabled || resolveDisabled(sf, formValues) || sfFormReadOnly;
1477
1529
  const sfOnChange = sf.debounce ? (v) => handleDebouncedFieldChange(sf.name, v) : (v) => handleFieldChange(sf.name, v);
1478
1530
  const sfProps = {
1479
1531
  name: sf.name,
@@ -2270,13 +2322,16 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
2270
2322
  const sectionContext = { values: formValues, errors: formErrors };
2271
2323
  const sectionOverrides = sec.columns ? { columns: sec.columns } : void 0;
2272
2324
  const accordionContent = /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap }, sec.renderBefore && sec.renderBefore(sectionContext), renderFieldSubset(sectionFields, sectionOverrides), sec.renderAfter && sec.renderAfter(sectionContext));
2325
+ const isValidationOverrideTarget = validationOpenSection && validationOpenSection.id === sec.id;
2326
+ const accordionKey = isValidationOverrideTarget ? `${sec.id}::open::${validationOpenSection.nonce}` : sec.id;
2327
+ const accordionDefaultOpen = isValidationOverrideTarget ? true : sec.defaultOpen !== false;
2273
2328
  const accordion = /* @__PURE__ */ import_react.default.createElement(
2274
2329
  import_ui_extensions.Accordion,
2275
2330
  {
2276
- key: sec.id,
2331
+ key: accordionKey,
2277
2332
  title: sec.label,
2278
2333
  size: "sm",
2279
- defaultOpen: sec.defaultOpen !== false
2334
+ defaultOpen: accordionDefaultOpen
2280
2335
  },
2281
2336
  accordionContent
2282
2337
  );
@@ -2310,6 +2365,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
2310
2365
  if (submitPosition === "none" || formReadOnly) return null;
2311
2366
  const isLastStep = !isMultiStep || currentStep === steps.length - 1;
2312
2367
  const isFirstStep = !isMultiStep || currentStep === 0;
2368
+ const singleStepJustify = submitAlign || (showCancel ? "between" : "start");
2313
2369
  const buttonContext = {
2314
2370
  isMultiStep,
2315
2371
  isFirstStep,
@@ -2344,7 +2400,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
2344
2400
  submitButtonLabel
2345
2401
  ) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "primary", onClick: handleNext, disabled }, nextButtonLabel)));
2346
2402
  }
2347
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ import_react.default.createElement(
2403
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: singleStepJustify, gap: "sm" }, showCancel && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ import_react.default.createElement(
2348
2404
  import_ui_extensions.LoadingButton,
2349
2405
  {
2350
2406
  variant: submitVariant,
package/dist/form.mjs CHANGED
@@ -382,8 +382,12 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
382
382
  // validate on blur
383
383
  validateOnSubmit = true,
384
384
  // validate all before onSubmit
385
- onValidationChange
385
+ onValidationChange,
386
386
  // (errors) => void
387
+ onValidationFail,
388
+ // ({ errors, fields, firstInvalidField }) => void — called when submit-time validation blocks submission
389
+ openSectionOnValidationFail = false
390
+ // auto-open accordion section containing first invalid field on submit failure
387
391
  } = props;
388
392
  const {
389
393
  steps,
@@ -408,6 +412,8 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
408
412
  // () => void
409
413
  submitPosition = "bottom",
410
414
  // "bottom" | "none"
415
+ submitAlign,
416
+ // default single-step action row alignment
411
417
  loading: controlledLoading,
412
418
  // controlled loading state
413
419
  disabled = false,
@@ -530,6 +536,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
530
536
  "tooltip",
531
537
  "required",
532
538
  "readOnly",
539
+ "alwaysEditable",
533
540
  "disabled",
534
541
  "defaultValue",
535
542
  "fieldProps",
@@ -702,6 +709,17 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
702
709
  }
703
710
  return map;
704
711
  }, [fields]);
712
+ const sectionIdByFieldName = useMemo(() => {
713
+ const map = /* @__PURE__ */ new Map();
714
+ if (Array.isArray(sections)) {
715
+ for (const sec of sections) {
716
+ if (!sec || !Array.isArray(sec.fields)) continue;
717
+ for (const name of sec.fields) map.set(name, sec.id);
718
+ }
719
+ }
720
+ return map;
721
+ }, [sections]);
722
+ const [validationOpenSection, setValidationOpenSection] = useState(null);
705
723
  const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
706
724
  const configWarningsRef = useRef(/* @__PURE__ */ new Set());
707
725
  const warnConfig = useCallback((message) => {
@@ -712,6 +730,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
712
730
  console.warn(`[FormBuilder] ${message}`);
713
731
  }
714
732
  }, [isDev]);
733
+ useEffect(() => {
734
+ if (!isMultiStep || !submitAlign) return;
735
+ warnConfig("submitAlign is ignored when steps are provided. Use renderButtons for custom multi-step button layout.");
736
+ }, [isMultiStep, submitAlign, warnConfig]);
715
737
  const replaceErrors = useCallback(
716
738
  (nextErrors) => {
717
739
  if (controlledErrors == null) setInternalErrors(nextErrors);
@@ -1278,10 +1300,35 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1278
1300
  const handleSubmit = useCallback(
1279
1301
  async (e) => {
1280
1302
  if (e && e.preventDefault) e.preventDefault();
1303
+ const reportValidationFailure = (errors) => {
1304
+ const errorNames = Object.keys(errors).filter((n) => !!errors[n]);
1305
+ if (errorNames.length === 0) return;
1306
+ const orderedNames = allVisibleFields.map((f) => f.name).filter((n) => errorNames.includes(n));
1307
+ for (const n of errorNames) if (!orderedNames.includes(n)) orderedNames.push(n);
1308
+ const fieldInfos = orderedNames.map((name) => {
1309
+ const f = fieldByName.get(name);
1310
+ return {
1311
+ name,
1312
+ label: f == null ? void 0 : f.label,
1313
+ sectionId: sectionIdByFieldName.get(name)
1314
+ };
1315
+ });
1316
+ const firstInvalidField = fieldInfos[0];
1317
+ if (openSectionOnValidationFail && (firstInvalidField == null ? void 0 : firstInvalidField.sectionId)) {
1318
+ setValidationOpenSection({
1319
+ id: firstInvalidField.sectionId,
1320
+ nonce: ((validationOpenSection == null ? void 0 : validationOpenSection.nonce) || 0) + 1
1321
+ });
1322
+ }
1323
+ if (onValidationFail) {
1324
+ onValidationFail({ errors, fields: fieldInfos, firstInvalidField });
1325
+ }
1326
+ };
1281
1327
  if (validateOnSubmit) {
1282
1328
  const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
1283
1329
  if (hasErrors) {
1284
1330
  replaceErrors(errors);
1331
+ reportValidationFailure(errors);
1285
1332
  return;
1286
1333
  }
1287
1334
  const asyncSubmitValidations = getAsyncValidationTargets(allVisibleFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
@@ -1293,7 +1340,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1293
1340
  ])
1294
1341
  ];
1295
1342
  await Promise.all(pendingValidations);
1296
- if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) return;
1343
+ if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) {
1344
+ reportValidationFailure(formErrorsRef.current);
1345
+ return;
1346
+ }
1297
1347
  }
1298
1348
  }
1299
1349
  const reset = () => {
@@ -1339,7 +1389,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1339
1389
  if (controlledLoading == null) setInternalLoading(false);
1340
1390
  }
1341
1391
  },
1342
- [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget]
1392
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget, onValidationFail, openSectionOnValidationFail, sectionIdByFieldName, validationOpenSection]
1343
1393
  );
1344
1394
  const handleNext = useCallback(async () => {
1345
1395
  if (!isMultiStep) return;
@@ -1432,8 +1482,9 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1432
1482
  const fieldError = formErrors[field.name] || null;
1433
1483
  const hasError = !!fieldError;
1434
1484
  const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
1435
- const isReadOnly = field.readOnly || formReadOnly;
1436
- const isDisabled = disabled || resolveDisabled(field, formValues) || formReadOnly;
1485
+ const fieldFormReadOnly = field.alwaysEditable ? false : formReadOnly;
1486
+ const isReadOnly = field.readOnly || fieldFormReadOnly;
1487
+ const isDisabled = disabled || resolveDisabled(field, formValues) || fieldFormReadOnly;
1437
1488
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
1438
1489
  if (field.type === "display" || field.type === "slot") {
1439
1490
  if (field.render) {
@@ -1476,8 +1527,9 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1476
1527
  const sfValue = formValues[sf.name];
1477
1528
  const sfError = formErrors[sf.name] || null;
1478
1529
  const sfLabel = itemIdx === 0 ? sf.label : void 0;
1479
- const sfReadOnly = sf.readOnly || formReadOnly;
1480
- const sfDisabled = disabled || resolveDisabled(sf, formValues) || formReadOnly;
1530
+ const sfFormReadOnly = sf.alwaysEditable ? false : formReadOnly;
1531
+ const sfReadOnly = sf.readOnly || sfFormReadOnly;
1532
+ const sfDisabled = disabled || resolveDisabled(sf, formValues) || sfFormReadOnly;
1481
1533
  const sfOnChange = sf.debounce ? (v) => handleDebouncedFieldChange(sf.name, v) : (v) => handleFieldChange(sf.name, v);
1482
1534
  const sfProps = {
1483
1535
  name: sf.name,
@@ -2274,13 +2326,16 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2274
2326
  const sectionContext = { values: formValues, errors: formErrors };
2275
2327
  const sectionOverrides = sec.columns ? { columns: sec.columns } : void 0;
2276
2328
  const accordionContent = /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap }, sec.renderBefore && sec.renderBefore(sectionContext), renderFieldSubset(sectionFields, sectionOverrides), sec.renderAfter && sec.renderAfter(sectionContext));
2329
+ const isValidationOverrideTarget = validationOpenSection && validationOpenSection.id === sec.id;
2330
+ const accordionKey = isValidationOverrideTarget ? `${sec.id}::open::${validationOpenSection.nonce}` : sec.id;
2331
+ const accordionDefaultOpen = isValidationOverrideTarget ? true : sec.defaultOpen !== false;
2277
2332
  const accordion = /* @__PURE__ */ React.createElement(
2278
2333
  Accordion,
2279
2334
  {
2280
- key: sec.id,
2335
+ key: accordionKey,
2281
2336
  title: sec.label,
2282
2337
  size: "sm",
2283
- defaultOpen: sec.defaultOpen !== false
2338
+ defaultOpen: accordionDefaultOpen
2284
2339
  },
2285
2340
  accordionContent
2286
2341
  );
@@ -2314,6 +2369,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2314
2369
  if (submitPosition === "none" || formReadOnly) return null;
2315
2370
  const isLastStep = !isMultiStep || currentStep === steps.length - 1;
2316
2371
  const isFirstStep = !isMultiStep || currentStep === 0;
2372
+ const singleStepJustify = submitAlign || (showCancel ? "between" : "start");
2317
2373
  const buttonContext = {
2318
2374
  isMultiStep,
2319
2375
  isFirstStep,
@@ -2348,7 +2404,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2348
2404
  submitButtonLabel
2349
2405
  ) : /* @__PURE__ */ React.createElement(Button, { variant: "primary", onClick: handleNext, disabled }, nextButtonLabel)));
2350
2406
  }
2351
- return /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ React.createElement(Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ React.createElement(
2407
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: singleStepJustify, gap: "sm" }, showCancel && /* @__PURE__ */ React.createElement(Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ React.createElement(
2352
2408
  LoadingButton,
2353
2409
  {
2354
2410
  variant: submitVariant,
package/dist/index.js CHANGED
@@ -217,6 +217,8 @@ var DataTable = ({
217
217
  showFirstLastButtons,
218
218
  // show First/Last page buttons (default: auto when pageCount > 5)
219
219
  // Row count
220
+ title,
221
+ // optional title shown as demibold text above the table toolbar
220
222
  showRowCount = true,
221
223
  // show "X records" / "X of Y records" text
222
224
  rowCountBold = false,
@@ -686,6 +688,8 @@ var DataTable = ({
686
688
  selectionResetRef.current = combinedSelectionResetKey;
687
689
  }, [combinedSelectionResetKey, selectable, externalSelectedIds]);
688
690
  const selectedIds = externalSelectedIds != null ? new Set(externalSelectedIds) : internalSelectedIds;
691
+ const showToolbarCount = showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
692
+ const hasToolbarContent = showSearch && searchFields.length > 0 || filters.length > 0 || activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) || showToolbarCount;
689
693
  const showRowActionsColumn = !!rowActions && !(hideRowActionsWhenSelectionActive && selectable && selectedIds.size > 0);
690
694
  const applySelection = (0, import_react.useCallback)((nextSet) => {
691
695
  if (externalSelectedIds == null) {
@@ -1035,7 +1039,7 @@ var DataTable = ({
1035
1039
  }
1036
1040
  );
1037
1041
  };
1038
- 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(
1042
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, title && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", justify: "between", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, title)), hasToolbarContent && /* @__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(
1039
1043
  import_ui_extensions.SearchInput,
1040
1044
  {
1041
1045
  name: "datatable-search",
@@ -1061,7 +1065,7 @@ var DataTable = ({
1061
1065
  onClick: () => handleFilterRemove("all")
1062
1066
  },
1063
1067
  resolvedClearAllLabel
1064
- )))), 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({
1068
+ )))), showToolbarCount && /* @__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({
1065
1069
  selectedIds,
1066
1070
  selectedCount: selectedIds.size,
1067
1071
  displayCount,
@@ -1514,8 +1518,12 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1514
1518
  // validate on blur
1515
1519
  validateOnSubmit = true,
1516
1520
  // validate all before onSubmit
1517
- onValidationChange
1521
+ onValidationChange,
1518
1522
  // (errors) => void
1523
+ onValidationFail,
1524
+ // ({ errors, fields, firstInvalidField }) => void — called when submit-time validation blocks submission
1525
+ openSectionOnValidationFail = false
1526
+ // auto-open accordion section containing first invalid field on submit failure
1519
1527
  } = props;
1520
1528
  const {
1521
1529
  steps,
@@ -1540,6 +1548,8 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1540
1548
  // () => void
1541
1549
  submitPosition = "bottom",
1542
1550
  // "bottom" | "none"
1551
+ submitAlign,
1552
+ // default single-step action row alignment
1543
1553
  loading: controlledLoading,
1544
1554
  // controlled loading state
1545
1555
  disabled = false,
@@ -1662,6 +1672,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1662
1672
  "tooltip",
1663
1673
  "required",
1664
1674
  "readOnly",
1675
+ "alwaysEditable",
1665
1676
  "disabled",
1666
1677
  "defaultValue",
1667
1678
  "fieldProps",
@@ -1834,6 +1845,17 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1834
1845
  }
1835
1846
  return map;
1836
1847
  }, [fields]);
1848
+ const sectionIdByFieldName = (0, import_react2.useMemo)(() => {
1849
+ const map = /* @__PURE__ */ new Map();
1850
+ if (Array.isArray(sections)) {
1851
+ for (const sec of sections) {
1852
+ if (!sec || !Array.isArray(sec.fields)) continue;
1853
+ for (const name of sec.fields) map.set(name, sec.id);
1854
+ }
1855
+ }
1856
+ return map;
1857
+ }, [sections]);
1858
+ const [validationOpenSection, setValidationOpenSection] = (0, import_react2.useState)(null);
1837
1859
  const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
1838
1860
  const configWarningsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
1839
1861
  const warnConfig = (0, import_react2.useCallback)((message) => {
@@ -1844,6 +1866,10 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1844
1866
  console.warn(`[FormBuilder] ${message}`);
1845
1867
  }
1846
1868
  }, [isDev]);
1869
+ (0, import_react2.useEffect)(() => {
1870
+ if (!isMultiStep || !submitAlign) return;
1871
+ warnConfig("submitAlign is ignored when steps are provided. Use renderButtons for custom multi-step button layout.");
1872
+ }, [isMultiStep, submitAlign, warnConfig]);
1847
1873
  const replaceErrors = (0, import_react2.useCallback)(
1848
1874
  (nextErrors) => {
1849
1875
  if (controlledErrors == null) setInternalErrors(nextErrors);
@@ -2410,10 +2436,35 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2410
2436
  const handleSubmit = (0, import_react2.useCallback)(
2411
2437
  async (e) => {
2412
2438
  if (e && e.preventDefault) e.preventDefault();
2439
+ const reportValidationFailure = (errors) => {
2440
+ const errorNames = Object.keys(errors).filter((n) => !!errors[n]);
2441
+ if (errorNames.length === 0) return;
2442
+ const orderedNames = allVisibleFields.map((f) => f.name).filter((n) => errorNames.includes(n));
2443
+ for (const n of errorNames) if (!orderedNames.includes(n)) orderedNames.push(n);
2444
+ const fieldInfos = orderedNames.map((name) => {
2445
+ const f = fieldByName.get(name);
2446
+ return {
2447
+ name,
2448
+ label: f == null ? void 0 : f.label,
2449
+ sectionId: sectionIdByFieldName.get(name)
2450
+ };
2451
+ });
2452
+ const firstInvalidField = fieldInfos[0];
2453
+ if (openSectionOnValidationFail && (firstInvalidField == null ? void 0 : firstInvalidField.sectionId)) {
2454
+ setValidationOpenSection({
2455
+ id: firstInvalidField.sectionId,
2456
+ nonce: ((validationOpenSection == null ? void 0 : validationOpenSection.nonce) || 0) + 1
2457
+ });
2458
+ }
2459
+ if (onValidationFail) {
2460
+ onValidationFail({ errors, fields: fieldInfos, firstInvalidField });
2461
+ }
2462
+ };
2413
2463
  if (validateOnSubmit) {
2414
2464
  const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
2415
2465
  if (hasErrors) {
2416
2466
  replaceErrors(errors);
2467
+ reportValidationFailure(errors);
2417
2468
  return;
2418
2469
  }
2419
2470
  const asyncSubmitValidations = getAsyncValidationTargets(allVisibleFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
@@ -2425,7 +2476,10 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2425
2476
  ])
2426
2477
  ];
2427
2478
  await Promise.all(pendingValidations);
2428
- if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) return;
2479
+ if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) {
2480
+ reportValidationFailure(formErrorsRef.current);
2481
+ return;
2482
+ }
2429
2483
  }
2430
2484
  }
2431
2485
  const reset = () => {
@@ -2471,7 +2525,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2471
2525
  if (controlledLoading == null) setInternalLoading(false);
2472
2526
  }
2473
2527
  },
2474
- [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget]
2528
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget, onValidationFail, openSectionOnValidationFail, sectionIdByFieldName, validationOpenSection]
2475
2529
  );
2476
2530
  const handleNext = (0, import_react2.useCallback)(async () => {
2477
2531
  if (!isMultiStep) return;
@@ -2564,8 +2618,9 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2564
2618
  const fieldError = formErrors[field.name] || null;
2565
2619
  const hasError = !!fieldError;
2566
2620
  const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
2567
- const isReadOnly = field.readOnly || formReadOnly;
2568
- const isDisabled = disabled || resolveDisabled(field, formValues) || formReadOnly;
2621
+ const fieldFormReadOnly = field.alwaysEditable ? false : formReadOnly;
2622
+ const isReadOnly = field.readOnly || fieldFormReadOnly;
2623
+ const isDisabled = disabled || resolveDisabled(field, formValues) || fieldFormReadOnly;
2569
2624
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
2570
2625
  if (field.type === "display" || field.type === "slot") {
2571
2626
  if (field.render) {
@@ -2608,8 +2663,9 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2608
2663
  const sfValue = formValues[sf.name];
2609
2664
  const sfError = formErrors[sf.name] || null;
2610
2665
  const sfLabel = itemIdx === 0 ? sf.label : void 0;
2611
- const sfReadOnly = sf.readOnly || formReadOnly;
2612
- const sfDisabled = disabled || resolveDisabled(sf, formValues) || formReadOnly;
2666
+ const sfFormReadOnly = sf.alwaysEditable ? false : formReadOnly;
2667
+ const sfReadOnly = sf.readOnly || sfFormReadOnly;
2668
+ const sfDisabled = disabled || resolveDisabled(sf, formValues) || sfFormReadOnly;
2613
2669
  const sfOnChange = sf.debounce ? (v) => handleDebouncedFieldChange(sf.name, v) : (v) => handleFieldChange(sf.name, v);
2614
2670
  const sfProps = {
2615
2671
  name: sf.name,
@@ -3406,13 +3462,16 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
3406
3462
  const sectionContext = { values: formValues, errors: formErrors };
3407
3463
  const sectionOverrides = sec.columns ? { columns: sec.columns } : void 0;
3408
3464
  const accordionContent = /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "column", gap }, sec.renderBefore && sec.renderBefore(sectionContext), renderFieldSubset(sectionFields, sectionOverrides), sec.renderAfter && sec.renderAfter(sectionContext));
3465
+ const isValidationOverrideTarget = validationOpenSection && validationOpenSection.id === sec.id;
3466
+ const accordionKey = isValidationOverrideTarget ? `${sec.id}::open::${validationOpenSection.nonce}` : sec.id;
3467
+ const accordionDefaultOpen = isValidationOverrideTarget ? true : sec.defaultOpen !== false;
3409
3468
  const accordion = /* @__PURE__ */ import_react2.default.createElement(
3410
3469
  import_ui_extensions2.Accordion,
3411
3470
  {
3412
- key: sec.id,
3471
+ key: accordionKey,
3413
3472
  title: sec.label,
3414
3473
  size: "sm",
3415
- defaultOpen: sec.defaultOpen !== false
3474
+ defaultOpen: accordionDefaultOpen
3416
3475
  },
3417
3476
  accordionContent
3418
3477
  );
@@ -3446,6 +3505,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
3446
3505
  if (submitPosition === "none" || formReadOnly) return null;
3447
3506
  const isLastStep = !isMultiStep || currentStep === steps.length - 1;
3448
3507
  const isFirstStep = !isMultiStep || currentStep === 0;
3508
+ const singleStepJustify = submitAlign || (showCancel ? "between" : "start");
3449
3509
  const buttonContext = {
3450
3510
  isMultiStep,
3451
3511
  isFirstStep,
@@ -3480,7 +3540,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
3480
3540
  submitButtonLabel
3481
3541
  ) : /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "primary", onClick: handleNext, disabled }, nextButtonLabel)));
3482
3542
  }
3483
- return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ import_react2.default.createElement(
3543
+ return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "row", justify: singleStepJustify, gap: "sm" }, showCancel && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ import_react2.default.createElement(
3484
3544
  import_ui_extensions2.LoadingButton,
3485
3545
  {
3486
3546
  variant: submitVariant,
package/dist/index.mjs CHANGED
@@ -182,6 +182,8 @@ var DataTable = ({
182
182
  showFirstLastButtons,
183
183
  // show First/Last page buttons (default: auto when pageCount > 5)
184
184
  // Row count
185
+ title,
186
+ // optional title shown as demibold text above the table toolbar
185
187
  showRowCount = true,
186
188
  // show "X records" / "X of Y records" text
187
189
  rowCountBold = false,
@@ -651,6 +653,8 @@ var DataTable = ({
651
653
  selectionResetRef.current = combinedSelectionResetKey;
652
654
  }, [combinedSelectionResetKey, selectable, externalSelectedIds]);
653
655
  const selectedIds = externalSelectedIds != null ? new Set(externalSelectedIds) : internalSelectedIds;
656
+ const showToolbarCount = showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
657
+ const hasToolbarContent = showSearch && searchFields.length > 0 || filters.length > 0 || activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) || showToolbarCount;
654
658
  const showRowActionsColumn = !!rowActions && !(hideRowActionsWhenSelectionActive && selectable && selectedIds.size > 0);
655
659
  const applySelection = useCallback((nextSet) => {
656
660
  if (externalSelectedIds == null) {
@@ -1000,7 +1004,7 @@ var DataTable = ({
1000
1004
  }
1001
1005
  );
1002
1006
  };
1003
- 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(
1007
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, title && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", justify: "between", gap: "sm" }, /* @__PURE__ */ React.createElement(Text, { format: { fontWeight: "demibold" } }, title)), hasToolbarContent && /* @__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(
1004
1008
  SearchInput,
1005
1009
  {
1006
1010
  name: "datatable-search",
@@ -1026,7 +1030,7 @@ var DataTable = ({
1026
1030
  onClick: () => handleFilterRemove("all")
1027
1031
  },
1028
1032
  resolvedClearAllLabel
1029
- )))), 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({
1033
+ )))), showToolbarCount && /* @__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({
1030
1034
  selectedIds,
1031
1035
  selectedCount: selectedIds.size,
1032
1036
  displayCount,
@@ -1519,8 +1523,12 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1519
1523
  // validate on blur
1520
1524
  validateOnSubmit = true,
1521
1525
  // validate all before onSubmit
1522
- onValidationChange
1526
+ onValidationChange,
1523
1527
  // (errors) => void
1528
+ onValidationFail,
1529
+ // ({ errors, fields, firstInvalidField }) => void — called when submit-time validation blocks submission
1530
+ openSectionOnValidationFail = false
1531
+ // auto-open accordion section containing first invalid field on submit failure
1524
1532
  } = props;
1525
1533
  const {
1526
1534
  steps,
@@ -1545,6 +1553,8 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1545
1553
  // () => void
1546
1554
  submitPosition = "bottom",
1547
1555
  // "bottom" | "none"
1556
+ submitAlign,
1557
+ // default single-step action row alignment
1548
1558
  loading: controlledLoading,
1549
1559
  // controlled loading state
1550
1560
  disabled = false,
@@ -1667,6 +1677,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1667
1677
  "tooltip",
1668
1678
  "required",
1669
1679
  "readOnly",
1680
+ "alwaysEditable",
1670
1681
  "disabled",
1671
1682
  "defaultValue",
1672
1683
  "fieldProps",
@@ -1839,6 +1850,17 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1839
1850
  }
1840
1851
  return map;
1841
1852
  }, [fields]);
1853
+ const sectionIdByFieldName = useMemo2(() => {
1854
+ const map = /* @__PURE__ */ new Map();
1855
+ if (Array.isArray(sections)) {
1856
+ for (const sec of sections) {
1857
+ if (!sec || !Array.isArray(sec.fields)) continue;
1858
+ for (const name of sec.fields) map.set(name, sec.id);
1859
+ }
1860
+ }
1861
+ return map;
1862
+ }, [sections]);
1863
+ const [validationOpenSection, setValidationOpenSection] = useState2(null);
1842
1864
  const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
1843
1865
  const configWarningsRef = useRef2(/* @__PURE__ */ new Set());
1844
1866
  const warnConfig = useCallback2((message) => {
@@ -1849,6 +1871,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1849
1871
  console.warn(`[FormBuilder] ${message}`);
1850
1872
  }
1851
1873
  }, [isDev]);
1874
+ useEffect2(() => {
1875
+ if (!isMultiStep || !submitAlign) return;
1876
+ warnConfig("submitAlign is ignored when steps are provided. Use renderButtons for custom multi-step button layout.");
1877
+ }, [isMultiStep, submitAlign, warnConfig]);
1852
1878
  const replaceErrors = useCallback2(
1853
1879
  (nextErrors) => {
1854
1880
  if (controlledErrors == null) setInternalErrors(nextErrors);
@@ -2415,10 +2441,35 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2415
2441
  const handleSubmit = useCallback2(
2416
2442
  async (e) => {
2417
2443
  if (e && e.preventDefault) e.preventDefault();
2444
+ const reportValidationFailure = (errors) => {
2445
+ const errorNames = Object.keys(errors).filter((n) => !!errors[n]);
2446
+ if (errorNames.length === 0) return;
2447
+ const orderedNames = allVisibleFields.map((f) => f.name).filter((n) => errorNames.includes(n));
2448
+ for (const n of errorNames) if (!orderedNames.includes(n)) orderedNames.push(n);
2449
+ const fieldInfos = orderedNames.map((name) => {
2450
+ const f = fieldByName.get(name);
2451
+ return {
2452
+ name,
2453
+ label: f == null ? void 0 : f.label,
2454
+ sectionId: sectionIdByFieldName.get(name)
2455
+ };
2456
+ });
2457
+ const firstInvalidField = fieldInfos[0];
2458
+ if (openSectionOnValidationFail && (firstInvalidField == null ? void 0 : firstInvalidField.sectionId)) {
2459
+ setValidationOpenSection({
2460
+ id: firstInvalidField.sectionId,
2461
+ nonce: ((validationOpenSection == null ? void 0 : validationOpenSection.nonce) || 0) + 1
2462
+ });
2463
+ }
2464
+ if (onValidationFail) {
2465
+ onValidationFail({ errors, fields: fieldInfos, firstInvalidField });
2466
+ }
2467
+ };
2418
2468
  if (validateOnSubmit) {
2419
2469
  const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
2420
2470
  if (hasErrors) {
2421
2471
  replaceErrors(errors);
2472
+ reportValidationFailure(errors);
2422
2473
  return;
2423
2474
  }
2424
2475
  const asyncSubmitValidations = getAsyncValidationTargets(allVisibleFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
@@ -2430,7 +2481,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2430
2481
  ])
2431
2482
  ];
2432
2483
  await Promise.all(pendingValidations);
2433
- if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) return;
2484
+ if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) {
2485
+ reportValidationFailure(formErrorsRef.current);
2486
+ return;
2487
+ }
2434
2488
  }
2435
2489
  }
2436
2490
  const reset = () => {
@@ -2476,7 +2530,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2476
2530
  if (controlledLoading == null) setInternalLoading(false);
2477
2531
  }
2478
2532
  },
2479
- [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget]
2533
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget, onValidationFail, openSectionOnValidationFail, sectionIdByFieldName, validationOpenSection]
2480
2534
  );
2481
2535
  const handleNext = useCallback2(async () => {
2482
2536
  if (!isMultiStep) return;
@@ -2569,8 +2623,9 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2569
2623
  const fieldError = formErrors[field.name] || null;
2570
2624
  const hasError = !!fieldError;
2571
2625
  const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
2572
- const isReadOnly = field.readOnly || formReadOnly;
2573
- const isDisabled = disabled || resolveDisabled(field, formValues) || formReadOnly;
2626
+ const fieldFormReadOnly = field.alwaysEditable ? false : formReadOnly;
2627
+ const isReadOnly = field.readOnly || fieldFormReadOnly;
2628
+ const isDisabled = disabled || resolveDisabled(field, formValues) || fieldFormReadOnly;
2574
2629
  const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
2575
2630
  if (field.type === "display" || field.type === "slot") {
2576
2631
  if (field.render) {
@@ -2613,8 +2668,9 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2613
2668
  const sfValue = formValues[sf.name];
2614
2669
  const sfError = formErrors[sf.name] || null;
2615
2670
  const sfLabel = itemIdx === 0 ? sf.label : void 0;
2616
- const sfReadOnly = sf.readOnly || formReadOnly;
2617
- const sfDisabled = disabled || resolveDisabled(sf, formValues) || formReadOnly;
2671
+ const sfFormReadOnly = sf.alwaysEditable ? false : formReadOnly;
2672
+ const sfReadOnly = sf.readOnly || sfFormReadOnly;
2673
+ const sfDisabled = disabled || resolveDisabled(sf, formValues) || sfFormReadOnly;
2618
2674
  const sfOnChange = sf.debounce ? (v) => handleDebouncedFieldChange(sf.name, v) : (v) => handleFieldChange(sf.name, v);
2619
2675
  const sfProps = {
2620
2676
  name: sf.name,
@@ -3411,13 +3467,16 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
3411
3467
  const sectionContext = { values: formValues, errors: formErrors };
3412
3468
  const sectionOverrides = sec.columns ? { columns: sec.columns } : void 0;
3413
3469
  const accordionContent = /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap }, sec.renderBefore && sec.renderBefore(sectionContext), renderFieldSubset(sectionFields, sectionOverrides), sec.renderAfter && sec.renderAfter(sectionContext));
3470
+ const isValidationOverrideTarget = validationOpenSection && validationOpenSection.id === sec.id;
3471
+ const accordionKey = isValidationOverrideTarget ? `${sec.id}::open::${validationOpenSection.nonce}` : sec.id;
3472
+ const accordionDefaultOpen = isValidationOverrideTarget ? true : sec.defaultOpen !== false;
3414
3473
  const accordion = /* @__PURE__ */ React2.createElement(
3415
3474
  Accordion,
3416
3475
  {
3417
- key: sec.id,
3476
+ key: accordionKey,
3418
3477
  title: sec.label,
3419
3478
  size: "sm",
3420
- defaultOpen: sec.defaultOpen !== false
3479
+ defaultOpen: accordionDefaultOpen
3421
3480
  },
3422
3481
  accordionContent
3423
3482
  );
@@ -3451,6 +3510,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
3451
3510
  if (submitPosition === "none" || formReadOnly) return null;
3452
3511
  const isLastStep = !isMultiStep || currentStep === steps.length - 1;
3453
3512
  const isFirstStep = !isMultiStep || currentStep === 0;
3513
+ const singleStepJustify = submitAlign || (showCancel ? "between" : "start");
3454
3514
  const buttonContext = {
3455
3515
  isMultiStep,
3456
3516
  isFirstStep,
@@ -3485,7 +3545,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
3485
3545
  submitButtonLabel
3486
3546
  ) : /* @__PURE__ */ React2.createElement(Button2, { variant: "primary", onClick: handleNext, disabled }, nextButtonLabel)));
3487
3547
  }
3488
- return /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ React2.createElement(Button2, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ React2.createElement(
3548
+ return /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", justify: singleStepJustify, gap: "sm" }, showCancel && /* @__PURE__ */ React2.createElement(Button2, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ React2.createElement(
3489
3549
  LoadingButton,
3490
3550
  {
3491
3551
  variant: submitVariant,
package/form.d.ts CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  FormBuilderLabels,
18
18
  FormBuilderAlertConfig,
19
19
  FormBuilderButtonsRenderContext,
20
+ FormBuilderSubmitAlign,
20
21
  FormBuilderLayout,
21
22
  FormBuilderLayoutEntry,
22
23
  FormBuilderSection,
package/index.d.ts CHANGED
@@ -75,6 +75,7 @@ export type {
75
75
  FormBuilderLabels,
76
76
  FormBuilderAlertConfig,
77
77
  FormBuilderButtonsRenderContext,
78
+ FormBuilderSubmitAlign,
78
79
  FormBuilderLayout,
79
80
  FormBuilderLayoutEntry,
80
81
  FormBuilderSection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hs-uix",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "Production-ready UI components for HubSpot UI Extensions — DataTable, FormBuilder, and more",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",