@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g32331ea
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/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +128 -24
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +78 -15
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +48 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +243 -85
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.d.ts +18 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +22 -3
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +27 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +5 -0
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +12 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +72 -39
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +3 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/formAppearance.d.ts +48 -0
- package/dist/components/form/engine/formAppearance.d.ts.map +1 -0
- package/dist/components/form/engine/formAppearance.js +50 -0
- package/dist/components/form/engine/formAppearance.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +14 -2
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +6 -2
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rowFocus.d.ts +9 -1
- package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
- package/dist/components/form/engine/rowFocus.js +17 -5
- package/dist/components/form/engine/rowFocus.js.map +1 -1
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts +21 -0
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts.map +1 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js +33 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js.map +1 -0
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +5 -3
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +4 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.js +13 -7
- package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketHeader.js +4 -1
- package/dist/components/supportTicket/TicketHeader.js.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.js +29 -10
- package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +141 -22
- package/src/components/form/engine/CompactToolbar.tsx +85 -16
- package/src/components/form/engine/FormEngine.stories.tsx +25 -13
- package/src/components/form/engine/FormEngine.tsx +358 -77
- package/src/components/form/engine/FormEngineAppearance.stories.tsx +540 -0
- package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +5 -1
- package/src/components/form/engine/OptionFieldMessages.tsx +73 -2
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +17 -4
- package/src/components/form/engine/compactRowContext.ts +5 -0
- package/src/components/form/engine/compactRowStyles.ts +218 -38
- package/src/components/form/engine/compactToolbarContext.ts +3 -0
- package/src/components/form/engine/formAppearance.ts +78 -0
- package/src/components/form/engine/readFirst.ts +16 -3
- package/src/components/form/engine/rowFocus.ts +17 -6
- package/src/components/form/fields/ChoiceGroupHeading.tsx +38 -0
- package/src/components/form/fields/Field.tsx +6 -5
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +7 -4
- package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
- package/src/components/form/index.tsx +1 -0
- package/src/components/supportTicket/TicketHeader.tsx +2 -1
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +17 -2
- package/src/components/supportTicket/TicketReplyBox.tsx +27 -5
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ReqoreIcon,
|
|
7
7
|
ReqoreMessage,
|
|
8
8
|
ReqoreP,
|
|
9
|
+
ReqoreSpan,
|
|
9
10
|
ReqoreTag,
|
|
10
11
|
ReqoreTagGroup,
|
|
11
12
|
ReqoreVerticalSpacer,
|
|
@@ -43,6 +44,7 @@ import { offersTypeChoices } from './typeChoices';
|
|
|
43
44
|
import { optionRowActions, resolveOptionActions, TOptionActions } from './optionActions';
|
|
44
45
|
import { createRendererOnlyUiTypeCheck, isRendererOnlyUiType } from './rendererTypes';
|
|
45
46
|
import { revealRow } from './revealRow';
|
|
47
|
+
import { findRowFocusTarget } from './rowFocus';
|
|
46
48
|
import { useEmittedValues } from '../fields/emittedValues';
|
|
47
49
|
import { cloneDeep, findKey, flatten, forEach, isEqual, isPlainObject, last, uniq } from 'lodash';
|
|
48
50
|
import map from 'lodash/map';
|
|
@@ -84,6 +86,7 @@ import { CompactRow } from './CompactRow';
|
|
|
84
86
|
import { FormFieldsSkeleton } from './FormFieldsSkeleton';
|
|
85
87
|
import { CompactRowContext, ICompactRowContext, TCodePreviewRenderer } from './compactRowContext';
|
|
86
88
|
import {
|
|
89
|
+
CALM_SECTION_INSET,
|
|
87
90
|
GROUP_INDENT,
|
|
88
91
|
LABEL_AFFORDANCE_WIDTH,
|
|
89
92
|
LABEL_COL_MAX,
|
|
@@ -96,6 +99,7 @@ import {
|
|
|
96
99
|
StyledRequiredClusterHeader,
|
|
97
100
|
StyledStatusBox,
|
|
98
101
|
StyledStatusBoxGroupLabel,
|
|
102
|
+
StyledStatusDot,
|
|
99
103
|
} from './compactRowStyles';
|
|
100
104
|
import { CompactToolbar } from './CompactToolbar';
|
|
101
105
|
import {
|
|
@@ -114,6 +118,14 @@ import {
|
|
|
114
118
|
TMarkdownRenderer,
|
|
115
119
|
} from '../../Description/markdownRendererContext';
|
|
116
120
|
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
121
|
+
import {
|
|
122
|
+
FormAppearanceProvider,
|
|
123
|
+
getCalmPanelHairline,
|
|
124
|
+
getCalmPanelSurface,
|
|
125
|
+
TFormEngineAppearance,
|
|
126
|
+
useFormAppearance,
|
|
127
|
+
useIsCalmForm,
|
|
128
|
+
} from './formAppearance';
|
|
117
129
|
import { firstDeclaredType, isUntypedOptionType } from '../../../helpers/optionUiTypes';
|
|
118
130
|
import {
|
|
119
131
|
TReadFirstStatus,
|
|
@@ -197,6 +209,14 @@ export interface IFormValidityData {
|
|
|
197
209
|
invalidFields: IFormFieldValidityData[];
|
|
198
210
|
}
|
|
199
211
|
|
|
212
|
+
/** The two numbers the readiness line shows ("2 need attention · 1 to fill in"). */
|
|
213
|
+
export interface IFormAttention {
|
|
214
|
+
/** Fields in the first box: still to fill in, and faults. */
|
|
215
|
+
count: number;
|
|
216
|
+
/** How many of those are faults (amber) rather than only still to fill in. */
|
|
217
|
+
faults: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
200
220
|
/**
|
|
201
221
|
* Resolve a field's `inherit_props` map against the current available
|
|
202
222
|
* options (plus any values inherited from an outer FormEngine scope),
|
|
@@ -848,6 +868,21 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
848
868
|
* value; clicking expands the real editor inline, Done collapses it.
|
|
849
869
|
*/
|
|
850
870
|
compact?: boolean;
|
|
871
|
+
/**
|
|
872
|
+
* How the form LOOKS — never what it does. `'default'` keeps the established
|
|
873
|
+
* look; `'calm'` draws the status boxes as plain panels on the theme's panel
|
|
874
|
+
* surface with label/value rows split by hairlines, grey labels, an amber
|
|
875
|
+
* "Required · add a value" in place of red asterisks and messages, quiet
|
|
876
|
+
* counts, no dot on a healthy row, Yes/No as words, and a readiness meter
|
|
877
|
+
* whose done part is grey. See {@link TFormEngineAppearance}.
|
|
878
|
+
*
|
|
879
|
+
* Opt-in, so a consumer that does not pass it is unchanged. Inherited: a
|
|
880
|
+
* nested (`arg_schema`) sub-form and every field editor the form mounts
|
|
881
|
+
* follow the form's appearance, and a form with no `appearance` of its own
|
|
882
|
+
* follows a `FormAppearanceProvider` above it. Applies to compact mode; the
|
|
883
|
+
* classic collection layout only passes it on to its field editors.
|
|
884
|
+
*/
|
|
885
|
+
appearance?: TFormEngineAppearance;
|
|
851
886
|
/**
|
|
852
887
|
* Compact mode only: drop the read-first wrap's horizontal gutter (the 12px
|
|
853
888
|
* breathing room) so the form sits flush to its container's edges. For embeds
|
|
@@ -928,8 +963,32 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
928
963
|
* `Optional` names a category rather than the things in it, and
|
|
929
964
|
* `Optional settings` is what the reader is looking at. Only the labels
|
|
930
965
|
* change; bucketing, icons and intents are untouched.
|
|
966
|
+
*
|
|
967
|
+
* `pending` names the first box while nothing in it is a fault yet — a new form's
|
|
968
|
+
* empty required fields, which are still to fill in (default `To fill in`).
|
|
969
|
+
*/
|
|
970
|
+
compactBoxLabels?: Partial<Record<TFormEngineBoxKey | 'pending', string>>;
|
|
971
|
+
/**
|
|
972
|
+
* The host attempted a submit. Until then an empty required field the person has not
|
|
973
|
+
* been to yet is only "to fill in": a new form opens neutral rather than with every
|
|
974
|
+
* required field in amber, and a field turns amber once someone opens it and leaves
|
|
975
|
+
* it empty. Pass `true` after a failed submit to show every empty required field as
|
|
976
|
+
* needing attention. Invalid values are faults from the start either way.
|
|
977
|
+
*/
|
|
978
|
+
revealRequired?: boolean;
|
|
979
|
+
/**
|
|
980
|
+
* Bump it to move the caret to the first field that needs attention: the answer to a
|
|
981
|
+
* submit that could not go is where to type. Each new value opens that field and puts
|
|
982
|
+
* the caret in it once, even though the host's submit button holds the caret (an open
|
|
983
|
+
* row otherwise never takes a caret held outside it). Pair it with `revealRequired`.
|
|
984
|
+
*/
|
|
985
|
+
focusAttentionRequest?: number;
|
|
986
|
+
/**
|
|
987
|
+
* The readiness line's two numbers, reported whenever they change. A host that draws
|
|
988
|
+
* its own submit control draws it from these, so the control and the form can never
|
|
989
|
+
* disagree about what is left. `{ count: 0, faults: 0 }` is a form with nothing left.
|
|
931
990
|
*/
|
|
932
|
-
|
|
991
|
+
onAttentionChange?: (attention: IFormAttention) => void;
|
|
933
992
|
/**
|
|
934
993
|
* Compact mode only: the three status-box headers stay pinned to the top of
|
|
935
994
|
* whatever scrolls while their own box is in view. Default `true`.
|
|
@@ -1235,6 +1294,9 @@ const FormEngineImpl = ({
|
|
|
1235
1294
|
forceDropdown,
|
|
1236
1295
|
showTypeToggle = true,
|
|
1237
1296
|
compact,
|
|
1297
|
+
// Read through the context `FormEngine` provides (resolved there against an
|
|
1298
|
+
// inherited appearance); destructured only to keep it out of `rest`.
|
|
1299
|
+
appearance: _appearance, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
1238
1300
|
// Destructured only to keep it out of `rest` (and so off the DOM). The gutter
|
|
1239
1301
|
// this prop documents was never implemented: it was passed to the wrap as
|
|
1240
1302
|
// `$flush`, whose styled template had no interpolation slot, so it has always
|
|
@@ -1247,6 +1309,9 @@ const FormEngineImpl = ({
|
|
|
1247
1309
|
compactPanelProps,
|
|
1248
1310
|
compactCollapsedGroups: compactCollapsedGroupsProp,
|
|
1249
1311
|
compactBoxLabels,
|
|
1312
|
+
revealRequired = false,
|
|
1313
|
+
focusAttentionRequest,
|
|
1314
|
+
onAttentionChange,
|
|
1250
1315
|
compactStickyBoxHeaders = true,
|
|
1251
1316
|
compactStickyOffset = 0,
|
|
1252
1317
|
compactToolbar = true,
|
|
@@ -1318,6 +1383,7 @@ const FormEngineImpl = ({
|
|
|
1318
1383
|
const [loading, setLoading] = useState<boolean>(!!(url || customUrl || operatorsUrl));
|
|
1319
1384
|
const confirmAction = useReqoreProperty('confirmAction');
|
|
1320
1385
|
const theme = useReqoreTheme();
|
|
1386
|
+
const calm = useIsCalmForm();
|
|
1321
1387
|
const [focusedEditing, setFocusedEditing] = useState<string>();
|
|
1322
1388
|
const [showFieldTypes, setShowFieldTypes] = useState<boolean>(false);
|
|
1323
1389
|
// Global toggle (toolbar ⓘ): reveal the short-description info panel on every
|
|
@@ -1330,6 +1396,21 @@ const FormEngineImpl = ({
|
|
|
1330
1396
|
const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
|
|
1331
1397
|
// Which options are expanded into their editor (several can be open at once).
|
|
1332
1398
|
const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
|
|
1399
|
+
// The fields the person has opened and left. An empty required field becomes a
|
|
1400
|
+
// fault only then (or after a submit attempt, `revealRequired`): a form nobody has
|
|
1401
|
+
// touched is not already wrong.
|
|
1402
|
+
const [visitedOptions, setVisitedOptions] = useState<string[]>([]);
|
|
1403
|
+
const lastExpandedOptions = useRef<string[]>([]);
|
|
1404
|
+
useEffect(() => {
|
|
1405
|
+
const left = lastExpandedOptions.current.filter((name) => !expandedOptions.includes(name));
|
|
1406
|
+
lastExpandedOptions.current = expandedOptions;
|
|
1407
|
+
if (left.length) {
|
|
1408
|
+
setVisitedOptions((prev) => {
|
|
1409
|
+
const added = left.filter((name) => !prev.includes(name));
|
|
1410
|
+
return added.length ? [...prev, ...added] : prev;
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}, [expandedOptions]);
|
|
1333
1414
|
/** Status boxes whose "N more fields" control the reader has opened. Keyed by
|
|
1334
1415
|
box so opening Optional does not also unfold Set. */
|
|
1335
1416
|
const [expandedMoreBoxes, setExpandedMoreBoxes] = useState<TFormEngineBoxKey[]>([]);
|
|
@@ -2458,6 +2539,11 @@ const FormEngineImpl = ({
|
|
|
2458
2539
|
(newValue, option, optionName) => {
|
|
2459
2540
|
if (
|
|
2460
2541
|
showInvalidOptionsOnly &&
|
|
2542
|
+
// An open row stays while the reader is in it, as its box does
|
|
2543
|
+
// (`stableBucketOf`): the first character typed into an empty required
|
|
2544
|
+
// field makes it valid, and hiding it then would pull the field out
|
|
2545
|
+
// from under the caret mid-word.
|
|
2546
|
+
!expandedOptions.includes(optionName) &&
|
|
2461
2547
|
isOptionValid(
|
|
2462
2548
|
optionName,
|
|
2463
2549
|
getOptionFieldStorageType(
|
|
@@ -2480,6 +2566,7 @@ const FormEngineImpl = ({
|
|
|
2480
2566
|
);
|
|
2481
2567
|
}, [
|
|
2482
2568
|
showInvalidOptionsOnly,
|
|
2569
|
+
expandedOptions,
|
|
2483
2570
|
isRendererOnly,
|
|
2484
2571
|
JSON.stringify(availableOptions),
|
|
2485
2572
|
JSON.stringify(options),
|
|
@@ -2548,9 +2635,12 @@ const FormEngineImpl = ({
|
|
|
2548
2635
|
covered,
|
|
2549
2636
|
invalid,
|
|
2550
2637
|
warned: msgIntent === 'warning',
|
|
2638
|
+
visited: revealRequired || visitedOptions.includes(name),
|
|
2551
2639
|
});
|
|
2552
2640
|
},
|
|
2553
2641
|
[
|
|
2642
|
+
revealRequired,
|
|
2643
|
+
visitedOptions,
|
|
2554
2644
|
isRendererOnly,
|
|
2555
2645
|
JSON.stringify(options),
|
|
2556
2646
|
JSON.stringify(availableOptions),
|
|
@@ -2583,6 +2673,24 @@ const FormEngineImpl = ({
|
|
|
2583
2673
|
.length,
|
|
2584
2674
|
[JSON.stringify(availableOptions), getOptionBucket]
|
|
2585
2675
|
);
|
|
2676
|
+
// How many of those are faults rather than only still to fill in (`pending`). Zero
|
|
2677
|
+
// on a new form, so its first box reads "To fill in" without amber.
|
|
2678
|
+
const readFirstAttentionFaultCount = useMemo(
|
|
2679
|
+
() =>
|
|
2680
|
+
Object.keys(availableOptions || {}).filter(
|
|
2681
|
+
(name) => getOptionBucket(name) === 'attention' && getOptionStatus(name) !== 'pending'
|
|
2682
|
+
).length,
|
|
2683
|
+
[JSON.stringify(availableOptions), getOptionBucket, getOptionStatus]
|
|
2684
|
+
);
|
|
2685
|
+
// The same two numbers, to a host that draws its own submit control from them.
|
|
2686
|
+
const onAttentionChangeRef = useRef(onAttentionChange);
|
|
2687
|
+
onAttentionChangeRef.current = onAttentionChange;
|
|
2688
|
+
useEffect(() => {
|
|
2689
|
+
onAttentionChangeRef.current?.({
|
|
2690
|
+
count: readFirstAttentionCount,
|
|
2691
|
+
faults: readFirstAttentionFaultCount,
|
|
2692
|
+
});
|
|
2693
|
+
}, [readFirstAttentionCount, readFirstAttentionFaultCount]);
|
|
2586
2694
|
|
|
2587
2695
|
const getIntent = useCallback(
|
|
2588
2696
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -2708,6 +2816,39 @@ const FormEngineImpl = ({
|
|
|
2708
2816
|
// field update or an in-place schema reload will NOT re-grab focus; only a
|
|
2709
2817
|
// genuine remount (a fresh instance) auto-focuses again, which is the intended
|
|
2710
2818
|
// on-mount behaviour.
|
|
2819
|
+
// What the "first field to fix" scans read: the opener on arrival below and the
|
|
2820
|
+
// caret move on a submit attempt (`focusAttentionRequest`) pick the same field.
|
|
2821
|
+
const getAttentionFieldMeta = useCallback(
|
|
2822
|
+
(fieldName: string) => {
|
|
2823
|
+
const schema = options?.[fieldName];
|
|
2824
|
+
if (!schema) {
|
|
2825
|
+
return undefined;
|
|
2826
|
+
}
|
|
2827
|
+
return {
|
|
2828
|
+
focusable:
|
|
2829
|
+
!readOnly &&
|
|
2830
|
+
!schema.disabled &&
|
|
2831
|
+
!(schema as { readonly?: boolean }).readonly &&
|
|
2832
|
+
!dependencyLockedNames.includes(fieldName),
|
|
2833
|
+
// Single source of truth: the same bucket the read-first status boxes
|
|
2834
|
+
// show, so a filled-but-invalid required row is a target too.
|
|
2835
|
+
needsAttention: getOptionBucket(fieldName) === 'attention',
|
|
2836
|
+
};
|
|
2837
|
+
},
|
|
2838
|
+
[options, readOnly, dependencyLockedNames, getOptionBucket]
|
|
2839
|
+
);
|
|
2840
|
+
// Opening a row by name: `expandMode: 'single'` replaces only what a click could
|
|
2841
|
+
// (see the note in the opener below), never a row the host named.
|
|
2842
|
+
const openOption = useCallback(
|
|
2843
|
+
(target: string) =>
|
|
2844
|
+
setExpandedOptions((prev) =>
|
|
2845
|
+
prev.includes(target) ? prev
|
|
2846
|
+
: expandMode === 'multi' ? [...prev, target]
|
|
2847
|
+
: [...prev.filter((name) => initialExpandedOptions?.includes(name)), target]
|
|
2848
|
+
),
|
|
2849
|
+
[expandMode, initialExpandedOptions]
|
|
2850
|
+
);
|
|
2851
|
+
|
|
2711
2852
|
const hasAutoFocusedRef = useRef(false);
|
|
2712
2853
|
useEffect(() => {
|
|
2713
2854
|
// Two decisions, one scan. Both open the row that needs attention;
|
|
@@ -2746,22 +2887,7 @@ const FormEngineImpl = ({
|
|
|
2746
2887
|
return;
|
|
2747
2888
|
}
|
|
2748
2889
|
|
|
2749
|
-
const target = getFirstAttentionOptionName(orderedNames,
|
|
2750
|
-
const schema = options[fieldName];
|
|
2751
|
-
if (!schema) {
|
|
2752
|
-
return undefined;
|
|
2753
|
-
}
|
|
2754
|
-
return {
|
|
2755
|
-
focusable:
|
|
2756
|
-
!readOnly &&
|
|
2757
|
-
!schema.disabled &&
|
|
2758
|
-
!(schema as { readonly?: boolean }).readonly &&
|
|
2759
|
-
!dependencyLockedNames.includes(fieldName),
|
|
2760
|
-
// Single source of truth: the same bucket the read-first status boxes
|
|
2761
|
-
// show, so a filled-but-invalid required row is a target too.
|
|
2762
|
-
needsAttention: getOptionBucket(fieldName) === 'attention',
|
|
2763
|
-
};
|
|
2764
|
-
});
|
|
2890
|
+
const target = getFirstAttentionOptionName(orderedNames, getAttentionFieldMeta);
|
|
2765
2891
|
|
|
2766
2892
|
// Mark done even when nothing needs attention, so later value edits or an
|
|
2767
2893
|
// in-place schema reload never re-scan or re-focus. Only a remount arms it
|
|
@@ -2769,17 +2895,13 @@ const FormEngineImpl = ({
|
|
|
2769
2895
|
hasAutoFocusedRef.current = true;
|
|
2770
2896
|
|
|
2771
2897
|
if (target) {
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
// the reader did anything — and this scan is a convenience layered on
|
|
2780
|
-
// top of it. Keep the named rows; replace only what a click could.
|
|
2781
|
-
: [...prev.filter((name) => initialExpandedOptions?.includes(name)), target]
|
|
2782
|
-
);
|
|
2898
|
+
// `expandMode: 'single'` is a rule about what a CLICK does: opening a row
|
|
2899
|
+
// closes the one the reader last opened. It is not a licence to close a row
|
|
2900
|
+
// the HOST named through `initialExpandedOptions`. That is an address — a
|
|
2901
|
+
// statement about what the page is FOR, made before the reader did anything
|
|
2902
|
+
// — and this scan is a convenience layered on top of it. `openOption` keeps
|
|
2903
|
+
// the named rows and replaces only what a click could.
|
|
2904
|
+
openOption(target);
|
|
2783
2905
|
}
|
|
2784
2906
|
}, [
|
|
2785
2907
|
autoFocusFirstRequired,
|
|
@@ -2787,13 +2909,52 @@ const FormEngineImpl = ({
|
|
|
2787
2909
|
compact,
|
|
2788
2910
|
options,
|
|
2789
2911
|
availableOptions,
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
getOptionBucket,
|
|
2793
|
-
expandMode,
|
|
2794
|
-
initialExpandedOptions,
|
|
2912
|
+
getAttentionFieldMeta,
|
|
2913
|
+
openOption,
|
|
2795
2914
|
]);
|
|
2796
2915
|
|
|
2916
|
+
// A submit that could not go is answered with where to type: each new
|
|
2917
|
+
// `focusAttentionRequest` opens the first field that needs attention and puts the
|
|
2918
|
+
// caret in it. The host's submit button holds the caret at that moment, and an open
|
|
2919
|
+
// row never takes a caret held outside it (CompactRow), so the button lets go of it
|
|
2920
|
+
// here first; the person asked to submit, and this is the answer to that.
|
|
2921
|
+
const lastFocusRequest = useRef(focusAttentionRequest);
|
|
2922
|
+
useEffect(() => {
|
|
2923
|
+
if (focusAttentionRequest === lastFocusRequest.current) {
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
lastFocusRequest.current = focusAttentionRequest;
|
|
2927
|
+
if (!focusAttentionRequest || !compact || !options) {
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
const target = getFirstAttentionOptionName(
|
|
2931
|
+
Object.keys(availableOptions),
|
|
2932
|
+
getAttentionFieldMeta
|
|
2933
|
+
);
|
|
2934
|
+
if (!target) {
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
const wrap = compactWrapNodeRef.current;
|
|
2938
|
+
const active = document.activeElement as HTMLElement | null;
|
|
2939
|
+
if (active && active !== document.body && !wrap?.contains(active)) {
|
|
2940
|
+
active.blur();
|
|
2941
|
+
}
|
|
2942
|
+
if (!expandedOptions.includes(target)) {
|
|
2943
|
+
// The row's own open effect reveals it and takes the caret.
|
|
2944
|
+
openOption(target);
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
// Already open (the field the form opened on arrival): its open effect has run,
|
|
2948
|
+
// so the caret goes to its control here.
|
|
2949
|
+
const row = wrap?.querySelector<HTMLElement>(`[data-field="${target}"]`);
|
|
2950
|
+
if (row) {
|
|
2951
|
+
revealRow(row, 'auto');
|
|
2952
|
+
findRowFocusTarget(row.querySelector<HTMLElement>('.options-readfirst-row-editor'), {
|
|
2953
|
+
textEntryOnly: row.classList.contains('options-readfirst-card'),
|
|
2954
|
+
})?.focus({ preventScroll: true });
|
|
2955
|
+
}
|
|
2956
|
+
}, [focusAttentionRequest]);
|
|
2957
|
+
|
|
2797
2958
|
// Read-first completion summary (how many shown options have a value set),
|
|
2798
2959
|
// surfaced as a progress meter at the top of the compact form.
|
|
2799
2960
|
const readFirstCompletion = useMemo(
|
|
@@ -3191,6 +3352,9 @@ const FormEngineImpl = ({
|
|
|
3191
3352
|
option={{ type: resolvedType, ...other }}
|
|
3192
3353
|
getType={getTypeForOption}
|
|
3193
3354
|
isRendererOnly={isRendererOnly}
|
|
3355
|
+
// Open is not left: a field the form opened for typing (the first empty
|
|
3356
|
+
// one, on a new form) says what it needs without already being wrong.
|
|
3357
|
+
requirementPending={!(revealRequired || visitedOptions.includes(optionName))}
|
|
3194
3358
|
/>
|
|
3195
3359
|
{operators && size(operators) && size(other.op) ?
|
|
3196
3360
|
<>
|
|
@@ -3242,6 +3406,8 @@ const FormEngineImpl = ({
|
|
|
3242
3406
|
getTypeForOption,
|
|
3243
3407
|
inheritedScopes,
|
|
3244
3408
|
inheritedFromParent,
|
|
3409
|
+
revealRequired,
|
|
3410
|
+
visitedOptions,
|
|
3245
3411
|
]
|
|
3246
3412
|
);
|
|
3247
3413
|
|
|
@@ -3266,6 +3432,10 @@ const FormEngineImpl = ({
|
|
|
3266
3432
|
// The "N more fields" row's dashed edge. Well above the divider tint: this
|
|
3267
3433
|
// one is an affordance the reader has to notice, not a hairline between rows.
|
|
3268
3434
|
const cMoreBorder = `${cText}4d`;
|
|
3435
|
+
// Calm: the section surface and its hairline, from the theme's panel (the
|
|
3436
|
+
// same surface and edge Reqore gives the section panels themselves).
|
|
3437
|
+
const cPanel = getCalmPanelSurface(theme);
|
|
3438
|
+
const cHairline = getCalmPanelHairline(theme);
|
|
3269
3439
|
|
|
3270
3440
|
// The closure surface the extracted CompactRow reads through context. Refs and
|
|
3271
3441
|
// setters are stable; the state/memo/handler fields change identity as they do
|
|
@@ -3284,6 +3454,8 @@ const FormEngineImpl = ({
|
|
|
3284
3454
|
codePreviewRenderer,
|
|
3285
3455
|
showAllDescriptions,
|
|
3286
3456
|
expandedOptions,
|
|
3457
|
+
visitedOptions,
|
|
3458
|
+
revealRequired,
|
|
3287
3459
|
highlightedOptions,
|
|
3288
3460
|
flashedOptions,
|
|
3289
3461
|
infoPanelOverrides,
|
|
@@ -3332,6 +3504,8 @@ const FormEngineImpl = ({
|
|
|
3332
3504
|
codePreviewRenderer,
|
|
3333
3505
|
showAllDescriptions,
|
|
3334
3506
|
expandedOptions,
|
|
3507
|
+
visitedOptions,
|
|
3508
|
+
revealRequired,
|
|
3335
3509
|
highlightedOptions,
|
|
3336
3510
|
flashedOptions,
|
|
3337
3511
|
infoPanelOverrides,
|
|
@@ -3383,6 +3557,7 @@ const FormEngineImpl = ({
|
|
|
3383
3557
|
readOnly,
|
|
3384
3558
|
invalidCount: size(validityData.invalidFields),
|
|
3385
3559
|
attentionCount: readFirstAttentionCount,
|
|
3560
|
+
attentionFaultCount: readFirstAttentionFaultCount,
|
|
3386
3561
|
completion: readFirstCompletion,
|
|
3387
3562
|
showInvalidOnly: showInvalidOptionsOnly,
|
|
3388
3563
|
onToggleInvalidOnly: handleToggleInvalidOnly,
|
|
@@ -3430,6 +3605,7 @@ const FormEngineImpl = ({
|
|
|
3430
3605
|
onReadOnlyActivate,
|
|
3431
3606
|
validityData,
|
|
3432
3607
|
readFirstAttentionCount,
|
|
3608
|
+
readFirstAttentionFaultCount,
|
|
3433
3609
|
readFirstCompletion,
|
|
3434
3610
|
showInvalidOptionsOnly,
|
|
3435
3611
|
handleToggleInvalidOnly,
|
|
@@ -3711,12 +3887,20 @@ const FormEngineImpl = ({
|
|
|
3711
3887
|
intent?: 'warning' | 'success';
|
|
3712
3888
|
icon: IReqoreIconName;
|
|
3713
3889
|
}> = [
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3890
|
+
// The first box is amber only once something in it is a fault. On a new form it
|
|
3891
|
+
// holds the required fields still to fill in, which is not yet anything wrong.
|
|
3892
|
+
readFirstAttentionFaultCount ?
|
|
3893
|
+
{
|
|
3894
|
+
key: 'attention',
|
|
3895
|
+
label: compactBoxLabels?.attention ?? 'Needs attention',
|
|
3896
|
+
intent: 'warning',
|
|
3897
|
+
icon: 'ErrorWarningLine',
|
|
3898
|
+
}
|
|
3899
|
+
: {
|
|
3900
|
+
key: 'attention',
|
|
3901
|
+
label: compactBoxLabels?.pending ?? 'To fill in',
|
|
3902
|
+
icon: 'CheckboxBlankCircleLine',
|
|
3903
|
+
},
|
|
3720
3904
|
{ key: 'set', label: compactBoxLabels?.set ?? 'Set', intent: 'success', icon: 'CheckLine' },
|
|
3721
3905
|
{
|
|
3722
3906
|
key: 'optional',
|
|
@@ -3785,15 +3969,28 @@ const FormEngineImpl = ({
|
|
|
3785
3969
|
// box (the Focus cluster). A met group needs no banner — the rail + the
|
|
3786
3970
|
// "Covers"/"Covered by" chips already say which member satisfies it.
|
|
3787
3971
|
if (requiredGroupsInfo.satisfiedBy[grp]) return railed;
|
|
3972
|
+
// Amber once someone has been to the group (or a submit was attempted); until
|
|
3973
|
+
// then it is still to fill in, in the quiet colour, like its members.
|
|
3974
|
+
const clusterColor =
|
|
3975
|
+
memberEntries.some((e) => getOptionStatus(e.name) !== 'pending') ? cWarning : cMuted;
|
|
3788
3976
|
return (
|
|
3789
3977
|
<StyledRequiredClusterBox
|
|
3790
3978
|
key={grp}
|
|
3791
3979
|
className='options-readfirst-required-cluster'
|
|
3792
|
-
$border={`${
|
|
3793
|
-
$tint={`${
|
|
3980
|
+
$border={`${clusterColor}33`}
|
|
3981
|
+
$tint={`${clusterColor}0d`}
|
|
3982
|
+
$calm={calm}
|
|
3794
3983
|
>
|
|
3795
|
-
<StyledRequiredClusterHeader
|
|
3796
|
-
|
|
3984
|
+
<StyledRequiredClusterHeader
|
|
3985
|
+
className='options-readfirst-required-cluster-header'
|
|
3986
|
+
$color={clusterColor}
|
|
3987
|
+
$calm={calm}
|
|
3988
|
+
>
|
|
3989
|
+
<ReqoreIcon
|
|
3990
|
+
icon='LinkM'
|
|
3991
|
+
size={calm ? '13px' : '11px'}
|
|
3992
|
+
style={{ color: clusterColor }}
|
|
3993
|
+
/>
|
|
3797
3994
|
One of the below is required
|
|
3798
3995
|
</StyledRequiredClusterHeader>
|
|
3799
3996
|
{railed}
|
|
@@ -3821,20 +4018,31 @@ const FormEngineImpl = ({
|
|
|
3821
4018
|
return (
|
|
3822
4019
|
<React.Fragment key={groupName}>
|
|
3823
4020
|
{showGroupSubLabel(groupName) ?
|
|
3824
|
-
<StyledStatusBoxGroupLabel
|
|
4021
|
+
<StyledStatusBoxGroupLabel
|
|
4022
|
+
className='options-readfirst-group-label'
|
|
4023
|
+
$calm={calm}
|
|
4024
|
+
$color={cKey}
|
|
4025
|
+
>
|
|
3825
4026
|
{getOptionGroupLabel(groupName, groups)}
|
|
3826
4027
|
</StyledStatusBoxGroupLabel>
|
|
3827
4028
|
: null}
|
|
3828
4029
|
{showGroupSubLabel(groupName) && groupConfig?.subtitle ?
|
|
3829
4030
|
<ReqoreP
|
|
4031
|
+
className='options-readfirst-group-subtitle'
|
|
3830
4032
|
size='small'
|
|
3831
4033
|
effect={{ opacity: 0.6 }}
|
|
3832
|
-
style={
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
4034
|
+
style={
|
|
4035
|
+
calm ?
|
|
4036
|
+
// Under the calm heading, at the rows' inset; the space below
|
|
4037
|
+
// it is the next row's own padding.
|
|
4038
|
+
{ margin: 0, padding: `2px ${CALM_SECTION_INSET}px 4px` }
|
|
4039
|
+
: {
|
|
4040
|
+
marginTop: 2,
|
|
4041
|
+
marginBottom: 8,
|
|
4042
|
+
marginLeft: GROUP_INDENT,
|
|
4043
|
+
paddingRight: 10,
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
3838
4046
|
>
|
|
3839
4047
|
{groupConfig.subtitle}
|
|
3840
4048
|
</ReqoreP>
|
|
@@ -3888,8 +4096,15 @@ const FormEngineImpl = ({
|
|
|
3888
4096
|
)
|
|
3889
4097
|
}
|
|
3890
4098
|
flat
|
|
3891
|
-
|
|
4099
|
+
// Calm: no surface and no lift at all. A theme that gives
|
|
4100
|
+
// panels a surface of their own (Reqore's `theme.panel`) would
|
|
4101
|
+
// otherwise paint this wrapper as a card around the section
|
|
4102
|
+
// cards inside it. Transparent and un-raised, Reqore also drops
|
|
4103
|
+
// the wrapper's horizontal padding, so the readiness line, the
|
|
4104
|
+
// search and the sections share one left edge with the host.
|
|
4105
|
+
raised={!calm}
|
|
3892
4106
|
minimal
|
|
4107
|
+
transparent={calm || undefined}
|
|
3893
4108
|
// No panel background: the form sits transparently on whatever
|
|
3894
4109
|
// hosts it (page, drawer, or — for an arg_schema field — the
|
|
3895
4110
|
// parent's edit card) instead of stacking its own dark surface.
|
|
@@ -3967,8 +4182,13 @@ const FormEngineImpl = ({
|
|
|
3967
4182
|
the reader's record withheld, and a page scrolls. */}
|
|
3968
4183
|
{readOnly ?
|
|
3969
4184
|
bucketCount('set') ?
|
|
3970
|
-
<div
|
|
4185
|
+
<div
|
|
4186
|
+
className='options-readfirst-read'
|
|
4187
|
+
style={{ padding: calm ? 0 : '4px 4px 6px' }}
|
|
4188
|
+
>
|
|
3971
4189
|
<StyledGroupBody
|
|
4190
|
+
$calm={calm}
|
|
4191
|
+
$hairline={cHairline}
|
|
3972
4192
|
$divider={cDivider}
|
|
3973
4193
|
$hover={cHover}
|
|
3974
4194
|
$focus={cWarning}
|
|
@@ -3976,7 +4196,7 @@ const FormEngineImpl = ({
|
|
|
3976
4196
|
$rowBg={cRowBg}
|
|
3977
4197
|
$lineColor={cGroupLine}
|
|
3978
4198
|
$moreBorder={cMoreBorder}
|
|
3979
|
-
className={compactNarrow ? 'readfirst-narrow' :
|
|
4199
|
+
className={`readfirst-read-list${compactNarrow ? ' readfirst-narrow' : ''}`}
|
|
3980
4200
|
>
|
|
3981
4201
|
{bucketGroups.set.map((groupName) => renderBoxGroup('set', groupName))}
|
|
3982
4202
|
</StyledGroupBody>
|
|
@@ -4004,6 +4224,17 @@ const FormEngineImpl = ({
|
|
|
4004
4224
|
const orderedRows: TRowEntry[] = groupsInBox.flatMap(
|
|
4005
4225
|
(groupName) => buckets[box.key][groupName] || []
|
|
4006
4226
|
);
|
|
4227
|
+
// The number in the box's header counts the rows whose value
|
|
4228
|
+
// belongs in it NOW. A field being edited keeps its box until it
|
|
4229
|
+
// is left (so it does not jump away under the caret), and it used
|
|
4230
|
+
// to keep its count too: a box read "To fill in 1" while its last
|
|
4231
|
+
// field already had a value and the host's submit read ready.
|
|
4232
|
+
const headerCount =
|
|
4233
|
+
readOnly ? count : (
|
|
4234
|
+
orderedRows.filter(
|
|
4235
|
+
(entry) => getOptionBucket(entry.name, entry.hidden) === box.key
|
|
4236
|
+
).length
|
|
4237
|
+
);
|
|
4007
4238
|
// Optional's own rule first: a box opened solely to reveal
|
|
4008
4239
|
// preselected rows offers those, not an arbitrary first-N.
|
|
4009
4240
|
const candidateRows =
|
|
@@ -4036,24 +4267,32 @@ const FormEngineImpl = ({
|
|
|
4036
4267
|
);
|
|
4037
4268
|
const moreOpen = Boolean(query) || expandedMoreBoxes.includes(box.key);
|
|
4038
4269
|
const accent =
|
|
4039
|
-
box.key === 'attention' ? cWarning
|
|
4270
|
+
box.key === 'attention' && readFirstAttentionFaultCount ? cWarning
|
|
4040
4271
|
: box.key === 'set' ? cSuccess
|
|
4041
4272
|
: cMuted;
|
|
4042
4273
|
// The muted "Optional" box reads as a quieter, recessed
|
|
4043
4274
|
// surface — a touch darker than the page rather than the
|
|
4044
4275
|
// faint grey tint the accent would give.
|
|
4276
|
+
// Calm: every box is the same panel — no recessed one.
|
|
4045
4277
|
const boxBg =
|
|
4046
|
-
box.key === 'optional' ?
|
|
4278
|
+
box.key === 'optional' && !calm ?
|
|
4047
4279
|
changeDarkness(getMainBackgroundColor(theme), 0.06)
|
|
4048
4280
|
: undefined;
|
|
4049
4281
|
return (
|
|
4050
4282
|
<StyledStatusBox
|
|
4051
4283
|
$accent={accent}
|
|
4052
4284
|
$bg={boxBg}
|
|
4285
|
+
$calm={calm}
|
|
4053
4286
|
// Only a box that PINS needs an opaque header (see
|
|
4054
4287
|
// StyledStatusBox); passing it unconditionally would
|
|
4055
|
-
// repaint every form's boxes for nothing.
|
|
4056
|
-
|
|
4288
|
+
// repaint every form's boxes for nothing. A calm box
|
|
4289
|
+
// pins on its own panel surface.
|
|
4290
|
+
$surface={
|
|
4291
|
+
stickyBoxHeaders ?
|
|
4292
|
+
calm ? cPanel
|
|
4293
|
+
: getMainBackgroundColor(theme)
|
|
4294
|
+
: undefined
|
|
4295
|
+
}
|
|
4057
4296
|
key={box.key}
|
|
4058
4297
|
ref={(node: HTMLDivElement | null) => {
|
|
4059
4298
|
statusBoxRefs.current[box.key] = node;
|
|
@@ -4063,7 +4302,11 @@ const FormEngineImpl = ({
|
|
|
4063
4302
|
onCollapseChange={(collapsed) =>
|
|
4064
4303
|
setOpenedStatusBox(collapsed ? undefined : box.key)
|
|
4065
4304
|
}
|
|
4066
|
-
|
|
4305
|
+
// Calm: an ordinary panel — Reqore paints the theme's
|
|
4306
|
+
// panel surface and its hairline edge. The header stays
|
|
4307
|
+
// minimal (no surface or rule of its own); the first
|
|
4308
|
+
// row's hairline is the line under it.
|
|
4309
|
+
flat={!calm}
|
|
4067
4310
|
minimal
|
|
4068
4311
|
/* The header stays on screen while its own box is
|
|
4069
4312
|
being read — see `compactStickyBoxHeaders`. Sticky
|
|
@@ -4101,29 +4344,57 @@ const FormEngineImpl = ({
|
|
|
4101
4344
|
// author.
|
|
4102
4345
|
isCollapsed={wouldCollapse && !hasPreselectedRows}
|
|
4103
4346
|
label={
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4347
|
+
calm ?
|
|
4348
|
+
// One status dot and a word — and only where a
|
|
4349
|
+
// person is needed. The count is a quiet number.
|
|
4350
|
+
<StyledGroupHeader $calm>
|
|
4351
|
+
{box.key === 'attention' && readFirstAttentionFaultCount ?
|
|
4352
|
+
<StyledStatusDot
|
|
4353
|
+
className='options-readfirst-group-dot'
|
|
4354
|
+
$color={cWarning}
|
|
4355
|
+
aria-hidden
|
|
4356
|
+
/>
|
|
4357
|
+
: null}
|
|
4358
|
+
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
4359
|
+
{box.label}
|
|
4360
|
+
</ReqoreP>
|
|
4361
|
+
{headerCount ?
|
|
4362
|
+
<ReqoreSpan
|
|
4363
|
+
className='options-readfirst-group-count'
|
|
4364
|
+
size='small'
|
|
4365
|
+
effect={{ opacity: 0.65 }}
|
|
4366
|
+
>
|
|
4367
|
+
{headerCount}
|
|
4368
|
+
</ReqoreSpan>
|
|
4369
|
+
: null}
|
|
4370
|
+
</StyledGroupHeader>
|
|
4371
|
+
: <StyledGroupHeader>
|
|
4372
|
+
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
4373
|
+
{box.label}
|
|
4374
|
+
</ReqoreP>
|
|
4375
|
+
{headerCount ?
|
|
4376
|
+
<ReqoreTag
|
|
4377
|
+
size='small'
|
|
4378
|
+
minimal
|
|
4379
|
+
compact
|
|
4380
|
+
intent={box.intent}
|
|
4381
|
+
label={String(headerCount)}
|
|
4382
|
+
/>
|
|
4383
|
+
: null}
|
|
4384
|
+
</StyledGroupHeader>
|
|
4116
4385
|
}
|
|
4117
|
-
icon={box.icon}
|
|
4386
|
+
icon={calm ? undefined : box.icon}
|
|
4118
4387
|
className='options-readfirst-group'
|
|
4119
4388
|
padded={false}
|
|
4120
|
-
contentStyle={{ padding: '4px 4px 6px' }}
|
|
4389
|
+
contentStyle={{ padding: calm ? 0 : '4px 4px 6px' }}
|
|
4121
4390
|
>
|
|
4122
4391
|
{/* ONE group body per box: every field block (and the
|
|
4123
4392
|
thin schema sub-labels) is a direct flex child, so the
|
|
4124
4393
|
inter-field gap is identical everywhere — including
|
|
4125
4394
|
across schema-group boundaries. */}
|
|
4126
4395
|
<StyledGroupBody
|
|
4396
|
+
$calm={calm}
|
|
4397
|
+
$hairline={cHairline}
|
|
4127
4398
|
$divider={cDivider}
|
|
4128
4399
|
$hover={cHover}
|
|
4129
4400
|
$focus={cWarning}
|
|
@@ -4560,10 +4831,20 @@ const FormEngineImpl = ({
|
|
|
4560
4831
|
* a child of the row it belongs to. Threading a renderer to each of them by
|
|
4561
4832
|
* hand is how one of them gets missed.
|
|
4562
4833
|
*/
|
|
4563
|
-
export const FormEngine = (props: IFormEngineProps) =>
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
);
|
|
4834
|
+
export const FormEngine = (props: IFormEngineProps) => {
|
|
4835
|
+
// The form's own `appearance` wins; without one it follows the form (or the
|
|
4836
|
+
// host's provider) it sits in, so a nested sub-form never changes look
|
|
4837
|
+
// halfway down its parent.
|
|
4838
|
+
const inherited = useFormAppearance();
|
|
4839
|
+
const appearance = props.appearance ?? inherited;
|
|
4840
|
+
|
|
4841
|
+
return (
|
|
4842
|
+
<MarkdownRendererContext.Provider value={props.markdownRenderer}>
|
|
4843
|
+
<FormAppearanceProvider value={appearance}>
|
|
4844
|
+
<FormEngineImpl {...props} />
|
|
4845
|
+
</FormAppearanceProvider>
|
|
4846
|
+
</MarkdownRendererContext.Provider>
|
|
4847
|
+
);
|
|
4848
|
+
};
|
|
4568
4849
|
|
|
4569
4850
|
export default FormEngine;
|