@qoretechnologies/reqraft 0.10.2 → 0.10.4

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.
Files changed (71) hide show
  1. package/design/COMPACT_ENGINE_REDESIGN.md +156 -0
  2. package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +353 -0
  3. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  4. package/dist/components/form/engine/CompactRow.js +153 -94
  5. package/dist/components/form/engine/CompactRow.js.map +1 -1
  6. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  7. package/dist/components/form/engine/CompactToolbar.js +130 -94
  8. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  9. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  10. package/dist/components/form/engine/FormEngine.js +181 -45
  11. package/dist/components/form/engine/FormEngine.js.map +1 -1
  12. package/dist/components/form/engine/compactRowStyles.d.ts +6 -3
  13. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  14. package/dist/components/form/engine/compactRowStyles.js +70 -48
  15. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  16. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  17. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  18. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  19. package/dist/components/form/engine/readFirst.d.ts +19 -0
  20. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  21. package/dist/components/form/engine/readFirst.js +22 -1
  22. package/dist/components/form/engine/readFirst.js.map +1 -1
  23. package/dist/components/form/engine/variants/VariantCalmTable.d.ts +6 -0
  24. package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -0
  25. package/dist/components/form/engine/variants/VariantCalmTable.js +94 -0
  26. package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -0
  27. package/dist/components/form/engine/variants/VariantCards.d.ts +6 -0
  28. package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -0
  29. package/dist/components/form/engine/variants/VariantCards.js +80 -0
  30. package/dist/components/form/engine/variants/VariantCards.js.map +1 -0
  31. package/dist/components/form/engine/variants/VariantFocus.d.ts +7 -0
  32. package/dist/components/form/engine/variants/VariantFocus.d.ts.map +1 -0
  33. package/dist/components/form/engine/variants/VariantFocus.js +138 -0
  34. package/dist/components/form/engine/variants/VariantFocus.js.map +1 -0
  35. package/dist/components/form/engine/variants/VariantMinimal.d.ts +6 -0
  36. package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -0
  37. package/dist/components/form/engine/variants/VariantMinimal.js +73 -0
  38. package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -0
  39. package/dist/components/form/engine/variants/focusDemo.d.ts +13 -0
  40. package/dist/components/form/engine/variants/focusDemo.d.ts.map +1 -0
  41. package/dist/components/form/engine/variants/focusDemo.js +139 -0
  42. package/dist/components/form/engine/variants/focusDemo.js.map +1 -0
  43. package/dist/components/form/engine/variants/variantModel.d.ts +70 -0
  44. package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -0
  45. package/dist/components/form/engine/variants/variantModel.js +133 -0
  46. package/dist/components/form/engine/variants/variantModel.js.map +1 -0
  47. package/dist/components/form/engine/variants/variantParts.d.ts +79 -0
  48. package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -0
  49. package/dist/components/form/engine/variants/variantParts.js +191 -0
  50. package/dist/components/form/engine/variants/variantParts.js.map +1 -0
  51. package/dist/components/form/fields/auto/AutoFormField.d.ts +3 -0
  52. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  53. package/dist/components/form/fields/auto/AutoFormField.js +2 -2
  54. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/components/form/engine/CompactRow.tsx +256 -234
  57. package/src/components/form/engine/CompactToolbar.tsx +108 -68
  58. package/src/components/form/engine/FormEngine.stories.tsx +127 -110
  59. package/src/components/form/engine/FormEngine.tsx +248 -67
  60. package/src/components/form/engine/compactRowStyles.ts +207 -134
  61. package/src/components/form/engine/compactToolbarContext.ts +1 -0
  62. package/src/components/form/engine/readFirst.ts +35 -0
  63. package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +119 -0
  64. package/src/components/form/engine/variants/VariantCalmTable.tsx +242 -0
  65. package/src/components/form/engine/variants/VariantCards.tsx +212 -0
  66. package/src/components/form/engine/variants/VariantFocus.tsx +382 -0
  67. package/src/components/form/engine/variants/VariantMinimal.tsx +170 -0
  68. package/src/components/form/engine/variants/focusDemo.ts +145 -0
  69. package/src/components/form/engine/variants/variantModel.ts +216 -0
  70. package/src/components/form/engine/variants/variantParts.tsx +313 -0
  71. package/src/components/form/fields/auto/AutoFormField.tsx +5 -0
