@qoretechnologies/reqraft 0.10.2 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.claude/CLAUDE.md +5 -0
  2. package/design/COMPACT_ENGINE_REDESIGN.md +156 -0
  3. package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +353 -0
  4. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  5. package/dist/components/form/engine/CompactRow.js +158 -101
  6. package/dist/components/form/engine/CompactRow.js.map +1 -1
  7. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  8. package/dist/components/form/engine/CompactToolbar.js +122 -105
  9. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  10. package/dist/components/form/engine/FormEngine.d.ts +9 -1
  11. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  12. package/dist/components/form/engine/FormEngine.js +272 -82
  13. package/dist/components/form/engine/FormEngine.js.map +1 -1
  14. package/dist/components/form/engine/compactRowStyles.d.ts +6 -3
  15. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  16. package/dist/components/form/engine/compactRowStyles.js +76 -49
  17. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  18. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  19. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  20. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  21. package/dist/components/form/engine/readFirst.d.ts +19 -0
  22. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  23. package/dist/components/form/engine/readFirst.js +22 -1
  24. package/dist/components/form/engine/readFirst.js.map +1 -1
  25. package/dist/components/form/engine/variants/VariantCalmTable.d.ts +6 -0
  26. package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -0
  27. package/dist/components/form/engine/variants/VariantCalmTable.js +94 -0
  28. package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -0
  29. package/dist/components/form/engine/variants/VariantCards.d.ts +6 -0
  30. package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -0
  31. package/dist/components/form/engine/variants/VariantCards.js +80 -0
  32. package/dist/components/form/engine/variants/VariantCards.js.map +1 -0
  33. package/dist/components/form/engine/variants/VariantFocus.d.ts +7 -0
  34. package/dist/components/form/engine/variants/VariantFocus.d.ts.map +1 -0
  35. package/dist/components/form/engine/variants/VariantFocus.js +138 -0
  36. package/dist/components/form/engine/variants/VariantFocus.js.map +1 -0
  37. package/dist/components/form/engine/variants/VariantMinimal.d.ts +6 -0
  38. package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -0
  39. package/dist/components/form/engine/variants/VariantMinimal.js +73 -0
  40. package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -0
  41. package/dist/components/form/engine/variants/focusDemo.d.ts +13 -0
  42. package/dist/components/form/engine/variants/focusDemo.d.ts.map +1 -0
  43. package/dist/components/form/engine/variants/focusDemo.js +139 -0
  44. package/dist/components/form/engine/variants/focusDemo.js.map +1 -0
  45. package/dist/components/form/engine/variants/variantModel.d.ts +70 -0
  46. package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -0
  47. package/dist/components/form/engine/variants/variantModel.js +133 -0
  48. package/dist/components/form/engine/variants/variantModel.js.map +1 -0
  49. package/dist/components/form/engine/variants/variantParts.d.ts +79 -0
  50. package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -0
  51. package/dist/components/form/engine/variants/variantParts.js +191 -0
  52. package/dist/components/form/engine/variants/variantParts.js.map +1 -0
  53. package/dist/components/form/fields/auto/AutoFormField.d.ts +3 -0
  54. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  55. package/dist/components/form/fields/auto/AutoFormField.js +5 -2
  56. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/components/form/engine/CompactRow.tsx +273 -258
  59. package/src/components/form/engine/CompactToolbar.tsx +112 -85
  60. package/src/components/form/engine/FormEngine.stories.tsx +239 -115
  61. package/src/components/form/engine/FormEngine.tsx +332 -83
  62. package/src/components/form/engine/compactRowStyles.ts +221 -144
  63. package/src/components/form/engine/compactToolbarContext.ts +1 -0
  64. package/src/components/form/engine/readFirst.ts +35 -0
  65. package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +119 -0
  66. package/src/components/form/engine/variants/VariantCalmTable.tsx +242 -0
  67. package/src/components/form/engine/variants/VariantCards.tsx +212 -0
  68. package/src/components/form/engine/variants/VariantFocus.tsx +382 -0
  69. package/src/components/form/engine/variants/VariantMinimal.tsx +170 -0
  70. package/src/components/form/engine/variants/focusDemo.ts +145 -0
  71. package/src/components/form/engine/variants/variantModel.ts +216 -0
  72. package/src/components/form/engine/variants/variantParts.tsx +313 -0
  73. package/src/components/form/fields/auto/AutoFormField.stories.tsx +9 -2
  74. package/src/components/form/fields/auto/AutoFormField.tsx +8 -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,28 +148,16 @@ 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);
