@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g2d1b4c8

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 (101) hide show
  1. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  2. package/dist/components/form/engine/CompactRow.js +128 -24
  3. package/dist/components/form/engine/CompactRow.js.map +1 -1
  4. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  5. package/dist/components/form/engine/CompactToolbar.js +78 -15
  6. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  7. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  8. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  9. package/dist/components/form/engine/FormEngine.js +243 -85
  10. package/dist/components/form/engine/FormEngine.js.map +1 -1
  11. package/dist/components/form/engine/OptionFieldMessages.d.ts +18 -2
  12. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  13. package/dist/components/form/engine/OptionFieldMessages.js +22 -3
  14. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  15. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  16. package/dist/components/form/engine/_structuredData/SchemaDataView.js +27 -1
  17. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  18. package/dist/components/form/engine/compactRowContext.d.ts +5 -0
  19. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  20. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  21. package/dist/components/form/engine/compactRowStyles.d.ts +12 -0
  22. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  23. package/dist/components/form/engine/compactRowStyles.js +72 -39
  24. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  25. package/dist/components/form/engine/compactToolbarContext.d.ts +3 -0
  26. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  27. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  28. package/dist/components/form/engine/formAppearance.d.ts +48 -0
  29. package/dist/components/form/engine/formAppearance.d.ts.map +1 -0
  30. package/dist/components/form/engine/formAppearance.js +50 -0
  31. package/dist/components/form/engine/formAppearance.js.map +1 -0
  32. package/dist/components/form/engine/readFirst.d.ts +14 -2
  33. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  34. package/dist/components/form/engine/readFirst.js +6 -2
  35. package/dist/components/form/engine/readFirst.js.map +1 -1
  36. package/dist/components/form/engine/rowFocus.d.ts +9 -1
  37. package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
  38. package/dist/components/form/engine/rowFocus.js +17 -5
  39. package/dist/components/form/engine/rowFocus.js.map +1 -1
  40. package/dist/components/form/fields/ChoiceGroupHeading.d.ts +21 -0
  41. package/dist/components/form/fields/ChoiceGroupHeading.d.ts.map +1 -0
  42. package/dist/components/form/fields/ChoiceGroupHeading.js +33 -0
  43. package/dist/components/form/fields/ChoiceGroupHeading.js.map +1 -0
  44. package/dist/components/form/fields/Field.d.ts.map +1 -1
  45. package/dist/components/form/fields/Field.js +5 -3
  46. package/dist/components/form/fields/Field.js.map +1 -1
  47. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  48. package/dist/components/form/fields/allowed-values/AllowedValues.js +4 -2
  49. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  50. package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
  51. package/dist/components/form/fields/radio-group/RadioGroup.js +13 -7
  52. package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
  53. package/dist/components/form/index.d.ts +1 -0
  54. package/dist/components/form/index.d.ts.map +1 -1
  55. package/dist/components/form/index.js +1 -0
  56. package/dist/components/form/index.js.map +1 -1
  57. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +3 -2
  58. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -1
  59. package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -1
  60. package/dist/components/supportTicket/ReferencePicker.d.ts +2 -1
  61. package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
  62. package/dist/components/supportTicket/ReferencePicker.js +19 -13
  63. package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
  64. package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
  65. package/dist/components/supportTicket/TicketHeader.js +4 -1
  66. package/dist/components/supportTicket/TicketHeader.js.map +1 -1
  67. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
  68. package/dist/components/supportTicket/TicketReplyBox.js +29 -10
  69. package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
  70. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  71. package/dist/components/ticketThread/TicketThread.js +8 -16
  72. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/components/form/engine/CompactRow.tsx +141 -22
  75. package/src/components/form/engine/CompactToolbar.tsx +85 -16
  76. package/src/components/form/engine/FormEngine.stories.tsx +25 -13
  77. package/src/components/form/engine/FormEngine.tsx +358 -77
  78. package/src/components/form/engine/FormEngineAppearance.stories.tsx +540 -0
  79. package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +5 -1
  80. package/src/components/form/engine/OptionFieldMessages.tsx +73 -2
  81. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +17 -4
  82. package/src/components/form/engine/compactRowContext.ts +5 -0
  83. package/src/components/form/engine/compactRowStyles.ts +218 -38
  84. package/src/components/form/engine/compactToolbarContext.ts +3 -0
  85. package/src/components/form/engine/formAppearance.ts +78 -0
  86. package/src/components/form/engine/readFirst.ts +16 -3
  87. package/src/components/form/engine/rowFocus.ts +17 -6
  88. package/src/components/form/fields/ChoiceGroupHeading.tsx +38 -0
  89. package/src/components/form/fields/Field.tsx +6 -5
  90. package/src/components/form/fields/allowed-values/AllowedValues.tsx +7 -4
  91. package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
  92. package/src/components/form/index.tsx +1 -0
  93. package/src/components/supportTicket/InterfaceReferenceTags.tsx +3 -2
  94. package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -4
  95. package/src/components/supportTicket/ReferencePicker.tsx +22 -12
  96. package/src/components/supportTicket/TicketConversation.stories.tsx +0 -1
  97. package/src/components/supportTicket/TicketHeader.tsx +2 -1
  98. package/src/components/supportTicket/TicketReplyBox.stories.tsx +24 -21
  99. package/src/components/supportTicket/TicketReplyBox.tsx +27 -5
  100. package/src/components/ticketThread/TicketThread.stories.tsx +31 -2
  101. package/src/components/ticketThread/TicketThread.tsx +13 -28
