@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
@@ -35,17 +35,16 @@ import {
35
35
  StyledActionSlot,
36
36
  StyledCardHeading,
37
37
  StyledCardLabel,
38
- StyledClusterNode,
39
38
  StyledColorSwatch,
40
39
  StyledColumn,
41
40
  StyledEditCard,
42
- StyledInfoPanel,
43
41
  StyledLabelBlock,
44
42
  StyledLabelDesc,
45
43
  StyledRowActions,
46
44
  StyledRowInset,
47
45
  StyledRowLabel,
48
46
  StyledRowValue,
47
+ StyledStatusDot,
49
48
  } from './compactRowStyles';
50
49
  import { getOptionFieldMessages } from './OptionFieldMessages';
51
50
  import {
@@ -55,6 +54,7 @@ import {
55
54
  getAllowedValueImage,
56
55
  getFileSize,
57
56
  getHashEntries,
57
+ getReadFirstStatus,
58
58
  getValueType,
59
59
  isOptionValueEmpty,
60
60
  optionHasImages,
@@ -148,18 +148,10 @@ export const CompactRow = memo(
148
148
  const isFlashed = useContextSelector(CompactRowContext, (v) =>
149
149
  v.flashedOptions.includes(optionName)
150
150
  );
151
- const infoPanelOverride = useContextSelector(
152
- CompactRowContext,
153
- (v) => v.infoPanelOverrides[optionName]
154
- );
155
151
  const setHighlightedOptions = useContextSelector(
156
152
  CompactRowContext,
157
153
  (v) => v.setHighlightedOptions
158
154
  );
159
- const setInfoPanelOverrides = useContextSelector(
160
- CompactRowContext,
161
- (v) => v.setInfoPanelOverrides
162
- );
163
155
  const setFocusedEditing = useContextSelector(CompactRowContext, (v) => v.setFocusedEditing);
164
156
  const readRowHeights = useContextSelector(CompactRowContext, (v) => v.readRowHeights);
165
157
  const originalValue = useContextSelector(CompactRowContext, (v) => v.originalValue);
@@ -190,14 +182,12 @@ export const CompactRow = memo(
190
182
  const theme = useContextSelector(CompactRowContext, (v) => v.theme);
191
183
  const cMuted = useContextSelector(CompactRowContext, (v) => v.cMuted);
192
184
  const templates = useContextSelector(CompactRowContext, (v) => v.templates);
193
- const cFaint = useContextSelector(CompactRowContext, (v) => v.cFaint);
194
185
  const cKey = useContextSelector(CompactRowContext, (v) => v.cKey);
195
186
  const cDivider = useContextSelector(CompactRowContext, (v) => v.cDivider);
196
187
  const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
197
188
  const cDanger = useContextSelector(CompactRowContext, (v) => v.cDanger);
198
189
  const cWarning = useContextSelector(CompactRowContext, (v) => v.cWarning);
199
190
  const cInfo = useContextSelector(CompactRowContext, (v) => v.cInfo);
200
- const cBg = useContextSelector(CompactRowContext, (v) => v.cBg);
201
191
 
202
192
  // Value-cell content: colour adds a swatch, file an icon + size; hash keeps
203
193
  // its "N fields" summary (sub-fields reveal beneath the row).
@@ -420,6 +410,21 @@ export const CompactRow = memo(
420
410
  // A choice with per-option logos (e.g. language) reads better collapsed.
421
411
  !optionHasImages(schema) &&
422
412
  !COMPACT_COMPLEX_TYPES.has(editType);
413
+
414
+ // Auto-focus the editor's first input when a field is opened, so you can type
415
+ // straight away (matches the prototype's tap-to-edit feel).
416
+ const editorRef = React.useRef<HTMLDivElement>(null);
417
+ React.useEffect(() => {
418
+ if (!isExpanded) return undefined;
419
+ const id = window.setTimeout(() => {
420
+ const el = editorRef.current?.querySelector<HTMLElement>(
421
+ 'input:not([type="hidden"]):not([disabled]), textarea, [contenteditable="true"]'
422
+ );
423
+ el?.focus();
424
+ }, 60);
425
+ return () => window.clearTimeout(id);
426
+ }, [isExpanded]);
427
+
423
428
  const revertButton =
424
429
  changed ?
425
430
  <ReqoreButton
@@ -516,10 +521,13 @@ export const CompactRow = memo(
516
521
  .filter((m) => m.label !== 'This field is required')
517
522
  .map((m) => ({ intent: m.intent as string, content: String(m.label) }))
518
523
  : [];
519
- const isCriticalMsg = (m: TInfoMsg) => m.intent === 'danger' || m.intent === 'warning';
520
- const tier1 = [...schemaMessages, ...fieldMessages].filter(isCriticalMsg);
521
- const tier2: TInfoMsg[] = [
522
- ...[...schemaMessages, ...fieldMessages].filter((m) => !isCriticalMsg(m)),
524
+ // TWO display channels, matching the Focus prototype:
525
+ // • dedicated schema `messages` → prominent coloured PANELS below the row;
526
+ // • everything else (validation reasons, the unmet-dependency hint, the
527
+ // default-value note) a compact INLINE reason strip — not a panel.
528
+ const panelMessages = schemaMessages;
529
+ const inlineMessages: TInfoMsg[] = [
530
+ ...fieldMessages,
523
531
  ...(infoActive && schema?.default_value_desc ?
524
532
  [
525
533
  {
@@ -529,26 +537,20 @@ export const CompactRow = memo(
529
537
  ]
530
538
  : []),
531
539
  ];
540
+ const allMsgs = [...panelMessages, ...inlineMessages];
532
541
  const worstIntent =
533
- tier1.some((m) => m.intent === 'danger') ? 'danger'
534
- : tier1.length ? 'warning'
542
+ allMsgs.some((m) => m.intent === 'danger') ? 'danger'
543
+ : allMsgs.some((m) => m.intent === 'warning') ? 'warning'
535
544
  : undefined;
536
545
  const intentColor =
537
546
  worstIntent === 'danger' ? cDanger
538
547
  : worstIntent === 'warning' ? cWarning
539
548
  : undefined;
540
549
  const showStripe = infoActive && !!intentColor;
541
- // The short_desc renders UNDER the field name (revealed by the ⓘ toggle); the
542
- // value-side panel carries only the messages (tier1/tier2).
543
550
  const labelShortDesc = schema?.short_desc;
544
- const panelHasContent = tier1.length > 0 || tier2.length > 0;
545
- const hasInfoPanelContent = infoActive && (panelHasContent || !!labelShortDesc);
546
- // Open-state precedence: a per-row override is most specific; else the
547
- // global toggle when ENGAGED (show all / hide all — overriding the message
548
- // auto-open); else the default, where only critical (tier1) messages open.
549
- const defaultOpen =
550
- showAllDescriptions === undefined ? tier1.length > 0 : showAllDescriptions;
551
- const infoPanelOpen = hasInfoPanelContent && (infoPanelOverride ?? defaultOpen);
551
+ // The per-row is gone: short_desc shows under the name only when the global
552
+ // "descriptions" toggle is engaged (and inside the editor when expanded).
553
+ const showLabelDesc = !!labelShortDesc && showAllDescriptions === true;
552
554
 
553
555
  const renderInfoStrip = (m: TInfoMsg, index: number) => (
554
556
  <ReqoreMessage
@@ -562,33 +564,14 @@ export const CompactRow = memo(
562
564
  {m.content}
563
565
  </ReqoreMessage>
564
566
  );
567
+ const reasonColor = (intent?: string) =>
568
+ intent === 'danger' ? cDanger
569
+ : intent === 'warning' ? cWarning
570
+ : intent === 'success' ?
571
+ (theme?.intents as Record<string, string> | undefined)?.success || cInfo
572
+ : `${cMuted}cc`;
565
573
 
566
- const infoToggle =
567
- hasInfoPanelContent ?
568
- <ReqoreButton
569
- className='options-readfirst-info-toggle'
570
- size='tiny'
571
- minimal
572
- flat
573
- compact
574
- fixed
575
- active={infoPanelOpen}
576
- intent={worstIntent as never}
577
- icon={infoPanelOpen ? 'InformationFill' : 'InformationLine'}
578
- tooltip={infoPanelOpen ? 'Hide field information' : 'Show field information'}
579
- onClick={(e: React.MouseEvent) => {
580
- e.stopPropagation();
581
- setInfoPanelOverrides((prev) => ({ ...prev, [optionName]: !infoPanelOpen }));
582
- }}
583
- />
584
- : null;
585
-
586
- const infoBlock =
587
- infoPanelOpen && panelHasContent ?
588
- <StyledInfoPanel className='options-readfirst-info-panel'>
589
- {[...tier1, ...tier2].map(renderInfoStrip)}
590
- </StyledInfoPanel>
591
- : null;
574
+ const infoToggle = null;
592
575
 
593
576
  // Cluster (required-group connection) — shared by the read row, its block
594
577
  // wrapper AND the inline editor, so the rail/node/highlight persist across
@@ -604,20 +587,9 @@ export const CompactRow = memo(
604
587
  clustered ?
605
588
  `readfirst-cluster-rail${clusterFirst ? ' readfirst-cluster-first' : ''}${clusterLast ? ' readfirst-cluster-last' : ''}${clusterSatisfied ? ' readfirst-cluster-satisfied' : ''}`
606
589
  : '';
607
- const memberSet = clustered && !hidden && hasValue;
608
- const clusterNode =
609
- clustered ?
610
- <StyledClusterNode
611
- className='options-readfirst-node'
612
- $filled={!!memberSet}
613
- $color={
614
- clusterSatisfied ?
615
- (theme?.intents as Record<string, string> | undefined)?.success || cInfo
616
- : `${cWarning}99`
617
- }
618
- $bg={cBg}
619
- />
620
- : null;
590
+ // The connection rail + status nodes are gone — the "One of the below is
591
+ // required" box (and the Covers / Covered-by chips) carry the grouping now.
592
+ const clusterNode = null;
621
593
  const clusterHoverProps =
622
594
  clustered && clusterMembers.length ?
623
595
  {
@@ -626,6 +598,42 @@ export const CompactRow = memo(
626
598
  }
627
599
  : {};
628
600
 
601
+ // Secondary edit actions tuck into a "More" (⋮) menu so the card header stays
602
+ // calm: Fullscreen always, plus Remove field for a removable option. Rendered
603
+ // just before the Done ✓ in both expanded layouts.
604
+ const moreMenu = (
605
+ <ReqoreDropdown
606
+ className='options-readfirst-more'
607
+ icon='More2Fill'
608
+ flat
609
+ minimal
610
+ fixed
611
+ size='small'
612
+ tooltip='More actions'
613
+ items={[
614
+ {
615
+ label: 'Edit fullscreen',
616
+ icon: 'FullscreenLine',
617
+ onClick: () => setFocusedEditing(optionName),
618
+ } as IReqoreDropdownItem,
619
+ ...(removable && !hidden ?
620
+ [
621
+ {
622
+ label: 'Remove field',
623
+ icon: 'DeleteBinLine',
624
+ intent: 'danger',
625
+ onClick: () =>
626
+ confirmAction({
627
+ title: 'Remove field',
628
+ onConfirm: () => removeSelectedOption(optionName),
629
+ }),
630
+ } as IReqoreDropdownItem,
631
+ ]
632
+ : []),
633
+ ]}
634
+ />
635
+ );
636
+
629
637
  if (isExpanded) {
630
638
  if (inlineEditable) {
631
639
  const collapse = () => toggleExpandedOption(optionName);
@@ -641,25 +649,40 @@ export const CompactRow = memo(
641
649
  }
642
650
  {...clusterHoverProps}
643
651
  >
644
- <StyledRowLabel
645
- role='button'
646
- tabIndex={0}
647
- aria-label={`Collapse ${label}`}
648
- title={schema?.short_desc || undefined}
649
- $color={cKey}
650
- $pointer
651
- onClick={collapse}
652
- onKeyDown={(event) => {
653
- if (event.key === 'Enter' || event.key === ' ') {
654
- event.preventDefault();
655
- collapse();
656
- }
657
- }}
658
- >
659
- {label}
660
- {required ? <ReqoreIcon icon='Asterisk' color='danger' size='10px' /> : null}
661
- </StyledRowLabel>
652
+ <StyledLabelBlock>
653
+ <StyledRowLabel
654
+ role='button'
655
+ tabIndex={0}
656
+ aria-label={`Collapse ${label}`}
657
+ title={schema?.short_desc || undefined}
658
+ $color={cKey}
659
+ $pointer
660
+ onClick={collapse}
661
+ onKeyDown={(event) => {
662
+ if (event.key === 'Enter' || event.key === ' ') {
663
+ event.preventDefault();
664
+ collapse();
665
+ }
666
+ }}
667
+ >
668
+ {label}
669
+ {required ? <ReqoreIcon icon='Asterisk' color='danger' size='10px' /> : null}
670
+ </StyledRowLabel>
671
+ {/* Keep the short_desc visible while editing inline when the global
672
+ descriptions toggle is on — read rows show it, so opening a field
673
+ shouldn't make it vanish. */}
674
+ {showLabelDesc ?
675
+ <StyledLabelDesc
676
+ className='options-readfirst-label-desc'
677
+ size='small'
678
+ effect={{ opacity: 0.55 }}
679
+ >
680
+ {labelShortDesc}
681
+ </StyledLabelDesc>
682
+ : null}
683
+ </StyledLabelBlock>
662
684
  <div
685
+ ref={editorRef}
663
686
  style={{ minWidth: 0 }}
664
687
  onKeyDown={(event) => {
665
688
  if (event.key === 'Escape') {
@@ -678,12 +701,12 @@ export const CompactRow = memo(
678
701
  message strip is visible. */}
679
702
  {revertButton}
680
703
  {clearValueButton}
704
+ {moreMenu}
681
705
  <ReqoreButton
682
706
  className='options-readfirst-done'
683
707
  size='small'
684
- flat
685
- minimal
686
708
  intent='success'
709
+ fixed
687
710
  icon='CheckLine'
688
711
  tooltip='Done'
689
712
  onClick={collapse}
@@ -749,7 +772,7 @@ export const CompactRow = memo(
749
772
  }}
750
773
  >
751
774
  <StyledCardHeading>
752
- <StyledCardLabel $color={cMuted}>
775
+ <StyledCardLabel $color={cKey}>
753
776
  {(schema as { icon?: string } | undefined)?.icon || (schema as { image?: string } | undefined)?.image ?
754
777
  <ReqoreIcon
755
778
  icon={(schema as { icon?: string } | undefined)?.icon as never}
@@ -811,18 +834,8 @@ export const CompactRow = memo(
811
834
  </ReqoreButton>
812
835
  );
813
836
  })}
814
- <ReqoreButton
815
- size='small'
816
- icon='FullscreenLine'
817
- minimal
818
- flat
819
- fixed
820
- className='options-readfirst-fullscreen'
821
- tooltip='Edit fullscreen'
822
- onClick={() => setFocusedEditing(optionName)}
823
- />
824
- {/* Clear-value sits between focus-edit and Done — the card analog of
825
- the inline row's Clear. Empties the value (keeps the field). */}
837
+ {/* Clear-value sits before the More menu — the card analog of the
838
+ inline row's Clear. Empties the value (keeps the field). */}
826
839
  {hasValue && !readOnly ?
827
840
  <ReqoreButton
828
841
  size='small'
@@ -835,16 +848,16 @@ export const CompactRow = memo(
835
848
  onClick={() => handleValueChange(optionName, undefined)}
836
849
  />
837
850
  : null}
851
+ {moreMenu}
838
852
  <ReqoreButton
839
853
  size='small'
840
- icon='CheckLine'
854
+ icon={readOnly ? 'CloseLine' : 'CheckLine'}
841
855
  intent='success'
842
856
  fixed
843
857
  className='options-readfirst-done'
858
+ tooltip={readOnly ? 'Close' : 'Done'}
844
859
  onClick={() => toggleExpandedOption(optionName)}
845
- >
846
- {readOnly ? 'Close' : 'Done'}
847
- </ReqoreButton>
860
+ />
848
861
  </ReqoreControlGroup>
849
862
  </div>
850
863
  {/* Same fullscreen focused-editing affordance as the classic cards —
@@ -869,6 +882,15 @@ export const CompactRow = memo(
869
882
 
870
883
  const formatted = formatOptionValue(optionField, schema);
871
884
  const empty = formatted === '';
885
+ // Inline reasons shown on the value line: validation / dependency / one-of /
886
+ // default-value hints, plus a read-only covered-by note (editable covered rows
887
+ // carry that in their chip instead).
888
+ const valueReasons: TInfoMsg[] = [
889
+ ...inlineMessages,
890
+ ...(empty && coveredByLabel ?
891
+ [{ content: `Covered by “${coveredByLabel}”`, intent: 'success' }]
892
+ : []),
893
+ ];
872
894
  // A hash row reveals its sub-fields as read-only sub-rows under a "view
873
895
  // more" disclosure; the row itself still expands the real editor on click.
874
896
  const valueType = getValueType(optionField, schema);
@@ -936,8 +958,7 @@ export const CompactRow = memo(
936
958
  flat
937
959
  compact
938
960
  icon='LockLine'
939
- label='Depends on'
940
- effect={{uppercase: true, spaced: 1}}
961
+ tooltip='Disabled — depends on other fields. Click to locate them.'
941
962
  items={[
942
963
  { divider: true, label: 'Unlocked by:', dividerAlign: 'left' } as IReqoreDropdownItem,
943
964
  ...dependencyEntries.flatMap((entry): IReqoreDropdownItem[] => [
@@ -1002,8 +1023,26 @@ export const CompactRow = memo(
1002
1023
  rowStripeColor ?
1003
1024
  ({ ['--readfirst-stripe']: rowStripeColor } as React.CSSProperties)
1004
1025
  : undefined;
1005
- const blockWrapped = hashEntries.length > 0 || !!infoBlock;
1006
1026
 
1027
+ // "Focus" trailing status dot, derived from the SHARED status helper (the
1028
+ // same one FormEngine uses to bucket rows into the Needs-attention/Set/Optional
1029
+ // boxes — so the dot and the box can never disagree). Hidden (not-yet-added)
1030
+ // and dependency-locked rows show no dot (the add/lock affordance speaks for
1031
+ // them). worstIntent already folds in schema + validation messages.
1032
+ const cSuccess = (theme?.intents as Record<string, string> | undefined)?.success;
1033
+ const rowStatus = getReadFirstStatus({
1034
+ empty,
1035
+ required,
1036
+ covered: !!coveredByLabel || groupResolved,
1037
+ invalid: worstIntent === 'danger',
1038
+ warned: worstIntent === 'warning',
1039
+ });
1040
+ const dotStatus = hidden || fieldDisabled ? undefined : rowStatus;
1041
+ const dotColor =
1042
+ dotStatus === 'invalid' ? cDanger
1043
+ : dotStatus === 'todo' ? cWarning
1044
+ : dotStatus === 'set' ? cSuccess || cInfo
1045
+ : undefined;
1007
1046
  const row = (
1008
1047
  <div
1009
1048
  key={optionName}
@@ -1011,9 +1050,9 @@ export const CompactRow = memo(
1011
1050
  role='button'
1012
1051
  tabIndex={0}
1013
1052
  aria-label={`${label}${hidden ? ' (add field)' : ''}`}
1014
- className={`readfirst-row options-readfirst-value${hidden ? ' readfirst-row-hidden' : ''}${fieldDisabled ? ' readfirst-row-disabled' : ''}${isHighlighted ? ' readfirst-row-group-highlight' : ''}${isFlashed ? ' readfirst-row-flash' : ''}${labelShortDesc && infoPanelOpen ? ' readfirst-row-info-open' : ''}${!blockWrapped && clusterBlockClass ? ' ' + clusterBlockClass : ''}`}
1053
+ className={`readfirst-row options-readfirst-value${hidden ? ' readfirst-row-hidden' : ''}${fieldDisabled ? ' readfirst-row-disabled' : ''}${isHighlighted ? ' readfirst-row-group-highlight' : ''}${isFlashed ? ' readfirst-row-flash' : ''}${showLabelDesc ? ' readfirst-row-info-open' : ''}${clusterBlockClass ? ' ' + clusterBlockClass : ''}`}
1015
1054
  aria-disabled={fieldDisabled || undefined}
1016
- style={blockWrapped ? undefined : stripeStyle}
1055
+ style={stripeStyle}
1017
1056
  onClick={activate}
1018
1057
  onKeyDown={(event) => {
1019
1058
  if (event.key === 'Enter' || event.key === ' ') {
@@ -1027,31 +1066,37 @@ export const CompactRow = memo(
1027
1066
  <StyledLabelBlock>
1028
1067
  <StyledRowLabel title={schema?.short_desc || undefined} $color={cKey}>
1029
1068
  {rowChromeIcon}
1030
- {label}
1031
- {required ? <ReqoreIcon icon='Asterisk' color='danger' size='10px' /> : null}
1069
+ {/* label + asterisk + help flow as ONE inline run, so the asterisk
1070
+ stays right after the last word even when the name wraps. */}
1071
+ <span className='options-readfirst-label-text' style={{ minWidth: 0 }}>
1072
+ {label}
1073
+ {required ?
1074
+ <ReqoreIcon icon='Asterisk' color='danger' size='10px' margin='left' marginSize='tiny' />
1075
+ : null}
1076
+ {schema?.desc ?
1077
+ <ReqoreIcon
1078
+ icon='QuestionLine'
1079
+ size='12px'
1080
+ effect={{ opacity: 0.55 }}
1081
+ margin='left'
1082
+ marginSize='tiny'
1083
+ role='button'
1084
+ tabIndex={-1}
1085
+ aria-label='Help'
1086
+ className='options-readfirst-help'
1087
+ style={{ cursor: 'help' }}
1088
+ onClick={(event) => {
1089
+ event.stopPropagation();
1090
+ handleOptionLabelClick(optionName);
1091
+ }}
1092
+ />
1093
+ : null}
1094
+ </span>
1032
1095
  {typeLabel ?
1033
1096
  <ReqoreTag size='tiny' minimal label={typeLabel} labelEffect={{ opacity: 0.55 }} />
1034
1097
  : null}
1035
- {schema?.desc ?
1036
- <ReqoreIcon
1037
- icon='QuestionLine'
1038
- size='12px'
1039
- effect={{ opacity: 0.55 }}
1040
- margin='left'
1041
- marginSize={5}
1042
- role='button'
1043
- tabIndex={-1}
1044
- aria-label='Help'
1045
- className='options-readfirst-help'
1046
- style={{ cursor: 'help' }}
1047
- onClick={(event) => {
1048
- event.stopPropagation();
1049
- handleOptionLabelClick(optionName);
1050
- }}
1051
- />
1052
- : null}
1053
1098
  </StyledRowLabel>
1054
- {labelShortDesc && infoPanelOpen ?
1099
+ {showLabelDesc ?
1055
1100
  <StyledLabelDesc
1056
1101
  className='options-readfirst-label-desc'
1057
1102
  size='small'
@@ -1063,22 +1108,59 @@ export const CompactRow = memo(
1063
1108
  </StyledLabelBlock>
1064
1109
  <StyledRowValue
1065
1110
  title={!empty && !hidden && typeof formatted === 'string' ? formatted : undefined}
1066
- $color={empty || hidden ? cFaint : `${cMuted}cc`}
1111
+ // A SET value reads at full key brightness so it stands out as the
1112
+ // actual data — crucial when stacked under a (muted) description on
1113
+ // mobile, where a dim value blends into the prose. The bold name still
1114
+ // outranks it; the empty dash stays faint.
1115
+ $color={empty || hidden ? `${cMuted}66` : cKey}
1067
1116
  $empty={empty || hidden}
1068
1117
  >
1069
- {hidden ?
1070
- 'Not in formadd'
1071
- : empty ?
1072
- coveredByLabel ?
1073
- // Editable rows carry the "covered by" explanation in the group
1074
- // chip; read-only rows have no chip, so keep it inline here.
1075
- readOnly ?
1076
- `Not set covered by “${coveredByLabel}”`
1077
- : 'Not set'
1078
- : required ?
1079
- 'Required — not set'
1080
- : 'Not set'
1081
- : renderReadFirstValue(optionField, schema, formatted)}
1118
+ <span className='options-readfirst-valuetext'>
1119
+ {hidden || empty ? '' : renderReadFirstValue(optionField, schema, formatted)}
1120
+ </span>
1121
+ {valueReasons.map((m, i) => (
1122
+ <span
1123
+ key={`${m.content}-${i}`}
1124
+ className='options-readfirst-reason'
1125
+ style={{ color: reasonColor(m.intent) }}
1126
+ >
1127
+ {m.content}
1128
+ </span>
1129
+ ))}
1130
+ {/* Dedicated schema message PANELS render right here, directly under the
1131
+ value (full width of the value column) — never pushed down by the
1132
+ label's short_desc. */}
1133
+ {panelMessages.length ?
1134
+ <div className='options-readfirst-info-panel'>{panelMessages.map(renderInfoStrip)}</div>
1135
+ : null}
1136
+ {/* The structured hash/list preview also lives in the value cell, so it
1137
+ starts directly under the value summary ("N fields"), not below the
1138
+ label's short_desc. */}
1139
+ {hashEntries.length ?
1140
+ // The inset lives inside the row now, so a click on a value chip would
1141
+ // bubble to the row's onClick and fire activate() a SECOND time (the
1142
+ // chip's onItemClick already calls it) — toggling the editor shut again.
1143
+ // Stop the bubble here; the chip's own handler still opens the editor.
1144
+ <StyledRowInset
1145
+ className='options-readfirst-inset'
1146
+ onClick={(e) => e.stopPropagation()}
1147
+ >
1148
+ <ReqoreCollapsibleContent
1149
+ maxCollapsedHeight={96}
1150
+ buttonProps={{ className: 'options-readfirst-viewmore' }}
1151
+ >
1152
+ <div className='options-readfirst-structured'>
1153
+ <StructuredDataView
1154
+ value={optionField?.value}
1155
+ collapsibleRoot={false}
1156
+ showTypes={showFieldTypes}
1157
+ defaultExpandDepth={2}
1158
+ onItemClick={() => activate()}
1159
+ />
1160
+ </div>
1161
+ </ReqoreCollapsibleContent>
1162
+ </StyledRowInset>
1163
+ : null}
1082
1164
  </StyledRowValue>
1083
1165
  <StyledRowActions>
1084
1166
  {/* Column discipline (table treatment): variable-width chips lead and
@@ -1093,46 +1175,11 @@ export const CompactRow = memo(
1093
1175
  the "One of"/"Covers" chip — keep only "Covered by <X>", the one fact
1094
1176
  the rail can't show. Non-clustered members (split-across-panels or
1095
1177
  narrow mode, where there's no rail) keep the full chip as a fallback. */}
1096
- {!hidden && !fieldDisabled && (!clustered || !!coveredByLabel) ? requiredGroupChip : null}
1178
+ {!hidden && !fieldDisabled && !clustered && !coveredByLabel ? requiredGroupChip : null}
1097
1179
  {!hidden ? dependsOnChip : null}
1098
1180
  {draftChip}
1099
- {changed ?
1100
- <ReqoreButton
1101
- className='options-readfirst-revert'
1102
- size='tiny'
1103
- flat
1104
- minimal
1105
- compact
1106
- icon='HistoryLine'
1107
- tooltip='Revert changes'
1108
- onClick={(e: React.MouseEvent) => {
1109
- e.stopPropagation();
1110
- handleValueChange(
1111
- optionName,
1112
- originalValue.current?.[optionName]?.value,
1113
- originalValue.current?.[optionName]?.type
1114
- );
1115
- }}
1116
- />
1117
- : null}
1118
- {removable && !hidden ?
1119
- <ReqoreButton
1120
- className='readfirst-action'
1121
- size='small'
1122
- flat
1123
- minimal
1124
- intent='danger'
1125
- icon='DeleteBinLine'
1126
- tooltip='Remove field'
1127
- onClick={(e: React.MouseEvent) => {
1128
- e.stopPropagation();
1129
- confirmAction({
1130
- title: 'Remove field',
1131
- onConfirm: () => removeSelectedOption(optionName),
1132
- });
1133
- }}
1134
- />
1135
- : null}
1181
+ {/* Remove-field lives in the expanded editor's "More" (⋮) menu now — no
1182
+ standalone delete button on the read row. */}
1136
1183
  {/* Lock/add slot BEFORE the info slot so the ⓘ keeps the same far-right
1137
1184
  x on every row — a disabled field's lock sits to the ⓘ's left rather
1138
1185
  than pushing it inward. ADD for a hidden field; a plain LOCK for a
@@ -1157,61 +1204,36 @@ export const CompactRow = memo(
1157
1204
  {infoToggle}
1158
1205
  </StyledActionSlot>
1159
1206
  : null}
1207
+ {/* The revert affordance lives in the status-dot column (a changed field
1208
+ swaps its dot for the revert icon) so it sits at the same fixed x as
1209
+ the dot — out of the value's way instead of floating over it. */}
1210
+ <StyledActionSlot className='options-readfirst-statusdot-slot' $width={12}>
1211
+ {changed && !readOnly && !hidden ?
1212
+ <ReqoreButton
1213
+ className='options-readfirst-revert'
1214
+ size='tiny'
1215
+ flat
1216
+ minimal
1217
+ compact
1218
+ icon='HistoryLine'
1219
+ tooltip='Revert changes'
1220
+ onClick={(e: React.MouseEvent) => {
1221
+ e.stopPropagation();
1222
+ handleValueChange(
1223
+ optionName,
1224
+ originalValue.current?.[optionName]?.value,
1225
+ originalValue.current?.[optionName]?.type
1226
+ );
1227
+ }}
1228
+ />
1229
+ : dotColor ?
1230
+ <StyledStatusDot $color={dotColor} $ring={dotStatus !== 'set'} aria-hidden />
1231
+ : null}
1232
+ </StyledActionSlot>
1160
1233
  </StyledRowActions>
1161
1234
  </div>
1162
1235
  );
1163
1236
 
1164
- if (hashEntries.length) {
1165
- return (
1166
- <StyledColumn
1167
- key={optionName}
1168
- data-field={optionName}
1169
- className={`options-readfirst-hash-row${clusterBlockClass ? ' ' + clusterBlockClass : ''}`}
1170
- style={stripeStyle}
1171
- >
1172
- {row}
1173
- <StyledRowInset className='options-readfirst-inset'>
1174
- <ReqoreCollapsibleContent
1175
- maxCollapsedHeight={96}
1176
- buttonProps={{ className: 'options-readfirst-viewmore' }}
1177
- >
1178
- {/* The IDE workflow-orders renderer (ReqoreDataView): a nested,
1179
- type-coloured tree. Section summaries own their
1180
- expand/collapse clicks, but clicking a VALUE chip opens the
1181
- hash's editor. The Fields-menu "Show field types" toggle also
1182
- drives the per-scalar type chips here. Depth 2 = root + first
1183
- level open; deeper nests start collapsed so the preview stays
1184
- short before the fade's "Show more". */}
1185
- <div className='options-readfirst-structured'>
1186
- <StructuredDataView
1187
- value={optionField?.value}
1188
- collapsibleRoot={false}
1189
- showTypes={showFieldTypes}
1190
- defaultExpandDepth={2}
1191
- onItemClick={() => activate()}
1192
- />
1193
- </div>
1194
- </ReqoreCollapsibleContent>
1195
- </StyledRowInset>
1196
- {infoBlock}
1197
- </StyledColumn>
1198
- );
1199
- }
1200
-
1201
- if (infoBlock) {
1202
- return (
1203
- <StyledColumn
1204
- key={optionName}
1205
- data-field={optionName}
1206
- className={`options-readfirst-info-row${clusterBlockClass ? ' ' + clusterBlockClass : ''}`}
1207
- style={stripeStyle}
1208
- >
1209
- {row}
1210
- {infoBlock}
1211
- </StyledColumn>
1212
- );
1213
- }
1214
-
1215
1237
  return row;
1216
1238
  }
1217
1239
  );