166
158
  const availableOptions = useContextSelector(CompactRowContext, (v) => v.availableOptions);
167
159
  const requiredGroupsInfo = useContextSelector(CompactRowContext, (v) => v.requiredGroupsInfo);
168
160
  const handleValueChange = useContextSelector(CompactRowContext, (v) => v.handleValueChange);
169
- const handleAddOptionalFieldChange = useContextSelector(
170
- CompactRowContext,
171
- (v) => v.handleAddOptionalFieldChange
172
- );
173
161
  const toggleExpandedOption = useContextSelector(
174
162
  CompactRowContext,
175
163
  (v) => v.toggleExpandedOption
@@ -190,14 +178,12 @@ export const CompactRow = memo(
190
178
  const theme = useContextSelector(CompactRowContext, (v) => v.theme);
191
179
  const cMuted = useContextSelector(CompactRowContext, (v) => v.cMuted);
192
180
  const templates = useContextSelector(CompactRowContext, (v) => v.templates);
193
- const cFaint = useContextSelector(CompactRowContext, (v) => v.cFaint);
194
181
  const cKey = useContextSelector(CompactRowContext, (v) => v.cKey);
195
182
  const cDivider = useContextSelector(CompactRowContext, (v) => v.cDivider);
196
183
  const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
197
184
  const cDanger = useContextSelector(CompactRowContext, (v) => v.cDanger);
198
185
  const cWarning = useContextSelector(CompactRowContext, (v) => v.cWarning);
199
186
  const cInfo = useContextSelector(CompactRowContext, (v) => v.cInfo);
200
- const cBg = useContextSelector(CompactRowContext, (v) => v.cBg);
201
187
 
202
188
  // Value-cell content: colour adds a swatch, file an icon + size; hash keeps
203
189
  // its "N fields" summary (sub-fields reveal beneath the row).
@@ -420,6 +406,21 @@ export const CompactRow = memo(
420
406
  // A choice with per-option logos (e.g. language) reads better collapsed.
421
407
  !optionHasImages(schema) &&
422
408
  !COMPACT_COMPLEX_TYPES.has(editType);
409
+
410
+ // Auto-focus the editor's first input when a field is opened, so you can type
411
+ // straight away (matches the prototype's tap-to-edit feel).
412
+ const editorRef = React.useRef<HTMLDivElement>(null);
413
+ React.useEffect(() => {
414
+ if (!isExpanded) return undefined;
415
+ const id = window.setTimeout(() => {
416
+ const el = editorRef.current?.querySelector<HTMLElement>(
417
+ 'input:not([type="hidden"]):not([disabled]), textarea, [contenteditable="true"]'
418
+ );
419
+ el?.focus();
420
+ }, 60);
421
+ return () => window.clearTimeout(id);
422
+ }, [isExpanded]);
423
+
423
424
  const revertButton =
424
425
  changed ?
425
426
  <ReqoreButton
@@ -516,10 +517,13 @@ export const CompactRow = memo(
516
517
  .filter((m) => m.label !== 'This field is required')
517
518
  .map((m) => ({ intent: m.intent as string, content: String(m.label) }))
518
519
  : [];
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)),
520
+ // TWO display channels, matching the Focus prototype:
521
+ // • dedicated schema `messages` → prominent coloured PANELS below the row;
522
+ // • everything else (validation reasons, the unmet-dependency hint, the
523
+ // default-value note) a compact INLINE reason strip — not a panel.
524
+ const panelMessages = schemaMessages;
525
+ const inlineMessages: TInfoMsg[] = [
526
+ ...fieldMessages,
523
527
  ...(infoActive && schema?.default_value_desc ?
524
528
  [
525
529
  {
@@ -529,26 +533,20 @@ export const CompactRow = memo(
529
533
  ]
530
534
  : []),
531
535
  ];
536
+ const allMsgs = [...panelMessages, ...inlineMessages];
532
537
  const worstIntent =
533
- tier1.some((m) => m.intent === 'danger') ? 'danger'
534
- : tier1.length ? 'warning'
538
+ allMsgs.some((m) => m.intent === 'danger') ? 'danger'
539
+ : allMsgs.some((m) => m.intent === 'warning') ? 'warning'
535
540
  : undefined;
536
541
  const intentColor =
537
542
  worstIntent === 'danger' ? cDanger
538
543
  : worstIntent === 'warning' ? cWarning
539
544
  : undefined;
540
545
  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
546
  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);
547
+ // The per-row is gone: short_desc shows under the name only when the global
548
+ // "descriptions" toggle is engaged (and inside the editor when expanded).
549
+ const showLabelDesc = !!labelShortDesc && showAllDescriptions === true;
552
550
 
553
551
  const renderInfoStrip = (m: TInfoMsg, index: number) => (
554
552
  <ReqoreMessage
@@ -562,33 +560,14 @@ export const CompactRow = memo(
562
560
  {m.content}
563
561
  </ReqoreMessage>
564
562
  );
563
+ const reasonColor = (intent?: string) =>
564
+ intent === 'danger' ? cDanger
565
+ : intent === 'warning' ? cWarning
566
+ : intent === 'success' ?
567
+ (theme?.intents as Record<string, string> | undefined)?.success || cInfo
568
+ : `${cMuted}cc`;
565
569
 
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;
570
+ const infoToggle = null;
592
571
 
593
572
  // Cluster (required-group connection) — shared by the read row, its block
594
573
  // wrapper AND the inline editor, so the rail/node/highlight persist across
@@ -604,20 +583,9 @@ export const CompactRow = memo(
604
583
  clustered ?
605
584
  `readfirst-cluster-rail${clusterFirst ? ' readfirst-cluster-first' : ''}${clusterLast ? ' readfirst-cluster-last' : ''}${clusterSatisfied ? ' readfirst-cluster-satisfied' : ''}`
606
585
  : '';
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;
586
+ // The connection rail + status nodes are gone — the "One of the below is
587
+ // required" box (and the Covers / Covered-by chips) carry the grouping now.
588
+ const clusterNode = null;
621
589
  const clusterHoverProps =
622
590
  clustered && clusterMembers.length ?
623
591
  {
@@ -626,6 +594,42 @@ export const CompactRow = memo(
626
594
  }
627
595
  : {};
628
596
 
597
+ // Secondary edit actions tuck into a "More" (⋮) menu so the card header stays
598
+ // calm: Fullscreen always, plus Remove field for a removable option. Rendered
599
+ // just before the Done ✓ in both expanded layouts.
600
+ const moreMenu = (
601
+ <ReqoreDropdown
602
+ className='options-readfirst-more'
603
+ icon='More2Fill'
604
+ flat
605
+ minimal
606
+ fixed
607
+ size='small'
608
+ tooltip='More actions'
609
+ items={[
610
+ {
611
+ label: 'Edit fullscreen',
612
+ icon: 'FullscreenLine',
613
+ onClick: () => setFocusedEditing(optionName),
614
+ } as IReqoreDropdownItem,
615
+ ...(removable && !hidden ?
616
+ [
617
+ {
618
+ label: 'Remove field',
619
+ icon: 'DeleteBinLine',
620
+ intent: 'danger',
621
+ onClick: () =>
622
+ confirmAction({
623
+ title: 'Remove field',
624
+ onConfirm: () => removeSelectedOption(optionName),
625
+ }),
626
+ } as IReqoreDropdownItem,
627
+ ]
628
+ : []),
629
+ ]}
630
+ />
631
+ );
632
+
629
633
  if (isExpanded) {
630
634
  if (inlineEditable) {
631
635
  const collapse = () => toggleExpandedOption(optionName);
@@ -641,25 +645,40 @@ export const CompactRow = memo(
641
645
  }
642
646
  {...clusterHoverProps}
643
647
  >
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>
648
+ <StyledLabelBlock>
649
+ <StyledRowLabel
650
+ role='button'
651
+ tabIndex={0}
652
+ aria-label={`Collapse ${label}`}
653
+ title={schema?.short_desc || undefined}
654
+ $color={cKey}
655
+ $pointer
656
+ onClick={collapse}
657
+ onKeyDown={(event) => {
658
+ if (event.key === 'Enter' || event.key === ' ') {
659
+ event.preventDefault();
660
+ collapse();
661
+ }
662
+ }}
663
+ >
664
+ {label}
665
+ {required ? <ReqoreIcon icon='Asterisk' color='danger' size='10px' /> : null}
666
+ </StyledRowLabel>
667
+ {/* Keep the short_desc visible while editing inline when the global
668
+ descriptions toggle is on — read rows show it, so opening a field
669
+ shouldn't make it vanish. */}
670
+ {showLabelDesc ?
671
+ <StyledLabelDesc
672
+ className='options-readfirst-label-desc'
673
+ size='small'
674
+ effect={{ opacity: 0.55 }}
675
+ >
676
+ {labelShortDesc}
677
+ </StyledLabelDesc>
678
+ : null}
679
+ </StyledLabelBlock>
662
680
  <div
681
+ ref={editorRef}
663
682
  style={{ minWidth: 0 }}
664
683
  onKeyDown={(event) => {
665
684
  if (event.key === 'Escape') {
@@ -678,12 +697,12 @@ export const CompactRow = memo(
678
697
  message strip is visible. */}
679
698
  {revertButton}
680
699
  {clearValueButton}
700
+ {moreMenu}
681
701
  <ReqoreButton
682
702
  className='options-readfirst-done'
683
703
  size='small'
684
- flat
685
- minimal
686
704
  intent='success'
705
+ fixed
687
706
  icon='CheckLine'
688
707
  tooltip='Done'
689
708
  onClick={collapse}
@@ -749,7 +768,7 @@ export const CompactRow = memo(
749
768
  }}
750
769
  >
751
770
  <StyledCardHeading>
752
- <StyledCardLabel $color={cMuted}>
771
+ <StyledCardLabel $color={cKey}>
753
772
  {(schema as { icon?: string } | undefined)?.icon || (schema as { image?: string } | undefined)?.image ?
754
773
  <ReqoreIcon
755
774
  icon={(schema as { icon?: string } | undefined)?.icon as never}
@@ -811,18 +830,8 @@ export const CompactRow = memo(
811
830
  </ReqoreButton>
812
831
  );
813
832
  })}
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). */}
833
+ {/* Clear-value sits before the More menu — the card analog of the
834
+ inline row's Clear. Empties the value (keeps the field). */}
826
835
  {hasValue && !readOnly ?
827
836
  <ReqoreButton
828
837
  size='small'
@@ -835,16 +844,16 @@ export const CompactRow = memo(
835
844
  onClick={() => handleValueChange(optionName, undefined)}
836
845
  />
837
846
  : null}
847
+ {moreMenu}
838
848
  <ReqoreButton
839
849
  size='small'
840
- icon='CheckLine'
850
+ icon={readOnly ? 'CloseLine' : 'CheckLine'}
841
851
  intent='success'
842
852
  fixed
843
853
  className='options-readfirst-done'
854
+ tooltip={readOnly ? 'Close' : 'Done'}
844
855
  onClick={() => toggleExpandedOption(optionName)}
845
- >
846
- {readOnly ? 'Close' : 'Done'}
847
- </ReqoreButton>
856
+ />
848
857
  </ReqoreControlGroup>
849
858
  </div>
850
859
  {/* Same fullscreen focused-editing affordance as the classic cards —
@@ -869,6 +878,15 @@ export const CompactRow = memo(
869
878
 
870
879
  const formatted = formatOptionValue(optionField, schema);
871
880
  const empty = formatted === '';
881
+ // Inline reasons shown on the value line: validation / dependency / one-of /
882
+ // default-value hints, plus a read-only covered-by note (editable covered rows
883
+ // carry that in their chip instead).
884
+ const valueReasons: TInfoMsg[] = [
885
+ ...inlineMessages,
886
+ ...(empty && coveredByLabel ?
887
+ [{ content: `Covered by “${coveredByLabel}”`, intent: 'success' }]
888
+ : []),
889
+ ];
872
890
  // A hash row reveals its sub-fields as read-only sub-rows under a "view
873
891
  // more" disclosure; the row itself still expands the real editor on click.
874
892
  const valueType = getValueType(optionField, schema);
@@ -936,8 +954,7 @@ export const CompactRow = memo(
936
954
  flat
937
955
  compact
938
956
  icon='LockLine'
939
- label='Depends on'
940
- effect={{uppercase: true, spaced: 1}}
957
+ tooltip='Disabled — depends on other fields. Click to locate them.'
941
958
  items={[
942
959
  { divider: true, label: 'Unlocked by:', dividerAlign: 'left' } as IReqoreDropdownItem,
943
960
  ...dependencyEntries.flatMap((entry): IReqoreDropdownItem[] => [
@@ -972,9 +989,10 @@ export const CompactRow = memo(
972
989
  if (target?.classList?.contains('readfirst-row')) {
973
990
  readRowHeights.current[optionName] = Math.round(target.getBoundingClientRect().height);
974
991
  }
975
- if (hidden) {
976
- handleAddOptionalFieldChange('options', optionName);
977
- }
992
+ // Optional fields aren't "added" — every one is editable in place. Opening a
993
+ // not-yet-set field just expands its editor (with an empty value); it joins
994
+ // the form's output the moment a value is set (and moves to Set / Needs
995
+ // attention), and drops back to Optional when cleared. No explicit add step.
978
996
  toggleExpandedOption(optionName);
979
997
  };
980
998
 
@@ -1002,18 +1020,36 @@ export const CompactRow = memo(
1002
1020
  rowStripeColor ?
1003
1021
  ({ ['--readfirst-stripe']: rowStripeColor } as React.CSSProperties)
1004
1022
  : undefined;
1005
- const blockWrapped = hashEntries.length > 0 || !!infoBlock;
1006
1023
 
1024
+ // "Focus" trailing status dot, derived from the SHARED status helper (the
1025
+ // same one FormEngine uses to bucket rows into the Needs-attention/Set/Optional
1026
+ // boxes — so the dot and the box can never disagree). Hidden (not-yet-added)
1027
+ // and dependency-locked rows show no dot (the add/lock affordance speaks for
1028
+ // them). worstIntent already folds in schema + validation messages.
1029
+ const cSuccess = (theme?.intents as Record<string, string> | undefined)?.success;
1030
+ const rowStatus = getReadFirstStatus({
1031
+ empty,
1032
+ required,
1033
+ covered: !!coveredByLabel || groupResolved,
1034
+ invalid: worstIntent === 'danger',
1035
+ warned: worstIntent === 'warning',
1036
+ });
1037
+ const dotStatus = hidden || fieldDisabled ? undefined : rowStatus;
1038
+ const dotColor =
1039
+ dotStatus === 'invalid' ? cDanger
1040
+ : dotStatus === 'todo' ? cWarning
1041
+ : dotStatus === 'set' ? cSuccess || cInfo
1042
+ : undefined;
1007
1043
  const row = (
1008
1044
  <div
1009
1045
  key={optionName}
1010
1046
  data-field={optionName}
1011
1047
  role='button'
1012
1048
  tabIndex={0}
1013
- 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 : ''}`}
1049
+ aria-label={label}
1050
+ 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' : ''}${panelMessages.length || hashEntries.length ? ' readfirst-row-tall' : ''}${clusterBlockClass ? ' ' + clusterBlockClass : ''}`}
1015
1051
  aria-disabled={fieldDisabled || undefined}
1016
- style={blockWrapped ? undefined : stripeStyle}
1052
+ style={stripeStyle}
1017
1053
  onClick={activate}
1018
1054
  onKeyDown={(event) => {
1019
1055
  if (event.key === 'Enter' || event.key === ' ') {
@@ -1027,31 +1063,37 @@ export const CompactRow = memo(
1027
1063
  <StyledLabelBlock>
1028
1064
  <StyledRowLabel title={schema?.short_desc || undefined} $color={cKey}>
1029
1065
  {rowChromeIcon}
1030
- {label}
1031
- {required ? <ReqoreIcon icon='Asterisk' color='danger' size='10px' /> : null}
1066
+ {/* label + asterisk + help flow as ONE inline run, so the asterisk
1067
+ stays right after the last word even when the name wraps. */}
1068
+ <span className='options-readfirst-label-text' style={{ minWidth: 0 }}>
1069
+ {label}
1070
+ {required ?
1071
+ <ReqoreIcon icon='Asterisk' color='danger' size='10px' margin='left' marginSize='tiny' />
1072
+ : null}
1073
+ {schema?.desc ?
1074
+ <ReqoreIcon
1075
+ icon='QuestionLine'
1076
+ size='12px'
1077
+ effect={{ opacity: 0.55 }}
1078
+ margin='left'
1079
+ marginSize='tiny'
1080
+ role='button'
1081
+ tabIndex={-1}
1082
+ aria-label='Help'
1083
+ className='options-readfirst-help'
1084
+ style={{ cursor: 'help' }}
1085
+ onClick={(event) => {
1086
+ event.stopPropagation();
1087
+ handleOptionLabelClick(optionName);
1088
+ }}
1089
+ />
1090
+ : null}
1091
+ </span>
1032
1092
  {typeLabel ?
1033
1093
  <ReqoreTag size='tiny' minimal label={typeLabel} labelEffect={{ opacity: 0.55 }} />
1034
1094
  : 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
1095
  </StyledRowLabel>
1054
- {labelShortDesc && infoPanelOpen ?
1096
+ {showLabelDesc ?
1055
1097
  <StyledLabelDesc
1056
1098
  className='options-readfirst-label-desc'
1057
1099
  size='small'
@@ -1063,22 +1105,59 @@ export const CompactRow = memo(
1063
1105
  </StyledLabelBlock>
1064
1106
  <StyledRowValue
1065
1107
  title={!empty && !hidden && typeof formatted === 'string' ? formatted : undefined}
1066
- $color={empty || hidden ? cFaint : `${cMuted}cc`}
1108
+ // A SET value reads at full key brightness so it stands out as the
1109
+ // actual data — crucial when stacked under a (muted) description on
1110
+ // mobile, where a dim value blends into the prose. The bold name still
1111
+ // outranks it; the empty dash stays faint.
1112
+ $color={empty || hidden ? `${cMuted}66` : cKey}
1067
1113
  $empty={empty || hidden}
1068
1114
  >
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)}
1115
+ <span className='options-readfirst-valuetext'>
1116
+ {hidden || empty ? '' : renderReadFirstValue(optionField, schema, formatted)}
1117
+ </span>
1118
+ {valueReasons.map((m, i) => (
1119
+ <span
1120
+ key={`${m.content}-${i}`}
1121
+ className='options-readfirst-reason'
1122
+ style={{ color: reasonColor(m.intent) }}
1123
+ >
1124
+ {m.content}
1125
+ </span>
1126
+ ))}
1127
+ {/* Dedicated schema message PANELS render right here, directly under the
1128
+ value (full width of the value column) — never pushed down by the
1129
+ label's short_desc. */}
1130
+ {panelMessages.length ?
1131
+ <div className='options-readfirst-info-panel'>{panelMessages.map(renderInfoStrip)}</div>
1132
+ : null}
1133
+ {/* The structured hash/list preview also lives in the value cell, so it
1134
+ starts directly under the value summary ("N fields"), not below the
1135
+ label's short_desc. */}
1136
+ {hashEntries.length ?
1137
+ // The inset lives inside the row now, so a click on a value chip would
1138
+ // bubble to the row's onClick and fire activate() a SECOND time (the
1139
+ // chip's onItemClick already calls it) — toggling the editor shut again.
1140
+ // Stop the bubble here; the chip's own handler still opens the editor.
1141
+ <StyledRowInset
1142
+ className='options-readfirst-inset'
1143
+ onClick={(e) => e.stopPropagation()}
1144
+ >
1145
+ <ReqoreCollapsibleContent
1146
+ maxCollapsedHeight={96}
1147
+ buttonProps={{ className: 'options-readfirst-viewmore' }}
1148
+ >
1149
+ <div className='options-readfirst-structured'>
1150
+ <StructuredDataView
1151
+ value={optionField?.value}
1152
+ collapsibleRoot={false}
1153
+ showTypes={showFieldTypes}
1154
+ defaultExpandDepth={2}
1155
+ onItemClick={() => activate()}
1156
+ />
1157
+ </div>
1158
+ </ReqoreCollapsibleContent>
1159
+ </StyledRowInset>
1160
+ : null}
1082
1161
  </StyledRowValue>
1083
1162
  <StyledRowActions>
1084
1163
  {/* Column discipline (table treatment): variable-width chips lead and
@@ -1093,63 +1172,24 @@ export const CompactRow = memo(
1093
1172
  the "One of"/"Covers" chip — keep only "Covered by <X>", the one fact
1094
1173
  the rail can't show. Non-clustered members (split-across-panels or
1095
1174
  narrow mode, where there's no rail) keep the full chip as a fallback. */}
1096
- {!hidden && !fieldDisabled && (!clustered || !!coveredByLabel) ? requiredGroupChip : null}
1175
+ {!hidden && !fieldDisabled && !clustered && !coveredByLabel ? requiredGroupChip : null}
1097
1176
  {!hidden ? dependsOnChip : null}
1098
1177
  {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}
1136
- {/* Lock/add slot BEFORE the info slot so the ⓘ keeps the same far-right
1137
- x on every row — a disabled field's lock sits to the ⓘ's left rather
1138
- than pushing it inward. ADD for a hidden field; a plain LOCK for a
1139
- field disabled for a non-dependency reason. Dependency-locked fields
1140
- show the "Depends on" chip in the chips area instead; the whole row is
1141
- click-to-edit, so there's no hover edit pencil. */}
1142
- {hidden || (fieldDisabled && !dependencyEntries.length) ?
1178
+ {/* Remove-field lives in the expanded editor's "More" (⋮) menu now — no
1179
+ standalone delete button on the read row. */}
1180
+ {/* Lock slot BEFORE the info slot so the ⓘ keeps the same far-right x on
1181
+ every row — a disabled field's lock sits to the ⓘ's left rather than
1182
+ pushing it inward. (Not-yet-set optional fields show NO marker now —
1183
+ they're just editable in place, like every other row. Dependency-
1184
+ locked fields show the "Depends on" chip in the chips area instead.) */}
1185
+ {fieldDisabled && !dependencyEntries.length ?
1143
1186
  <StyledActionSlot className='options-readfirst-trailing-slot' $width={18}>
1144
- {hidden ?
1145
- <ReqoreIcon icon='AddLine' intent='info' size='14px' />
1146
- : <span
1147
- title={fieldDisabledReason}
1148
- style={{ display: 'inline-flex', opacity: 0.45 }}
1149
- >
1150
- <ReqoreIcon className='options-readfirst-locked' icon='LockLine' size='14px' />
1151
- </span>
1152
- }
1187
+ <span
1188
+ title={fieldDisabledReason}
1189
+ style={{ display: 'inline-flex', opacity: 0.45 }}
1190
+ >
1191
+ <ReqoreIcon className='options-readfirst-locked' icon='LockLine' size='14px' />
1192
+ </span>
1153
1193
  </StyledActionSlot>
1154
1194
  : null}
1155
1195
  {infoToggle ?
@@ -1157,61 +1197,36 @@ export const CompactRow = memo(
1157
1197
  {infoToggle}
1158
1198
  </StyledActionSlot>
1159
1199
  : null}
1200
+ {/* The revert affordance lives in the status-dot column (a changed field
1201
+ swaps its dot for the revert icon) so it sits at the same fixed x as
1202
+ the dot — out of the value's way instead of floating over it. */}
1203
+ <StyledActionSlot className='options-readfirst-statusdot-slot' $width={12}>
1204
+ {changed && !readOnly && !hidden ?
1205
+ <ReqoreButton
1206
+ className='options-readfirst-revert'
1207
+ size='tiny'
1208
+ flat
1209
+ minimal
1210
+ compact
1211
+ icon='HistoryLine'
1212
+ tooltip='Revert changes'
1213
+ onClick={(e: React.MouseEvent) => {
1214
+ e.stopPropagation();
1215
+ handleValueChange(
1216
+ optionName,
1217
+ originalValue.current?.[optionName]?.value,
1218
+ originalValue.current?.[optionName]?.type
1219
+ );
1220
+ }}
1221
+ />
1222
+ : dotColor ?
1223
+ <StyledStatusDot $color={dotColor} $ring={dotStatus !== 'set'} aria-hidden />
1224
+ : null}
1225
+ </StyledActionSlot>
1160
1226
  </StyledRowActions>
1161
1227
  </div>
1162
1228
  );
1163
1229
 
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
1230
  return row;
1216
1231
  }
1217
1232
  );