@@ -322,6 +322,10 @@ var FormEngine = function (_a) {
322
322
  var _x = (0, react_2.useState)(false), showInvalidOptionsOnly = _x[0], setShowInvalidOptionsOnly = _x[1];
323
323
  // Which options are expanded into their editor (several can be open at once).
324
324
  var _y = (0, react_2.useState)([]), expandedOptions = _y[0], setExpandedOptions = _y[1];
325
+ // Remembers each row's last settled status box, so an actively-edited field
326
+ // stays put when its status flips (e.g. becomes valid) instead of jumping to
327
+ // another box mid-edit and stealing focus. Keyed by option name.
328
+ var settledBucket = (0, react_2.useRef)({});
325
329
  // Measured form width (not viewport — the form lives in drawers/panels of
326
330
  // arbitrary width) drives the stacked narrow layout.
327
331
  var _z = (0, react_use_1.useMeasure)(), compactWrapRef = _z[0], compactWrapWidth = _z[1].width;
@@ -889,6 +893,67 @@ var FormEngine = function (_a) {
889
893
  return __assign(__assign({}, newValue), (_a = {}, _a[optionName] = option, _a));
890
894
  }, {});
891
895
  }, [showInvalidOptionsOnly, JSON.stringify(availableOptions)]);
896
+ // Read-first STATUS / BOX for one option — lifted to component scope so the
897
+ // status boxes (renderCompact) and the header's "needs attention" count share
898
+ // exactly one definition. One-of group members travel together (bucket by the
899
+ // group's satisfaction); everything else by its own status.
900
+ var schemaMsgIntent = (0, react_2.useCallback)(function (name) {
901
+ var _a;
902
+ var msgs = (((_a = options === null || options === void 0 ? void 0 : options[name]) === null || _a === void 0 ? void 0 : _a.messages) || []);
903
+ if (msgs.some(function (m) { return m.intent === 'danger'; }))
904
+ return 'danger';
905
+ if (msgs.some(function (m) { return m.intent === 'warning'; }))
906
+ return 'warning';
907
+ return undefined;
908
+ }, [JSON.stringify(options)]);
909
+ var getOptionStatus = (0, react_2.useCallback)(function (name, hidden) {
910
+ var _a;
911
+ if (hidden === void 0) { hidden = false; }
912
+ if (hidden)
913
+ return 'optional';
914
+ var schema = options === null || options === void 0 ? void 0 : options[name];
915
+ var type = ((schema === null || schema === void 0 ? void 0 : schema.ui_type) || (schema === null || schema === void 0 ? void 0 : schema.type));
916
+ var value = (_a = availableOptions === null || availableOptions === void 0 ? void 0 : availableOptions[name]) === null || _a === void 0 ? void 0 : _a.value;
917
+ var empty = (0, readFirst_1.isOptionValueEmpty)(value);
918
+ var reqGroups = (schema === null || schema === void 0 ? void 0 : schema.required_groups) || [];
919
+ var required = !!((schema === null || schema === void 0 ? void 0 : schema.required) || reqGroups.length);
920
+ var covered = empty &&
921
+ reqGroups.some(function (g) {
922
+ var by = requiredGroupsInfo.satisfiedBy[g];
923
+ return !!by && by !== name;
924
+ });
925
+ var msgIntent = schemaMsgIntent(name);
926
+ var invalid = (!empty && !isOptionValid(name, type, value)) || msgIntent === 'danger';
927
+ return (0, readFirst_1.getReadFirstStatus)({
928
+ empty: empty,
929
+ required: required,
930
+ covered: covered,
931
+ invalid: invalid,
932
+ warned: msgIntent === 'warning',
933
+ });
934
+ }, [
935
+ JSON.stringify(options),
936
+ JSON.stringify(availableOptions),
937
+ isOptionValid,
938
+ requiredGroupsInfo,
939
+ schemaMsgIntent,
940
+ ]);
941
+ var getOptionBucket = (0, react_2.useCallback)(function (name, hidden) {
942
+ var _a;
943
+ if (hidden === void 0) { hidden = false; }
944
+ if (!hidden) {
945
+ var reqGroups = ((_a = options === null || options === void 0 ? void 0 : options[name]) === null || _a === void 0 ? void 0 : _a.required_groups) || [];
946
+ if (reqGroups.length) {
947
+ return reqGroups.some(function (g) { return !requiredGroupsInfo.satisfiedBy[g]; }) ? 'attention' : 'set';
948
+ }
949
+ }
950
+ return (0, readFirst_1.getReadFirstBucket)(getOptionStatus(name, hidden));
951
+ }, [JSON.stringify(options), requiredGroupsInfo, getOptionStatus]);
952
+ // How many fields are in the "Needs attention" box — drives the header link.
953
+ var readFirstAttentionCount = (0, react_2.useMemo)(function () {
954
+ return Object.keys(availableOptions || {}).filter(function (name) { return getOptionBucket(name) === 'attention'; })
955
+ .length;
956
+ }, [JSON.stringify(availableOptions), getOptionBucket]);
892
957
  var getIntent = (0, react_2.useCallback)(
893
958
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
894
959
  function (optName, type, optValue, _op) {
@@ -1011,13 +1076,27 @@ var FormEngine = function (_a) {
1011
1076
  // The info panel below the row keeps showing schema messages while editing —
1012
1077
  // rendering them in the editor too would balloon a one-line edit.
1013
1078
  suppressSchemaMessages) {
1014
- var _b, _c, _d, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1079
+ var _b, _c, _d, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1015
1080
  var type = _a.type, other = __rest(_a, ["type"]);
1016
1081
  var operatorParts = (0, exports.fixOperatorValue)(other.op);
1017
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(suppressSchemaMessages ? [] : ((_b = options === null || options === void 0 ? void 0 : options[optionName]) === null || _b === void 0 ? void 0 : _b.messages) || []).map(function (_a, index) {
1018
- var intent = _a.intent, title = _a.title, content = _a.content;
1019
- return ((0, jsx_runtime_1.jsx)(reqore_1.ReqoreMessage, { intent: intent, title: title, opaque: false, size: 'small', margin: 'bottom', children: content }, title || index));
1020
- }), operators && (0, size_1.default)(operators) ?
1082
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(function () {
1083
+ var _a;
1084
+ var schemaMsgs = (suppressSchemaMessages ? [] : ((_a = options === null || options === void 0 ? void 0 : options[optionName]) === null || _a === void 0 ? void 0 : _a.messages) || []);
1085
+ if (!schemaMsgs.length)
1086
+ return null;
1087
+ var items = schemaMsgs.map(function (_a, index) {
1088
+ var intent = _a.intent, title = _a.title, content = _a.content;
1089
+ return ((0, jsx_runtime_1.jsx)(reqore_1.ReqoreMessage, { intent: intent, title: title, opaque: false, size: 'small',
1090
+ // Compact: flat (no border) to match the read-row info panels;
1091
+ // classic forms keep the bordered, bottom-margined message.
1092
+ flat: compact || undefined, margin: compact ? undefined : 'bottom', children: content }, title || index));
1093
+ });
1094
+ // Compact: stack them in a 4px-gap panel so a field's messages look
1095
+ // identical whether the row is collapsed (read panel) or expanded.
1096
+ return compact ?
1097
+ (0, jsx_runtime_1.jsx)("div", { className: 'options-readfirst-info-panel', style: { display: 'flex', flexFlow: 'column', gap: 4, marginBottom: 8 }, children: items })
1098
+ : (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: items });
1099
+ })(), operators && (0, size_1.default)(operators) ?
1021
1100
  (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreControlGroup, { fill: true, wrap: true, className: 'operators', children: operatorParts.map(function (operator, index) {
1022
1101
  var _a, _b;
1023
1102
  return ((0, jsx_runtime_1.jsxs)(react_2.default.Fragment, { children: [(0, jsx_runtime_1.jsx)(Select_1.SelectFormField, { items: (0, map_1.default)(operators, function (op) { return (__assign(__assign({}, op), { value: op.name })); }), disabled: readOnly, value: operator && "".concat((_a = operators === null || operators === void 0 ? void 0 : operators[operator]) === null || _a === void 0 ? void 0 : _a.name), onChange: function (val) {
@@ -1033,18 +1112,21 @@ var FormEngine = function (_a) {
1033
1112
  : null] }, index));
1034
1113
  }) }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreVerticalSpacer, { height: 5 })] })
1035
1114
  : null, (0, react_1.createElement)(TemplateField_1.TemplateField, __assign({ fluid: true }, options === null || options === void 0 ? void 0 : options[optionName], {
1115
+ // Propagate compact so an arg_schema field renders a COMPACT sub-form
1116
+ // (consistent with the parent) rather than the classic FormEngine.
1117
+ compact: compact,
1036
1118
  // SEAM: forwarded through TemplateField's rest-spread to AutoFormField,
1037
1119
  // which renders consumer-injected editors by field type/ui_type.
1038
- componentOverrides: componentOverrides, allowTemplates: !!(allowTemplates && ((_c = options === null || options === void 0 ? void 0 : options[optionName]) === null || _c === void 0 ? void 0 : _c.supports_templates)), allowFunctions: !!((_d = options === null || options === void 0 ? void 0 : options[optionName]) === null || _d === void 0 ? void 0 : _d.supports_expressions),
1120
+ componentOverrides: componentOverrides, allowTemplates: !!(allowTemplates && ((_b = options === null || options === void 0 ? void 0 : options[optionName]) === null || _b === void 0 ? void 0 : _b.supports_templates)), allowFunctions: !!((_c = options === null || options === void 0 ? void 0 : options[optionName]) === null || _c === void 0 ? void 0 : _c.supports_expressions),
1039
1121
  // reqraft: form-level expression fields get the Visual/Text shell
1040
1122
  // (DPQL text mode); opt out per-form via `templateFieldProps`.
1041
- allowTextExpressions: true, allowCustomValues: ((_f = options === null || options === void 0 ? void 0 : options[optionName]) === null || _f === void 0 ? void 0 : _f.supports_custom_values) !== false && type !== 'any', templates: templates.value }, (0, exports.getTypeAndCanBeNull)(type, (_g = options === null || options === void 0 ? void 0 : options[optionName]) === null || _g === void 0 ? void 0 : _g.allowed_values, other.op), { ui_type: type, name: optionName, uniqueName: "".concat(uniqueName ? "".concat(uniqueName, ".") : "".concat(name ? "".concat(name, ".") : '')).concat(optionName), onChange:
1123
+ allowTextExpressions: true, allowCustomValues: ((_d = options === null || options === void 0 ? void 0 : options[optionName]) === null || _d === void 0 ? void 0 : _d.supports_custom_values) !== false && type !== 'any', templates: templates.value }, (0, exports.getTypeAndCanBeNull)(type, (_f = options === null || options === void 0 ? void 0 : options[optionName]) === null || _f === void 0 ? void 0 : _f.allowed_values, other.op), { ui_type: type, name: optionName, uniqueName: "".concat(uniqueName ? "".concat(uniqueName, ".") : "".concat(name ? "".concat(name, ".") : '')).concat(optionName), onChange:
1042
1124
  // Identity-stable on purpose: the typed fields debounce on
1043
1125
  // `[localValue, onChange]` — an inline lambda resets the pending emit
1044
1126
  // every render and the typed value can starve.
1045
- handleValueChange, key: optionName, arg_schema: (_h = options === null || options === void 0 ? void 0 : options[optionName]) === null || _h === void 0 ? void 0 : _h.arg_schema, noSoft: !!(rest === null || rest === void 0 ? void 0 : rest.options), value: other.value, isFunction: other.is_expression, isDefaultFunction: ((_j = options === null || options === void 0 ? void 0 : options[optionName]) === null || _j === void 0 ? void 0 : _j.default_view) === 'expression', sensitive: (_k = options === null || options === void 0 ? void 0 : options[optionName]) === null || _k === void 0 ? void 0 : _k.sensitive, default_value: (0, common_1.getDefaultValue)(options === null || options === void 0 ? void 0 : options[optionName]), isDefaultTemplate: ((_l = options === null || options === void 0 ? void 0 : options[optionName]) === null || _l === void 0 ? void 0 : _l.default_view) === 'template', allowed_values: (_m = options === null || options === void 0 ? void 0 : options[optionName]) === null || _m === void 0 ? void 0 : _m.allowed_values, disabled: ((_o = options === null || options === void 0 ? void 0 : options[optionName]) === null || _o === void 0 ? void 0 : _o.disabled) ||
1127
+ handleValueChange, key: optionName, arg_schema: (_g = options === null || options === void 0 ? void 0 : options[optionName]) === null || _g === void 0 ? void 0 : _g.arg_schema, noSoft: !!(rest === null || rest === void 0 ? void 0 : rest.options), value: other.value, isFunction: other.is_expression, isDefaultFunction: ((_h = options === null || options === void 0 ? void 0 : options[optionName]) === null || _h === void 0 ? void 0 : _h.default_view) === 'expression', sensitive: (_j = options === null || options === void 0 ? void 0 : options[optionName]) === null || _j === void 0 ? void 0 : _j.sensitive, default_value: (0, common_1.getDefaultValue)(options === null || options === void 0 ? void 0 : options[optionName]), isDefaultTemplate: ((_k = options === null || options === void 0 ? void 0 : options[optionName]) === null || _k === void 0 ? void 0 : _k.default_view) === 'template', allowed_values: (_l = options === null || options === void 0 ? void 0 : options[optionName]) === null || _l === void 0 ? void 0 : _l.allowed_values, disabled: ((_m = options === null || options === void 0 ? void 0 : options[optionName]) === null || _m === void 0 ? void 0 : _m.disabled) ||
1046
1128
  readOnly ||
1047
- !(0, validations_1.hasAllDependenciesFullfilled)((_p = options === null || options === void 0 ? void 0 : options[optionName]) === null || _p === void 0 ? void 0 : _p.depends_on, availableOptions, options || {}), readOnly: readOnly, size: editorSize || rest.size, menuItems: ((_q = options === null || options === void 0 ? void 0 : options[optionName]) === null || _q === void 0 ? void 0 : _q.ui_type) === 'any' ?
1129
+ !(0, validations_1.hasAllDependenciesFullfilled)((_o = options === null || options === void 0 ? void 0 : options[optionName]) === null || _o === void 0 ? void 0 : _o.depends_on, availableOptions, options || {}), readOnly: readOnly, size: editorSize || rest.size, menuItems: ((_p = options === null || options === void 0 ? void 0 : options[optionName]) === null || _p === void 0 ? void 0 : _p.ui_type) === 'any' ?
1048
1130
  getCustomMenuTemplateItems(optionName)
1049
1131
  : undefined }, templateFieldProps)), (0, jsx_runtime_1.jsx)(OptionFieldMessages_1.OptionFieldMessages, { schema: options || {}, allOptions: availableOptions, name: optionName, option: __assign({ type: type }, other), getType: getTypeForOption }), operators && (0, size_1.default)(operators) && (0, size_1.default)(other.op) ?
1050
1132
  (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreVerticalSpacer, { height: 5 }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreMessage, { size: 'small', children: (0, jsx_runtime_1.jsxs)(reqore_1.ReqoreTagGroup, { children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreTag, { size: 'small', labelKey: 'WHERE', label: optionName }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreTag, { size: 'small', labelKey: 'IS', label: operatorParts.join(' ') }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreTag, { size: 'small', intent: 'info', label: other.value ?
@@ -1187,6 +1269,7 @@ var FormEngine = function (_a) {
1187
1269
  var compactToolbarContextValue = (0, react_2.useMemo)(function () { return ({
1188
1270
  readOnly: readOnly,
1189
1271
  invalidCount: (0, size_1.default)(validityData.invalidFields),
1272
+ attentionCount: readFirstAttentionCount,
1190
1273
  completion: readFirstCompletion,
1191
1274
  showInvalidOnly: showInvalidOptionsOnly,
1192
1275
  onToggleInvalidOnly: handleToggleInvalidOnly,
@@ -1211,6 +1294,7 @@ var FormEngine = function (_a) {
1211
1294
  }); }, [
1212
1295
  readOnly,
1213
1296
  validityData,
1297
+ readFirstAttentionCount,
1214
1298
  readFirstCompletion,
1215
1299
  showInvalidOptionsOnly,
1216
1300
  handleToggleInvalidOnly,
@@ -1317,6 +1401,57 @@ var FormEngine = function (_a) {
1317
1401
  return 1;
1318
1402
  return groupOrder.indexOf(a) - groupOrder.indexOf(b);
1319
1403
  });
1404
+ var buckets = {
1405
+ attention: {},
1406
+ set: {},
1407
+ optional: {},
1408
+ };
1409
+ var bucketGroups = { attention: [], set: [], optional: [] };
1410
+ // Freeze the box of any field currently being edited (or whose one-of group
1411
+ // has an edited member) to its last settled box — so finishing an edit that
1412
+ // flips its status doesn't remount it in another box and steal focus.
1413
+ var stableBucketOf = function (entry) {
1414
+ var _a;
1415
+ var fresh = getOptionBucket(entry.name, entry.hidden);
1416
+ var groupBeingEdited = !entry.hidden &&
1417
+ (((_a = options === null || options === void 0 ? void 0 : options[entry.name]) === null || _a === void 0 ? void 0 : _a.required_groups) || []).some(function (g) {
1418
+ return (requiredGroupsInfo.members[g] || []).some(function (m) { return expandedOptions.includes(m); });
1419
+ });
1420
+ if (!entry.hidden && (expandedOptions.includes(entry.name) || groupBeingEdited)) {
1421
+ var memo = settledBucket.current[entry.name];
1422
+ if (memo)
1423
+ return memo;
1424
+ }
1425
+ settledBucket.current[entry.name] = fresh;
1426
+ return fresh;
1427
+ };
1428
+ groupKeys.forEach(function (groupName) {
1429
+ grouped[groupName].forEach(function (entry) {
1430
+ var b = stableBucketOf(entry);
1431
+ if (!buckets[b][groupName]) {
1432
+ buckets[b][groupName] = [];
1433
+ bucketGroups[b].push(groupName);
1434
+ }
1435
+ buckets[b][groupName].push(entry);
1436
+ });
1437
+ });
1438
+ var bucketCount = function (b) {
1439
+ return bucketGroups[b].reduce(function (n, g) { return n + buckets[b][g].length; }, 0);
1440
+ };
1441
+ // 'general' / 'optional' are the SYNTHETIC fallback group keys getOptionGroup
1442
+ // assigns to fields with no explicit `group` — printing a "General"/"Optional"
1443
+ // sub-label for those is just noise, so suppress it. BUT a consumer may also
1444
+ // use 'general' as a REAL group (defining it in the `groups` prop and tagging
1445
+ // fields with `group: 'general'`); in that case it's a named group like any
1446
+ // other and DOES get its sub-label.
1447
+ var showGroupSubLabel = function (groupName) {
1448
+ return (groupName !== 'general' && groupName !== 'optional') || !!(groups === null || groups === void 0 ? void 0 : groups[groupName]);
1449
+ };
1450
+ var STATUS_BOXES = [
1451
+ { key: 'attention', label: 'Needs attention', intent: 'warning', icon: 'ErrorWarningLine' },
1452
+ { key: 'set', label: 'Set', intent: 'success', icon: 'CheckLine' },
1453
+ { key: 'optional', label: 'Optional', icon: 'CheckboxBlankCircleLine' },
1454
+ ];
1320
1455
  // Build the rows for one group: contiguous required-group members are pulled
1321
1456
  // together at the first member's slot and rendered as a connected rail (flat
1322
1457
  // rows — no wrapper — so the value surface applies normally; the rail + nodes
@@ -1331,8 +1466,9 @@ var FormEngine = function (_a) {
1331
1466
  }
1332
1467
  : shownOptions[entry.name], hidden: entry.hidden, clustered: clustered, clusterFirst: clusterFirst, clusterLast: clusterLast }, entry.name));
1333
1468
  };
1334
- if (compactNarrow)
1335
- return names.map(function (entry) { return renderRow(entry, false); });
1469
+ // (Clustering runs in narrow mode too now — the "One of the below is
1470
+ // required" box wraps the members regardless of width; it no longer relies
1471
+ // on a contiguous rail.)
1336
1472
  var emitted = new Set();
1337
1473
  var groupOf = function (name) { var _a, _b; return (_b = (_a = options === null || options === void 0 ? void 0 : options[name]) === null || _a === void 0 ? void 0 : _a.required_groups) === null || _b === void 0 ? void 0 : _b[0]; };
1338
1474
  return names.map(function (entry) {
@@ -1345,9 +1481,15 @@ var FormEngine = function (_a) {
1345
1481
  if (memberEntries.length < 2)
1346
1482
  return renderRow(entry, false);
1347
1483
  emitted.add(grp);
1348
- return memberEntries.map(function (e, idx) {
1484
+ var railed = memberEntries.map(function (e, idx) {
1349
1485
  return renderRow(e, true, idx === 0, idx === memberEntries.length - 1);
1350
1486
  });
1487
+ // An UNMET one-of group gets the explicit "One of the below is required"
1488
+ // box (the Focus cluster). A met group needs no banner — the rail + the
1489
+ // "Covers"/"Covered by" chips already say which member satisfies it.
1490
+ if (requiredGroupsInfo.satisfiedBy[grp])
1491
+ return railed;
1492
+ return ((0, jsx_runtime_1.jsxs)(compactRowStyles_1.StyledRequiredClusterBox, { className: 'options-readfirst-required-cluster', "$border": "".concat(cWarning, "33"), "$tint": "".concat(cWarning, "0d"), children: [(0, jsx_runtime_1.jsxs)(compactRowStyles_1.StyledRequiredClusterHeader, { "$color": cWarning, children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreIcon, { icon: 'LinkM', size: '11px', style: { color: cWarning } }), "One of the below is required"] }), railed] }, grp));
1351
1493
  });
1352
1494
  };
1353
1495
  return ((0, jsx_runtime_1.jsx)(exports.OptionsContext.Provider, { value: { schema: options, value: availableOptions }, children: (0, jsx_runtime_1.jsx)(compactRowContext_1.CompactRowContext.Provider, { value: compactRowContextValue, children: (0, jsx_runtime_1.jsxs)(reqore_1.ReqoreErrorBoundary, { children: [showHelpForOption && ((0, jsx_runtime_1.jsx)(OptionsHelpDialog_1.OptionsHelpDialog, { onClose: function () { return setShowHelpForOption(undefined); }, option: options[showHelpForOption] })), (0, jsx_runtime_1.jsx)(compactToolbarContext_1.CompactToolbarContext.Provider, { value: compactToolbarContextValue, children: (0, jsx_runtime_1.jsxs)(StyledCompactWrap, { ref: setCompactWrap, className: 'options-readfirst-scroll', "$flush": compactFlush, children: [(0, jsx_runtime_1.jsxs)(compactRowStyles_1.StyledCompactPanel, { "$headerBg": headerBg, flat: true, stickyHeader: true, padded: false, actions: compactHeaderActions, contentStyle: {
@@ -1357,39 +1499,33 @@ var FormEngine = function (_a) {
1357
1499
  padding: '0 0 12px',
1358
1500
  }, children: [(0, size_1.default)(groupKeys) === 0 ?
1359
1501
  (0, jsx_runtime_1.jsx)(reqore_1.ReqoreMessage, { flat: true, opaque: false, size: 'small', children: "No fields match the current filters." })
1360
- : null, groupKeys.map(function (groupName) {
1361
- var names = grouped[groupName];
1362
- var groupConfig = groups === null || groups === void 0 ? void 0 : groups[groupName];
1363
- var invalidCount = names.filter(function (entry) {
1364
- var _a, _b, _c;
1365
- return !entry.hidden &&
1366
- !isOptionValid(entry.name, ((_a = options === null || options === void 0 ? void 0 : options[entry.name]) === null || _a === void 0 ? void 0 : _a.ui_type) ||
1367
- ((_b = options === null || options === void 0 ? void 0 : options[entry.name]) === null || _b === void 0 ? void 0 : _b.type), (_c = shownOptions[entry.name]) === null || _c === void 0 ? void 0 : _c.value);
1368
- }).length;
1369
- return ((0, jsx_runtime_1.jsxs)(reqore_1.ReqorePanel, { flat: true, minimal: true, collapseButtonProps: { flat: true, minimal: true, size: 'small' }, collapsible: true, label: (0, jsx_runtime_1.jsxs)(compactRowStyles_1.StyledGroupHeader, { children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreP, { effect: { weight: 'bold' }, size: 'big', children: (0, readFirst_1.getOptionGroupLabel)(groupName, groups) }), (0, jsx_runtime_1.jsx)(compactRowStyles_1.StyledGroupHeaderLine, { "$color": cGroupLine }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreButton, __assign({ readOnly: true, size: 'tiny', minimal: true, flat: true, compact: true, effect: { uppercase: true, spaced: 1 } }, (groupName === 'optional' ? { label: "".concat(names.length, " optional") }
1370
- : invalidCount ?
1371
- {
1372
- label: "".concat(invalidCount, " to resolve"),
1373
- intent: 'warning',
1374
- icon: 'ErrorWarningLine',
1375
- }
1376
- : {
1377
- label: 'All set',
1378
- intent: 'success',
1379
- icon: 'CheckLine',
1380
- })))] }), icon: groupConfig === null || groupConfig === void 0 ? void 0 : groupConfig.icon, className: 'options-readfirst-group', padded: false, contentStyle: { padding: '4px 4px 6px' }, children: [(groupConfig === null || groupConfig === void 0 ? void 0 : groupConfig.subtitle) ?
1381
- (0, jsx_runtime_1.jsx)(reqore_1.ReqoreP, { size: 'small', effect: { opacity: 0.6 },
1382
- // Indent to the same content line as the rows (StyledGroupBody's
1383
- // `margin-left` clamp) so the subtitle sits tucked under the group
1384
- // name instead of at the panel edge — and clears the group's
1385
- // vertical rule (left:16px) rather than crossing it.
1386
- style: {
1387
- marginTop: 2,
1388
- marginBottom: 8,
1389
- marginLeft: compactRowStyles_1.GROUP_INDENT,
1390
- paddingRight: 10,
1391
- }, children: groupConfig.subtitle })
1392
- : null, (0, jsx_runtime_1.jsx)(compactRowStyles_1.StyledGroupBody, { "$divider": cDivider, "$hover": cHover, "$focus": cWarning, "$success": cSuccess, "$rowBg": cRowBg, "$lineColor": cGroupLine, className: compactNarrow ? 'readfirst-narrow' : undefined, children: renderGroupRows(names) })] }, groupName));
1502
+ : null, STATUS_BOXES.map(function (box) {
1503
+ var groupsInBox = bucketGroups[box.key];
1504
+ var count = bucketCount(box.key);
1505
+ if (!count)
1506
+ return null;
1507
+ var accent = box.key === 'attention' ? cWarning
1508
+ : box.key === 'set' ? cSuccess
1509
+ : cMuted;
1510
+ // The muted "Optional" box reads as a quieter, recessed
1511
+ // surface a touch darker than the page rather than the
1512
+ // faint grey tint the accent would give.
1513
+ var boxBg = box.key === 'optional' ?
1514
+ (0, colors_1.changeDarkness)((0, colors_1.getMainBackgroundColor)(theme), 0.06)
1515
+ : undefined;
1516
+ return ((0, jsx_runtime_1.jsx)(compactRowStyles_1.StyledStatusBox, { "$accent": accent, "$bg": boxBg, flat: true, minimal: true, collapseButtonProps: { flat: true, minimal: true, size: 'small' }, collapsible: true, label: (0, jsx_runtime_1.jsxs)(compactRowStyles_1.StyledGroupHeader, { children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreP, { effect: { weight: 'bold' }, size: 'normal', children: box.label }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreTag, { size: 'small', minimal: true, compact: true, intent: box.intent, label: String(count) })] }), icon: box.icon, className: 'options-readfirst-group', padded: false, contentStyle: { padding: '4px 4px 6px' }, children: (0, jsx_runtime_1.jsx)(compactRowStyles_1.StyledGroupBody, { "$divider": cDivider, "$hover": cHover, "$focus": cWarning, "$success": cSuccess, "$rowBg": cRowBg, "$lineColor": cGroupLine, className: compactNarrow ? 'readfirst-narrow' : undefined, children: groupsInBox.map(function (groupName) {
1517
+ var groupConfig = groups === null || groups === void 0 ? void 0 : groups[groupName];
1518
+ return ((0, jsx_runtime_1.jsxs)(react_2.default.Fragment, { children: [showGroupSubLabel(groupName) ?
1519
+ (0, jsx_runtime_1.jsx)(compactRowStyles_1.StyledStatusBoxGroupLabel, { children: (0, readFirst_1.getOptionGroupLabel)(groupName, groups) })
1520
+ : null, showGroupSubLabel(groupName) && (groupConfig === null || groupConfig === void 0 ? void 0 : groupConfig.subtitle) ?
1521
+ (0, jsx_runtime_1.jsx)(reqore_1.ReqoreP, { size: 'small', effect: { opacity: 0.6 }, style: {
1522
+ marginTop: 2,
1523
+ marginBottom: 8,
1524
+ marginLeft: compactRowStyles_1.GROUP_INDENT,
1525
+ paddingRight: 10,
1526
+ }, children: groupConfig.subtitle })
1527
+ : null, renderGroupRows(buckets[box.key][groupName])] }, groupName));
1528
+ }) }) }, box.key));
1393
1529
  })] }), commitMode === 'batched' && !readOnly && dirtyOptionNames.length ?
1394
1530
  (0, jsx_runtime_1.jsx)(StyledCommitDock, { "$bg": cBg, "$border": cDivider, children: (0, jsx_runtime_1.jsxs)(reqore_1.ReqoreControlGroup, { className: 'options-readfirst-commitbar', verticalAlign: 'center', wrap: true, children: [(0, jsx_runtime_1.jsx)(reqore_1.ReqoreTag, { size: 'tiny', minimal: true, flat: true, compact: true, effect: { uppercase: true, spaced: 1 }, intent: 'warning', icon: 'EditLine', label: "".concat(dirtyOptionNames.length, " unsaved change").concat(dirtyOptionNames.length === 1 ? '' : 's') }), (0, jsx_runtime_1.jsx)(reqore_1.ReqoreButton, { size: 'small', intent: 'success', icon: 'CheckLine', fixed: true, className: 'options-readfirst-save', disabled: !validityData.isValid, tooltip: validityData.isValid ?
1395
1531
  'Apply the staged changes'