@@ -35,6 +35,7 @@ import { Description } from '../../Description';
35
35
  import { useMarkdownRenderer } from '../../Description/markdownRendererContext';
36
36
  import { FocusedEditing } from '../../FocusedEditing';
37
37
  import { CompactRowContext } from './compactRowContext';
38
+ import { CALM_REQUIRED_LABEL, CALM_REQUIRED_READ_LABEL, useIsCalmForm } from './formAppearance';
38
39
  import { revealRow } from './revealRow';
39
40
  import {
40
41
  StyledActionSlot,
@@ -192,8 +193,17 @@ export const CompactRow = memo(
192
193
  clusterLast?: boolean;
193
194
  }) => {
194
195
  const readOnly = useContextSelector(CompactRowContext, (v) => v.readOnly);
196
+ // The look only — see `TFormEngineAppearance`. Nothing a row DOES reads it.
197
+ const calm = useIsCalmForm();
195
198
  const commitMode = useContextSelector(CompactRowContext, (v) => v.commitMode);
196
199
  const options = useContextSelector(CompactRowContext, (v) => v.options);
200
+ // Whether this field's emptiness is a fault yet: the person has opened it and left,
201
+ // or the host reported a submit attempt. Before that a required field is only to
202
+ // fill in, and says so in the quiet colour.
203
+ const visited = useContextSelector(
204
+ CompactRowContext,
205
+ (v) => v.revealRequired || !!v.visitedOptions?.includes(optionName)
206
+ );
197
207
 
198
208
  // An absorbed field keeps its own name — it is a different field, not a
199
209
  // property of its host — so the label comes from its schema exactly as it
@@ -296,6 +306,11 @@ export const CompactRow = memo(
296
306
  const cDanger = useContextSelector(CompactRowContext, (v) => v.cDanger);
297
307
  const cWarning = useContextSelector(CompactRowContext, (v) => v.cWarning);
298
308
  const cInfo = useContextSelector(CompactRowContext, (v) => v.cInfo);
309
+ const cText = useContextSelector(CompactRowContext, (v) => v.cText);
310
+ // A field's NAME: full text colour by default; grey in the calm look, so the
311
+ // value beside it is what the eye lands on. One colour for the read row, the
312
+ // open row and the edit card, so a name does not change as its field opens.
313
+ const cLabel = calm ? `${cText}b3` : cKey;
299
314
  // A markdown value renders as a document in the row's inset; the host's
300
315
  // renderer when it supplied one, reqraft's own otherwise.
301
316
 
@@ -402,6 +417,11 @@ export const CompactRow = memo(
402
417
  }
403
418
 
404
419
  if (valueType === 'bool' || valueType === 'boolean') {
420
+ // Calm: Yes and No are words. Neither is a status — "No" is not a
421
+ // failure — so neither is coloured.
422
+ if (calm) {
423
+ return <span style={textStyle}>{formatted}</span>;
424
+ }
405
425
  const truthy = field?.value === true || field?.value === 'true';
406
426
  return (
407
427
  <ReqoreTag
@@ -701,8 +721,11 @@ export const CompactRow = memo(
701
721
  );
702
722
  };
703
723
  const required = !!(schema?.required || schema?.required_groups);
704
- // The asterisk says "you have to fill this in". Read-only, nobody does.
705
- const requiredMark = required && !readOnly;
724
+ // The asterisk says "you have to fill this in". Read-only, nobody does. The
725
+ // calm look draws none at all: an empty required field says so in its
726
+ // value column instead ("Required · add a value"), and a filled one has
727
+ // nothing left to ask for.
728
+ const requiredMark = required && !readOnly && !calm;
706
729
  const removable =
707
730
  !readOnly && !schema?.preselected && !schema?.required && !schema?.required_groups;
708
731
  const changed = !hidden && !readOnly && hasOptionChanged(optionField?.value, optionName);
@@ -767,8 +790,15 @@ export const CompactRow = memo(
767
790
  minimal
768
791
  flat
769
792
  compact
770
- effect={{ uppercase: true, spaced: 1 }}
771
- intent={groupResolved ? 'success' : 'warning'}
793
+ effect={calm ? undefined : { uppercase: true, spaced: 1 }}
794
+ // Calm: a met group is nothing to point at — only the unmet one is
795
+ // coloured.
796
+ intent={
797
+ groupResolved ?
798
+ calm ? undefined
799
+ : 'success'
800
+ : 'warning'
801
+ }
772
802
  icon={groupResolved ? 'CheckLine' : 'LinkM'}
773
803
  label={
774
804
  !groupResolved ? 'One of'
@@ -921,16 +951,39 @@ export const CompactRow = memo(
921
951
  const editor = editorRef.current;
922
952
  if (!editor) return undefined;
923
953
  let settled = false;
954
+ /* A control that is not a text box holds the caret PROVISIONALLY. An editor that
955
+ takes text can mount after the row opens (a code editor loads late), and until
956
+ it does the only control there is its chrome: a fullscreen toggle. When the
957
+ text box arrives it takes the caret, unless the reader has moved it since. A
958
+ row that never grows one (a pick-one) simply keeps its control. */
959
+ let provisional: HTMLElement | null = null;
924
960
  const takeCaret = (): boolean => {
925
961
  if (settled) return true;
926
962
  const active = document.activeElement as HTMLElement | null;
963
+ if (provisional) {
964
+ if (active !== provisional) {
965
+ // The reader moved the caret themselves. Leave it there.
966
+ settled = true;
967
+ return true;
968
+ }
969
+ const textEntry = findRowFocusTarget(editor, { textEntryOnly: true });
970
+ if (!textEntry) return false;
971
+ textEntry.focus({ preventScroll: true });
972
+ // An editor's text box can be in the DOM before it can take focus (Monaco's
973
+ // edit area, mid-layout), and `focus()` then does nothing. Only a caret
974
+ // that landed counts; otherwise try again on the editor's next change.
975
+ if (document.activeElement !== textEntry) return false;
976
+ settled = true;
977
+ return true;
978
+ }
927
979
  if (active && active !== document.body && !rowRef.current?.contains(active)) {
928
980
  // Someone else holds the caret — a field the reader moved into while
929
981
  // this row's editor was still arriving. Leave it where it is.
930
982
  settled = true;
931
983
  return true;
932
984
  }
933
- const control = findRowFocusTarget(editor);
985
+ // A card hands the caret only to where text goes (see `findRowFocusTarget`).
986
+ const control = findRowFocusTarget(editor, { textEntryOnly: !inlineEditable });
934
987
  if (!control) return false;
935
988
  // The least scroll that shows the row clear of the pinned box header,
936
989
  // and none when it is already in view (see `revealRow`).
@@ -940,6 +993,9 @@ export const CompactRow = memo(
940
993
  // `preventScroll`, because the reveal above has already put the row
941
994
  // where it belongs; letting focus scroll as well moves it twice.
942
995
  control.focus({ preventScroll: true });
996
+ // A control that cannot take focus yet (still laying out) did not take
997
+ // the caret: try again on the editor's next change.
998
+ if (document.activeElement !== control) return false;
943
999
  /* …and if the control the row offers is a PICKER, open it.
944
1000
  *
945
1001
  * A closed picker prints the value it holds, which is exactly what the
@@ -957,6 +1013,13 @@ export const CompactRow = memo(
957
1013
  if (openedByAuthor.current && inlineEditable && isClosedPickerTrigger(control)) {
958
1014
  openedByAuthor.current = false;
959
1015
  setOpenThePicker(true);
1016
+ settled = true;
1017
+ return true;
1018
+ }
1019
+ if (findRowFocusTarget(editor, { textEntryOnly: true }) !== control) {
1020
+ // Not a text box: hold it until one arrives (see `provisional`).
1021
+ provisional = control;
1022
+ return false;
960
1023
  }
961
1024
  settled = true;
962
1025
  return true;
@@ -965,7 +1028,9 @@ export const CompactRow = memo(
965
1028
  const observer = new MutationObserver(() => {
966
1029
  if (takeCaret()) observer.disconnect();
967
1030
  });
968
- observer.observe(editor, { childList: true, subtree: true });
1031
+ // Attributes too: an editor's text box can become focusable by an attribute or
1032
+ // style change after it is inserted (see the `focus()` checks above).
1033
+ observer.observe(editor, { childList: true, subtree: true, attributes: true });
969
1034
  return () => observer.disconnect();
970
1035
  }, [isExpanded, optionName, inlineEditable]);
971
1036
 
@@ -1150,7 +1215,7 @@ export const CompactRow = memo(
1150
1215
  fixed
1151
1216
  flat
1152
1217
  compact
1153
- effect={{ uppercase: true, spaced: 1 }}
1218
+ effect={calm ? undefined : { uppercase: true, spaced: 1 }}
1154
1219
  />
1155
1220
  : null;
1156
1221
  const isPassiveCloseAction = fixedAllowedValueOption;
@@ -1174,7 +1239,7 @@ export const CompactRow = memo(
1174
1239
  minimal
1175
1240
  fixed
1176
1241
  icon='CloseLine'
1177
- intent='warning'
1242
+ intent={calm ? undefined : 'warning'}
1178
1243
  // "Cancel edit", not "Cancel": a bare Cancel collides with the one
1179
1244
  // in Reqore's confirmation modal, and this one is specifically about
1180
1245
  // abandoning the edit rather than dismissing a dialog.
@@ -1195,7 +1260,9 @@ export const CompactRow = memo(
1195
1260
  <ReqoreButton
1196
1261
  className='options-readfirst-done'
1197
1262
  size='small'
1198
- intent={!isPassiveCloseAction ? 'success' : undefined}
1263
+ // Calm: a neutral button. Closing a field is not a healthy figure and
1264
+ // not the page's one primary action, so it takes neither colour.
1265
+ intent={!isPassiveCloseAction && !calm ? 'success' : undefined}
1199
1266
  flat={isPassiveCloseAction}
1200
1267
  minimal={isPassiveCloseAction}
1201
1268
  fixed
@@ -1236,6 +1303,10 @@ export const CompactRow = memo(
1236
1303
  allOptions: availableOptions,
1237
1304
  getType: getTypeForOption,
1238
1305
  isRendererOnly,
1306
+ calm,
1307
+ readOnly,
1308
+ // Until the person has been to the field, its requirement is stated plainly.
1309
+ requirementPending: !visited,
1239
1310
  }).map((m) => ({ intent: m.intent as string, content: String(m.label) }))
1240
1311
  : [];
1241
1312
  // TWO display channels, matching the Focus prototype:
@@ -1317,7 +1388,14 @@ export const CompactRow = memo(
1317
1388
  size='small'
1318
1389
  opaque={false}
1319
1390
  flat
1320
- intent={m.intent as never}
1391
+ // Calm: a note that only informs takes no colour — blue is kept for "a
1392
+ // filter is on", green for a healthy figure. A warning or a failure
1393
+ // keeps its intent: those are what a person has to act on.
1394
+ intent={
1395
+ (calm && (!m.intent || m.intent === 'info' || m.intent === 'success') ?
1396
+ undefined
1397
+ : m.intent) as never
1398
+ }
1321
1399
  title={m.title}
1322
1400
  style={{ marginBottom: 8 }}
1323
1401
  >
@@ -1493,7 +1571,7 @@ export const CompactRow = memo(
1493
1571
  tabIndex={0}
1494
1572
  aria-label={`Collapse ${label}`}
1495
1573
  title={schema?.short_desc || undefined}
1496
- $color={cKey}
1574
+ $color={cLabel}
1497
1575
  $pointer
1498
1576
  onClick={collapse}
1499
1577
  onKeyDown={(event) => {
@@ -1504,7 +1582,7 @@ export const CompactRow = memo(
1504
1582
  }}
1505
1583
  >
1506
1584
  {label}
1507
- {required ?
1585
+ {required && !calm ?
1508
1586
  <ReqoreIcon icon='Asterisk' color='danger' size='10px' />
1509
1587
  : null}
1510
1588
  {helpIcon}
@@ -1526,6 +1604,7 @@ export const CompactRow = memo(
1526
1604
  </StyledLabelBlock>
1527
1605
  <div
1528
1606
  ref={editorRef}
1607
+ className='options-readfirst-row-editor'
1529
1608
  style={{ minWidth: 0 }}
1530
1609
  onKeyDown={(event) => {
1531
1610
  if (event.key === 'Escape') {
@@ -1664,6 +1743,7 @@ export const CompactRow = memo(
1664
1743
  return (
1665
1744
  <StyledEditCard
1666
1745
  key={optionName}
1746
+ ref={rowRef}
1667
1747
  data-field={optionName}
1668
1748
  className={
1669
1749
  COMPACT_SINGLE_VALUE_TYPES.has(editType) ?
@@ -1672,6 +1752,7 @@ export const CompactRow = memo(
1672
1752
  }
1673
1753
  $bg={cHover}
1674
1754
  $border={schemaIntentColor ? `${schemaIntentColor}66` : `${cInfo}66`}
1755
+ $calm={calm}
1675
1756
  >
1676
1757
  <div
1677
1758
  style={{
@@ -1682,7 +1763,7 @@ export const CompactRow = memo(
1682
1763
  }}
1683
1764
  >
1684
1765
  <StyledCardHeading>
1685
- <StyledCardLabel $color={cKey}>
1766
+ <StyledCardLabel $color={cLabel}>
1686
1767
  {(
1687
1768
  (schema as { icon?: string } | undefined)?.icon ||
1688
1769
  (schema as { image?: string } | undefined)?.image
@@ -1695,7 +1776,7 @@ export const CompactRow = memo(
1695
1776
  : null}
1696
1777
  <span>
1697
1778
  {label}
1698
- {required ?
1779
+ {required && !calm ?
1699
1780
  <ReqoreIcon icon='Asterisk' color='danger' size='10px' />
1700
1781
  : null}
1701
1782
  {/* The `?` belongs here too. The read row and the inline
@@ -1802,6 +1883,11 @@ export const CompactRow = memo(
1802
1883
  keystroke a silent commit. Same handler, same `cancelEdit()`, so
1803
1884
  the two shapes cannot diverge. */}
1804
1885
  <div
1886
+ // The card's editor, as the inline row's is: an opened card takes the
1887
+ // caret too (the tap-to-edit rule above), so opening Description or
1888
+ // Source Code by a click or by a blocked submit puts the cursor in it.
1889
+ ref={editorRef}
1890
+ className='options-readfirst-row-editor'
1805
1891
  style={{ display: 'contents' }}
1806
1892
  onKeyDown={(event) => {
1807
1893
  if (event.key === 'Escape') {
@@ -1861,7 +1947,7 @@ export const CompactRow = memo(
1861
1947
  const valueReasons: TInfoMsg[] = [
1862
1948
  ...inlineMessages,
1863
1949
  ...(empty && coveredByLabel ?
1864
- [{ content: `Covered by “${coveredByLabel}”`, intent: 'success' }]
1950
+ [{ content: `Covered by “${coveredByLabel}”`, intent: calm ? undefined : 'success' }]
1865
1951
  : []),
1866
1952
  ];
1867
1953
  // A hash row reveals its sub-fields as read-only sub-rows under a "view
@@ -2207,10 +2293,28 @@ export const CompactRow = memo(
2207
2293
  covered: !!coveredByLabel || groupResolved,
2208
2294
  invalid: worstIntent === 'danger',
2209
2295
  warned: worstIntent === 'warning',
2296
+ visited,
2210
2297
  });
2211
2298
  // No dot at all in a read-only form: the dot reports progress on the
2212
2299
  // form, and a read view has no progress to report.
2213
- const dotStatus = hidden || fieldDisabled || readOnly ? undefined : rowStatus;
2300
+ //
2301
+ // Calm: no dot on a healthy row, and none on an empty required one either —
2302
+ // its value column already says "Required · add a value" in amber, and one
2303
+ // thing is said once. Only a failing value keeps the red dot beside the
2304
+ // reason it gives.
2305
+ const dotStatus =
2306
+ hidden || fieldDisabled || readOnly || (calm && rowStatus !== 'invalid') ?
2307
+ undefined
2308
+ : rowStatus;
2309
+ // Calm: an empty required value is said IN the value column, in amber,
2310
+ // where the value would be — instead of a dash plus a red reason beside it.
2311
+ // Not on a locked field (its lock and reason speak for it) or one that
2312
+ // shows the default it will fall back to.
2313
+ const calmRequiredWord =
2314
+ calm && empty && !hidden && !!schema?.required && !fieldDisabled && !formattedDefault ?
2315
+ readOnly ? CALM_REQUIRED_READ_LABEL
2316
+ : CALM_REQUIRED_LABEL
2317
+ : undefined;
2214
2318
  const dotColor =
2215
2319
  dotStatus === 'invalid' ? cDanger
2216
2320
  : dotStatus === 'todo' ? cWarning
@@ -2252,7 +2356,7 @@ export const CompactRow = memo(
2252
2356
  >
2253
2357
  {clusterNode}
2254
2358
  <StyledLabelBlock className='options-readfirst-label-block'>
2255
- <StyledRowLabel title={schema?.short_desc || undefined} $color={cKey}>
2359
+ <StyledRowLabel title={schema?.short_desc || undefined} $color={cLabel}>
2256
2360
  {rowChromeIcon}
2257
2361
  {/* label + asterisk + help flow as ONE inline run, so the asterisk
2258
2362
  stays right after the last word even when the name wraps. */}
@@ -2322,8 +2426,15 @@ export const CompactRow = memo(
2322
2426
  // actual data — crucial when stacked under a (muted) description on
2323
2427
  // mobile, where a dim value blends into the prose. The bold name still
2324
2428
  // outranks it; the empty dash stays faint.
2325
- $color={empty || hidden ? `${cMuted}66` : cKey}
2326
- $empty={empty || hidden}
2429
+ $color={
2430
+ calmRequiredWord ?
2431
+ rowStatus === 'pending' ?
2432
+ `${cMuted}a6`
2433
+ : cWarning
2434
+ : empty || hidden ? `${cMuted}66`
2435
+ : cKey
2436
+ }
2437
+ $empty={(empty || hidden) && !calmRequiredWord}
2327
2438
  >
2328
2439
  {/* A markdown value with a host renderer draws in full in the inset
2329
2440
  below, so the row prints no value line at all: the stripped-prose
@@ -2358,13 +2469,16 @@ export const CompactRow = memo(
2358
2469
  null
2359
2470
  : <span className='options-readfirst-valuetext'>
2360
2471
  {hidden || empty ?
2361
- formattedDefault || '—'
2472
+ calmRequiredWord || formattedDefault || '—'
2362
2473
  : renderReadFirstValue(optionField, schema, formatted, readOnly)}
2363
2474
  </span>}
2364
2475
  {readOnlyChoiceDesc ?
2365
2476
  <span className='options-readfirst-choice-desc'>{readOnlyChoiceDesc}</span>
2366
2477
  : null}
2367
- {valueReasons.map((m, i) => (
2478
+ {(calmRequiredWord ?
2479
+ valueReasons.filter((m) => m.content !== calmRequiredWord)
2480
+ : valueReasons
2481
+ ).map((m, i) => (
2368
2482
  <span
2369
2483
  key={`${m.content}-${i}`}
2370
2484
  className='options-readfirst-reason'
@@ -2606,7 +2720,12 @@ export const CompactRow = memo(
2606
2720
  }}
2607
2721
  />
2608
2722
  : dotColor ?
2609
- <StyledStatusDot $color={dotColor} $ring={dotStatus !== 'set'} aria-hidden />
2723
+ <StyledStatusDot
2724
+ className='options-readfirst-status-dot'
2725
+ $color={dotColor}
2726
+ $ring={dotStatus !== 'set'}
2727
+ aria-hidden
2728
+ />
2610
2729
  : null}
2611
2730
  </StyledActionSlot>
2612
2731
  </StyledRowActions>
@@ -11,9 +11,10 @@ import {
11
11
  } from '@qoretechnologies/reqore/dist/components/Dropdown/list';
12
12
  import { useReqoreTheme } from '@qoretechnologies/reqore/dist/hooks/useTheme';
13
13
  import React, { memo } from 'react';
14
- import styled from 'styled-components';
14
+ import styled, { css } from 'styled-components';
15
15
  import { useContext } from 'use-context-selector';
16
16
  import { CompactToolbarContext, TCompactSort } from './compactToolbarContext';
17
+ import { useIsCalmForm } from './formAppearance';
17
18
 
18
19
  // Field sort modes, listed in the Fields menu's "Sort by" submenu. 'schema' is
19
20
  // the default (the schema's declared order); the rest reorder within each group.
@@ -28,11 +29,21 @@ const SORT_MODES: { value: TCompactSort; label: string; tooltip: string }[] = [
28
29
  // "Focus" header: a summary line ({pct}% complete · {set}/{total} set ·
29
30
  // {attention} need attention →) above a SEGMENTED meter — a green run (set) then
30
31
  // an amber run (needs attention) then the empty remainder.
31
- const StyledCompletion = styled.div`
32
+ const StyledCompletion = styled.div<{ $calm?: boolean }>`
32
33
  display: flex;
33
34
  flex-flow: column;
34
35
  gap: 8px;
35
36
  padding: 0 2px;
37
+ /* Calm: one line — the summary, then a short meter beside it — that wraps
38
+ the meter under the words where there is no room for both. */
39
+ ${({ $calm }) =>
40
+ $calm ?
41
+ css`
42
+ flex-flow: row wrap;
43
+ align-items: center;
44
+ gap: 6px 14px;
45
+ `
46
+ : ''}
36
47
  `;
37
48
  const StyledCompletionLine = styled.div`
38
49
  display: flex;
@@ -46,11 +57,20 @@ const StyledMeter = styled.div<{
46
57
  $track: string;
47
58
  $set_c: string;
48
59
  $att_c: string;
60
+ /** Calm: a short meter beside the summary instead of a full-width bar. */
61
+ $calm?: boolean;
49
62
  }>`
50
63
  position: relative;
51
64
  height: 4px;
52
- border-radius: 3px;
65
+ border-radius: ${({ $calm }) => ($calm ? 2 : 3)}px;
53
66
  width: 100%;
67
+ ${({ $calm }) =>
68
+ $calm ?
69
+ css`
70
+ flex: 1 1 160px;
71
+ max-width: 360px;
72
+ `
73
+ : ''}
54
74
  overflow: hidden;
55
75
  background: ${({ $track }) => $track};
56
76
  /* No per-segment radius — the container's overflow:hidden + radius rounds only
@@ -113,7 +133,9 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
113
133
  readOnly,
114
134
  invalidCount,
115
135
  attentionCount,
136
+ attentionFaultCount,
116
137
  completion,
138
+ showInvalidOnly,
117
139
  onToggleInvalidOnly,
118
140
  hasMultipleOptions,
119
141
  compactQuery,
@@ -135,16 +157,40 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
135
157
  } = useContext(CompactToolbarContext);
136
158
 
137
159
  const theme = useReqoreTheme();
160
+ const calm = useIsCalmForm();
138
161
  const intents = (theme.intents || {}) as Record<string, string>;
162
+ /* "N need attention →" / "N to fill in →" is a toggle (show only those fields), so it
163
+ is one: a button a keyboard reaches, which says whether it is on. A host that turns
164
+ it on for a person (a blocked submit) reads `aria-pressed` rather than guessing,
165
+ since the toolbar draws a different link once something becomes a fault. */
166
+ const attentionToggleProps = {
167
+ className: 'options-readfirst-attention-link',
168
+ role: 'button',
169
+ tabIndex: 0,
170
+ 'aria-pressed': !!showInvalidOnly,
171
+ onClick: onToggleInvalidOnly,
172
+ onKeyDown: (event: React.KeyboardEvent) => {
173
+ if (event.key === 'Enter' || event.key === ' ') {
174
+ event.preventDefault();
175
+ onToggleInvalidOnly?.();
176
+ }
177
+ },
178
+ };
139
179
  const cSuccess = intents.success || '#4a7110';
140
180
  const cWarning = intents.warning || '#d17c29';
141
181
  const cText = (theme.text?.color as string) || '#e8e8e8';
142
182
  const cTrack = `${cText}1f`;
183
+ // Calm: what is done is grey — finished work needs nobody — and only the
184
+ // outstanding part (the amber run, the "need attention" link) is coloured.
185
+ const cDone = calm ? `${cText}8c` : cSuccess;
186
+ const cQuiet = `${cText}a6`;
143
187
  // The GREEN run is what is done, not merely what has a value: a field that is
144
188
  // set but flagged belongs to the amber run, and counting it twice pushed that
145
189
  // run off the end of the track.
146
190
  const setPct = completion.total ? (completion.done / completion.total) * 100 : 0;
147
- const attentionPct = completion.total ? (attentionCount / completion.total) * 100 : 0;
191
+ // The amber run is what is WRONG. On a new form the required fields are only still
192
+ // to fill in, so the track shows them as not done yet, without amber.
193
+ const attentionPct = completion.total ? (attentionFaultCount / completion.total) * 100 : 0;
148
194
 
149
195
  return (
150
196
  <ReqoreControlGroup
@@ -161,11 +207,19 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
161
207
  work. The read view lists what is set, and that is the whole
162
208
  answer. */}
163
209
  {parts.completion && completion.total && !readOnly ?
164
- <StyledCompletion className='options-readfirst-completion'>
210
+ <StyledCompletion className='options-readfirst-completion' $calm={calm}>
165
211
  <StyledCompletionLine>
166
212
  <StyledSummary
167
213
  className='options-readfirst-status'
168
- $color={invalidCount || attentionCount ? cWarning : cSuccess}
214
+ // Calm: the word in the text colour. "Ready" is not a healthy
215
+ // figure to celebrate in green, and "Incomplete" is said in amber
216
+ // by the link beside it — only the outstanding part is coloured.
217
+ $color={
218
+ calm ? cText
219
+ : invalidCount || attentionFaultCount ? cWarning
220
+ : attentionCount ? cText
221
+ : cSuccess
222
+ }
169
223
  style={{ fontWeight: 600 }}
170
224
  >
171
225
  {/* "Ready" is a claim about the whole form, so anything the form
@@ -179,28 +233,43 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
179
233
  batched mode's per-row chip alone. */}
180
234
  {invalidCount || attentionCount ? 'Incomplete' : 'Ready'}
181
235
  </StyledSummary>
182
- <StyledSummary style={{ opacity: 0.5 }}>
236
+ <StyledSummary style={calm ? { color: cQuiet } : { opacity: 0.5 }}>
183
237
  · {completion.set}/{completion.total} set
184
238
  {attentionCount ? ' ·' : ''}
185
239
  </StyledSummary>
186
- {attentionCount ?
187
- <StyledAttentionLink
188
- $color={cWarning}
189
- className='options-readfirst-attention-link'
190
- onClick={onToggleInvalidOnly}
191
- >
192
- {attentionCount} need attention →
240
+ {/* Amber only for what is a fault. A new form's required fields are still to
241
+ fill in, which is the next step, not a warning: they are counted apart,
242
+ in the quiet colour. */}
243
+ {attentionFaultCount ?
244
+ <StyledAttentionLink $color={cWarning} {...attentionToggleProps}>
245
+ {attentionFaultCount} need attention →
193
246
  </StyledAttentionLink>
194
247
  : null}
195
- <StyledPct>{completion.pct}%</StyledPct>
248
+ {attentionCount > attentionFaultCount ?
249
+ attentionFaultCount ?
250
+ <StyledSummary style={{ color: cQuiet }}>
251
+ · {attentionCount - attentionFaultCount} to fill in
252
+ </StyledSummary>
253
+ : <StyledAttentionLink $color={cQuiet} {...attentionToggleProps}>
254
+ {attentionCount} to fill in →
255
+ </StyledAttentionLink>
256
+ : null}
257
+ {/* Calm: the percentage is one more quiet figure on the line, not
258
+ a large bold number pinned to the far edge. */}
259
+ {calm ?
260
+ <StyledSummary className='options-readfirst-pct' style={{ color: cQuiet }}>
261
+ · {completion.pct}%
262
+ </StyledSummary>
263
+ : <StyledPct className='options-readfirst-pct'>{completion.pct}%</StyledPct>}
196
264
  </StyledCompletionLine>
197
265
  <StyledMeter
198
266
  className='options-readfirst-completion-bar'
199
267
  $set={setPct}
200
268
  $attention={attentionPct}
201
269
  $track={cTrack}
202
- $set_c={cSuccess}
270
+ $set_c={cDone}
203
271
  $att_c={cWarning}
272
+ $calm={calm}
204
273
  />
205
274
  </StyledCompletion>
206
275
  : null}