@qoretechnologies/reqraft 0.10.13 → 0.10.15

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.
@@ -573,6 +573,30 @@ export const CompactRow = memo(
573
573
  // "descriptions" toggle is engaged (and inside the editor when expanded).
574
574
  const showLabelDesc = !!labelShortDesc && showAllDescriptions === true;
575
575
 
576
+ // The `?` that opens the long-form help dialog. Shared by the read row AND
577
+ // the inline editor: opening a field used to drop it, leaving an editing row
578
+ // with no route to the field's description at all. Stops propagation because
579
+ // both labels are click targets themselves (the row expands / collapses).
580
+ const helpIcon =
581
+ schema?.desc ?
582
+ <ReqoreIcon
583
+ icon='QuestionLine'
584
+ size='12px'
585
+ effect={{ opacity: 0.55 }}
586
+ margin='left'
587
+ marginSize='tiny'
588
+ role='button'
589
+ tabIndex={-1}
590
+ aria-label='Help'
591
+ className='options-readfirst-help'
592
+ style={{ cursor: 'help' }}
593
+ onClick={(event) => {
594
+ event.stopPropagation();
595
+ handleOptionLabelClick(optionName);
596
+ }}
597
+ />
598
+ : null;
599
+
576
600
  const renderInfoStrip = (m: TInfoMsg, index: number) => (
577
601
  <ReqoreMessage
578
602
  key={`${m.content}-${index}`}
@@ -688,11 +712,13 @@ export const CompactRow = memo(
688
712
  >
689
713
  {label}
690
714
  {required ? <ReqoreIcon icon='Asterisk' color='danger' size='10px' /> : null}
715
+ {helpIcon}
691
716
  </StyledRowLabel>
692
- {/* Keep the short_desc visible while editing inline when the global
693
- descriptions toggle is on read rows show it, so opening a field
694
- shouldn't make it vanish. */}
695
- {showLabelDesc ?
717
+ {/* An OPEN field always shows its short_desc that's where the hint
718
+ is needed and there's no per-row button to reveal it (the edit
719
+ card does the same). The global descriptions toggle governs READ
720
+ rows only, so collapsing the field is what hides it again. */}
721
+ {labelShortDesc ?
696
722
  <StyledLabelDesc
697
723
  className='options-readfirst-label-desc'
698
724
  size='small'
@@ -753,6 +779,27 @@ export const CompactRow = memo(
753
779
  className={clusterBlockClass || undefined}
754
780
  >
755
781
  {editingRow}
782
+ {/* Focused editing for an INLINE row. The edit card wraps its editor
783
+ in <FocusedEditing>, which renders the field in place AND in the
784
+ modal; an inline row already renders its editor in the row grid,
785
+ so it mounts the modal alone. Without this the More menu's "Edit
786
+ fullscreen" set the state and nothing appeared — the modal simply
787
+ had no mount point on this branch. */}
788
+ {focusedEditing === optionName ?
789
+ <FocusedEditing
790
+ isFullscreen
791
+ modalOnly
792
+ onClose={() => setFocusedEditing(undefined)}
793
+ description={(schema?.display_name as string) || optionName}
794
+ >
795
+ <Description
796
+ longDescription={schema?.desc}
797
+ shortDescription={schema?.short_desc}
798
+ longDescriptionShownByDefault
799
+ />
800
+ {renderOption(optionName, optionField)}
801
+ </FocusedEditing>
802
+ : null}
756
803
  </StyledColumn>
757
804
  );
758
805
  }
@@ -1122,24 +1169,7 @@ export const CompactRow = memo(
1122
1169
  {required ?
1123
1170
  <ReqoreIcon icon='Asterisk' color='danger' size='10px' margin='left' marginSize='tiny' />
1124
1171
  : null}
1125
- {schema?.desc ?
1126
- <ReqoreIcon
1127
- icon='QuestionLine'
1128
- size='12px'
1129
- effect={{ opacity: 0.55 }}
1130
- margin='left'
1131
- marginSize='tiny'
1132
- role='button'
1133
- tabIndex={-1}
1134
- aria-label='Help'
1135
- className='options-readfirst-help'
1136
- style={{ cursor: 'help' }}
1137
- onClick={(event) => {
1138
- event.stopPropagation();
1139
- handleOptionLabelClick(optionName);
1140
- }}
1141
- />
1142
- : null}
1172
+ {helpIcon}
1143
1173
  </span>
1144
1174
  {typeLabel ?
1145
1175
  <ReqoreTag size='tiny' minimal label={typeLabel} labelEffect={{ opacity: 0.55 }} />
@@ -4,15 +4,15 @@ import {
4
4
  ReqoreDropdown,
5
5
  ReqoreInput,
6
6
  } from '@qoretechnologies/reqore';
7
- import { useReqoreTheme } from '@qoretechnologies/reqore/dist/hooks/useTheme';
8
7
  import { IReqoreControlGroupProps } from '@qoretechnologies/reqore/dist/components/ControlGroup';
9
8
  import {
10
9
  IReqoreDropdownItem,
11
10
  TReqoreDropdownItems,
12
11
  } from '@qoretechnologies/reqore/dist/components/Dropdown/list';
12
+ import { useReqoreTheme } from '@qoretechnologies/reqore/dist/hooks/useTheme';
13
13
  import React, { memo } from 'react';
14
- import { useContext } from 'use-context-selector';
15
14
  import styled from 'styled-components';
15
+ import { useContext } from 'use-context-selector';
16
16
  import { CompactToolbarContext, TCompactSort } from './compactToolbarContext';
17
17
 
18
18
  // Field sort modes, listed in the Fields menu's "Sort by" submenu. 'schema' is
@@ -40,7 +40,13 @@ const StyledCompletionLine = styled.div`
40
40
  gap: 10px;
41
41
  flex-wrap: wrap;
42
42
  `;
43
- const StyledMeter = styled.div<{ $set: number; $attention: number; $track: string; $set_c: string; $att_c: string }>`
43
+ const StyledMeter = styled.div<{
44
+ $set: number;
45
+ $attention: number;
46
+ $track: string;
47
+ $set_c: string;
48
+ $att_c: string;
49
+ }>`
44
50
  position: relative;
45
51
  height: 4px;
46
52
  border-radius: 3px;
@@ -180,114 +186,115 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
180
186
  {hasMultipleOptions || (invalidCount && !readOnly) ?
181
187
  <ReqoreControlGroup vertical fluid gapSize='normal'>
182
188
  {hasMultipleOptions ?
183
- <ReqoreControlGroup fluid verticalAlign='center'>
184
- <ReqoreInput
185
- fluid
186
- pill
187
- icon='Search2Line'
188
- iconColor='muted'
189
- placeholder='Filter fields...'
190
- value={compactQuery}
191
- intent={compactQuery ? 'info' : undefined}
192
- className='options-readfirst-search'
193
- onChange={(event: React.FormEvent<HTMLInputElement>) =>
194
- setCompactQuery(event.currentTarget.value)
195
- }
196
- onClearClick={() => setCompactQuery('')}
197
- />
198
- {!readOnly ?
199
- <ReqoreDropdown
200
- fixed
201
- flat
202
- filterable
203
- icon='Filter3Line'
204
- tooltip='Fields'
205
- className='options-readfirst-fields'
206
- intent={requiredOnly ? 'info' : undefined}
207
- badge={requiredOnly ? 'Required only' : undefined}
208
- onItemSelect={(item: IReqoreDropdownItem) =>
209
- item.value && onAddOptionalField(item.value)
210
- }
211
- items={
212
- [
213
- {
214
- label: 'Required only',
215
- selected: requiredOnly,
216
- icon: requiredOnly ? 'CheckboxCircleLine' : 'CheckboxBlankCircleLine',
217
- tooltip: 'Show only required fields',
218
- onClick: () => setRequiredOnly((value) => !value),
219
- },
220
- {
221
- label: 'Show field types',
222
- selected: showFieldTypes,
223
- icon: showFieldTypes ? 'CheckboxCircleLine' : 'CheckboxBlankCircleLine',
224
- tooltip: 'Annotate each field with its type',
225
- onClick: onToggleFieldTypes,
226
- },
227
- // Sort by — a submenu (collapsed by default) so the five modes
228
- // don't crowd the Fields menu. Reorders fields WITHIN each group
229
- // (groups + required-group rails preserved); the active non-
230
- // default mode shows as a badge on the parent.
231
- {
232
- label: 'Sort by',
233
- icon: 'ArrowUpDownLine',
234
- intent: compactSort !== 'schema' ? 'info' : undefined,
235
- badge:
236
- compactSort !== 'schema' ?
237
- SORT_MODES.find((mode) => mode.value === compactSort)?.label
238
- : undefined,
239
- items: SORT_MODES.map((mode) => ({
240
- label: mode.label,
241
- tooltip: mode.tooltip,
242
- selected: compactSort === mode.value,
243
- icon:
244
- compactSort === mode.value ?
245
- 'CheckboxCircleLine'
246
- : 'CheckboxBlankCircleLine',
247
- onClick: () => setCompactSort(mode.value),
248
- })),
249
- },
250
- {
251
- label: 'Select all',
252
- icon: 'MenuAddLine',
253
- tooltip: 'Add every optional field',
254
- disabled: filteredCount === 0,
255
- onClick: onAddAll,
256
- },
257
- {
258
- label: 'Default fields',
259
- icon: 'RestartLine',
260
- tooltip: 'Reset to the default set of fields',
261
- onClick: onResetDefaults,
262
- },
263
- {
264
- label: 'Revert all changes',
265
- icon: 'HistoryLine',
266
- tooltip: 'Undo all edits back to the loaded values',
267
- disabled: !canRevert,
268
- onClick: onRevertAll,
269
- },
270
- // The individual not-yet-added fields are no longer listed here —
271
- // they render as addable rows in the Optional box instead. The
272
- // menu keeps only the bulk actions above (Select all / Default
273
- // fields / filters).
274
- ] as TReqoreDropdownItems
275
- }
276
- />
189
+ <ReqoreControlGroup fluid verticalAlign='center'>
190
+ <ReqoreInput
191
+ fluid
192
+ pill
193
+ icon='Search2Line'
194
+ iconColor='muted'
195
+ placeholder='Filter fields...'
196
+ value={compactQuery}
197
+ intent={compactQuery ? 'info' : undefined}
198
+ className='options-readfirst-search'
199
+ onChange={(event: React.FormEvent<HTMLInputElement>) =>
200
+ setCompactQuery(event.currentTarget.value)
201
+ }
202
+ onClearClick={() => setCompactQuery('')}
203
+ />
204
+ {!readOnly ?
205
+ <ReqoreDropdown
206
+ fixed
207
+ flat
208
+ filterable
209
+ icon='Filter3Line'
210
+ tooltip='Fields'
211
+ className='options-readfirst-fields'
212
+ intent={requiredOnly ? 'info' : undefined}
213
+ badge={requiredOnly ? 'Required only' : undefined}
214
+ onItemSelect={(item: IReqoreDropdownItem) =>
215
+ item.value && onAddOptionalField(item.value)
216
+ }
217
+ items={
218
+ [
219
+ {
220
+ label: 'Required only',
221
+ selected: requiredOnly,
222
+ icon: requiredOnly ? 'CheckboxCircleLine' : 'CheckboxBlankCircleLine',
223
+ tooltip: 'Show only required fields',
224
+ onClick: () => setRequiredOnly((value) => !value),
225
+ },
226
+ {
227
+ label: 'Show field types',
228
+ selected: showFieldTypes,
229
+ icon: showFieldTypes ? 'CheckboxCircleLine' : 'CheckboxBlankCircleLine',
230
+ tooltip: 'Annotate each field with its type',
231
+ onClick: onToggleFieldTypes,
232
+ },
233
+ // Sort by — a submenu (collapsed by default) so the five modes
234
+ // don't crowd the Fields menu. Reorders fields WITHIN each group
235
+ // (groups + required-group rails preserved); the active non-
236
+ // default mode shows as a badge on the parent.
237
+ {
238
+ label: 'Sort by',
239
+ icon: 'ArrowUpDownLine',
240
+ intent: compactSort !== 'schema' ? 'info' : undefined,
241
+ badge:
242
+ compactSort !== 'schema' ?
243
+ SORT_MODES.find((mode) => mode.value === compactSort)?.label
244
+ : undefined,
245
+ items: SORT_MODES.map((mode) => ({
246
+ label: mode.label,
247
+ tooltip: mode.tooltip,
248
+ selected: compactSort === mode.value,
249
+ icon:
250
+ compactSort === mode.value ?
251
+ 'CheckboxCircleLine'
252
+ : 'CheckboxBlankCircleLine',
253
+ onClick: () => setCompactSort(mode.value),
254
+ })),
255
+ },
256
+ {
257
+ label: 'Select all',
258
+ icon: 'MenuAddLine',
259
+ tooltip: 'Add every optional field',
260
+ disabled: filteredCount === 0,
261
+ onClick: onAddAll,
262
+ },
263
+ {
264
+ label: 'Default fields',
265
+ icon: 'RestartLine',
266
+ tooltip: 'Reset to the default set of fields',
267
+ onClick: onResetDefaults,
268
+ },
269
+ {
270
+ label: 'Revert all changes',
271
+ icon: 'HistoryLine',
272
+ tooltip: 'Undo all edits back to the loaded values',
273
+ disabled: !canRevert,
274
+ onClick: onRevertAll,
275
+ },
276
+ // The individual not-yet-added fields are no longer listed here —
277
+ // they render as addable rows in the Optional box instead. The
278
+ // menu keeps only the bulk actions above (Select all / Default
279
+ // fields / filters).
280
+ ] as TReqoreDropdownItems
281
+ }
282
+ />
283
+ : null}
284
+ <ReqoreButton
285
+ fixed
286
+ flat
287
+ minimal={showAllDescriptions === true}
288
+ icon={showAllDescriptions ? 'InformationFill' : 'InformationLine'}
289
+ className='options-readfirst-descriptions'
290
+ intent={showAllDescriptions ? 'info' : undefined}
291
+ tooltip={
292
+ showAllDescriptions ? 'Hide all field information' : 'Show all field information'
293
+ }
294
+ onClick={onToggleAllDescriptions}
295
+ />
296
+ </ReqoreControlGroup>
277
297
  : null}
278
- <ReqoreButton
279
- fixed
280
- flat
281
- minimal={showAllDescriptions === true}
282
- icon={showAllDescriptions ? 'InformationFill' : 'InformationLine'}
283
- className='options-readfirst-descriptions'
284
- active={showAllDescriptions}
285
- intent={showAllDescriptions ? 'info' : undefined}
286
- tooltip={showAllDescriptions ? 'Hide all field information' : 'Show all field information'}
287
- onClick={onToggleAllDescriptions}
288
- />
289
- </ReqoreControlGroup>
290
- : null}
291
298
  </ReqoreControlGroup>
292
299
  : null}
293
300
  </ReqoreControlGroup>
@@ -1680,6 +1680,114 @@ export const Compact: Story = {
1680
1680
  },
1681
1681
  };
1682
1682
 
1683
+ // `name` gains a long-form `desc` so the `?` help affordance has something to
1684
+ // open — the rest of the compact fixture is unchanged.
1685
+ const CompactHelpSchema: Record<string, TCompactField> = {
1686
+ ...CompactSchema,
1687
+ name: {
1688
+ ...CompactSchema.name,
1689
+ desc: 'The unique identifier for this interface. It is used in URLs, logs and cross-references, so it cannot be changed once the interface is deployed.',
1690
+ },
1691
+ };
1692
+
1693
+ export const CompactFocusedEditingInline: Story = {
1694
+ parameters: {
1695
+ docs: {
1696
+ description: {
1697
+ story:
1698
+ 'Renders the compact form, opens the Name row for inline editing and picks "Edit fullscreen" from its More menu — the Focused Editing modal opens over that single scalar field with its long description above the editor.',
1699
+ },
1700
+ },
1701
+ chromatic: { disable: true },
1702
+ },
1703
+ args: {
1704
+ ...Compact.args,
1705
+ options: CompactHelpSchema,
1706
+ },
1707
+ play: async () => {
1708
+ // Name is a scalar, so it edits INLINE in the row (no expanded card) — the
1709
+ // branch whose More menu used to set the focused state with nothing mounted
1710
+ // to render the modal. (CompactFocusedEditing covers the card branch.)
1711
+ await _testsClickText('order-fulfilment');
1712
+ await _testsWaitForInputValue('order-fulfilment', '.options-readfirst-inline .reqore-textarea');
1713
+ await _testsClickButton({ selector: '.options-readfirst-more' });
1714
+ let fsItem: Element | undefined;
1715
+ await waitFor(
1716
+ () => {
1717
+ fsItem = Array.from(document.querySelectorAll('.reqore-menu-item')).find((element) =>
1718
+ element.textContent?.includes('Edit fullscreen')
1719
+ );
1720
+ expect(fsItem).toBeTruthy();
1721
+ },
1722
+ { timeout: 10000 }
1723
+ );
1724
+ await fireEvent.click(fsItem as Element);
1725
+ // The modal mounts, carrying the field's long description with it.
1726
+ await waitFor(() => expect(document.querySelector('.reqore-modal')).toBeTruthy(), {
1727
+ timeout: 10000,
1728
+ });
1729
+ await _testsWaitForText(/unique identifier for this interface/i);
1730
+ },
1731
+ };
1732
+
1733
+ export const CompactEditingShowsDescription: Story = {
1734
+ parameters: {
1735
+ docs: {
1736
+ description: {
1737
+ story:
1738
+ "Renders the compact form with the descriptions toggle off — a collapsed row hides its short description, and opening the row for editing reveals it under the field name along with the `?` help affordance.",
1739
+ },
1740
+ },
1741
+ },
1742
+ args: {
1743
+ ...Compact.args,
1744
+ options: CompactHelpSchema,
1745
+ },
1746
+ play: async () => {
1747
+ // Collapsed: the global descriptions toggle is off, so no short_desc on the
1748
+ // read row (the label carries it as a title attribute only).
1749
+ await _testsWaitForText('order-fulfilment');
1750
+ await _testsWaitForTextToNotExist('Unique identifier for this interface');
1751
+ // Open it: the short description appears without the user clicking anything…
1752
+ await _testsClickText('order-fulfilment');
1753
+ await _testsWaitForInputValue('order-fulfilment', '.options-readfirst-inline .reqore-textarea');
1754
+ await _testsWaitForText('Unique identifier for this interface');
1755
+ // …and the `?` (long-form help) stays reachable while editing.
1756
+ await waitFor(() =>
1757
+ expect(
1758
+ document.querySelector('.options-readfirst-inline .options-readfirst-help')
1759
+ ).toBeTruthy()
1760
+ );
1761
+ },
1762
+ };
1763
+
1764
+ export const CompactWithPanelProps: Story = {
1765
+ parameters: {
1766
+ docs: {
1767
+ description: {
1768
+ story:
1769
+ "Renders the Compact fixture with `compactPanelProps` — the read-first form's outer ReqorePanel is dressed from the outside with a label, icon, intent and an extra header action. The engine's own toolbar survives: `actions` append after it and `contentStyle` merges over the engine's flex-column layout instead of replacing either.",
1770
+ },
1771
+ },
1772
+ },
1773
+ args: {
1774
+ ...Compact.args,
1775
+ compactPanelProps: {
1776
+ label: 'Connection settings',
1777
+ icon: 'Settings3Line',
1778
+ intent: 'info',
1779
+ actions: [{ label: 'Docs', icon: 'BookLine', responsive: false }],
1780
+ },
1781
+ },
1782
+ play: async () => {
1783
+ // The outside-supplied panel chrome renders…
1784
+ await _testsWaitForText('Connection settings');
1785
+ await _testsWaitForText('Docs');
1786
+ // …and the engine's own toolbar + rows are untouched by it.
1787
+ await _testsWaitForText('order-fulfilment');
1788
+ },
1789
+ };
1790
+
1683
1791
  export const CompactReadOnly: Story = {
1684
1792
  parameters: {
1685
1793
  docs: {
@@ -21,13 +21,13 @@ import {
21
21
  } from '@qoretechnologies/reqore/dist/components/Panel';
22
22
  import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
23
23
  import { TReqoreIntent } from '@qoretechnologies/reqore/dist/constants/theme';
24
- import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
25
24
  import {
26
25
  changeDarkness,
27
26
  getMainBackgroundColor,
28
27
  getReadableColor,
29
28
  percentToHexAlpha,
30
29
  } from '@qoretechnologies/reqore/dist/helpers/colors';
30
+ import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
31
31
  import {
32
32
  IQorusFormField,
33
33
  IQorusFormFieldMessage,
@@ -48,11 +48,10 @@ import reduce from 'lodash/reduce';
48
48
  import size from 'lodash/size';
49
49
  import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
50
50
  import { useMeasure, useMount, useUpdateEffect } from 'react-use';
51
- import { createContext } from 'use-context-selector';
52
51
  import styled from 'styled-components';
52
+ import { createContext } from 'use-context-selector';
53
53
  import { getDefaultValue, insertAtIndex, richtextToString } from '../../../helpers/common';
54
54
  import { getRequiredOptionMessage } from '../../../helpers/options';
55
- import { query } from '../../../utils/fetch';
56
55
  import {
57
56
  IValidationResult,
58
57
  hasAllDependenciesFullfilled,
@@ -61,6 +60,7 @@ import {
61
60
  } from '../../../helpers/validations';
62
61
  import { useQorusTypes } from '../../../hooks/useQorusTypes';
63
62
  import { useTemplates } from '../../../hooks/useTemplates';
63
+ import { query } from '../../../utils/fetch';
64
64
  import { Description } from '../../Description';
65
65
  import { FocusedEditing } from '../../FocusedEditing';
66
66
  import { SelectFormField } from '../fields/select/Select';
@@ -72,12 +72,6 @@ import {
72
72
  } from '../fields/template/TemplateField';
73
73
  import { CompactRow } from './CompactRow';
74
74
  import { CompactRowContext, ICompactRowContext } from './compactRowContext';
75
- import { CompactToolbar } from './CompactToolbar';
76
- import {
77
- CompactToolbarContext,
78
- ICompactToolbarContext,
79
- TCompactSort,
80
- } from './compactToolbarContext';
81
75
  import {
82
76
  GROUP_INDENT,
83
77
  LABEL_COL_MAX,
@@ -91,9 +85,16 @@ import {
91
85
  StyledStatusBox,
92
86
  StyledStatusBoxGroupLabel,
93
87
  } from './compactRowStyles';
88
+ import { CompactToolbar } from './CompactToolbar';
89
+ import {
90
+ CompactToolbarContext,
91
+ ICompactToolbarContext,
92
+ TCompactSort,
93
+ } from './compactToolbarContext';
94
94
  import { OptionFieldMessages } from './OptionFieldMessages';
95
95
  import { OptionsHelpDialog } from './OptionsHelpDialog';
96
96
  import {
97
+ TReadFirstStatus,
97
98
  getFirstAttentionOptionName,
98
99
  getOptionGroup,
99
100
  getOptionGroupLabel,
@@ -101,7 +102,6 @@ import {
101
102
  getReadFirstCompletion,
102
103
  getReadFirstStatus,
103
104
  isOptionValueEmpty,
104
- TReadFirstStatus,
105
105
  } from './readFirst';
106
106
 
107
107
  // Re-export types for consumers
@@ -201,13 +201,6 @@ const StyledCompactWrap = styled.div<{ $flush?: boolean }>`
201
201
  engage instead of overflowing the container horizontally. */
202
202
  min-width: 0;
203
203
  max-width: 100%;
204
- /* A bit of horizontal breathing room for the whole form (header + content
205
- alike). Horizontal only — top padding would break the sticky toolbar's
206
- flush pin (see the scroll-context note below). Dropped to flush via the
207
- compactFlush prop, for embeds that own their own gutters (e.g. the
208
- SchemaDefinition tab body). */
209
- padding: ${({ $flush }) => ($flush ? '0' : '0 12px')};
210
-
211
204
  /* Own our scroll context instead of borrowing the host's. The sticky toolbar
212
205
  pins to whatever scrolls; if that scroller carries top padding (e.g. a
213
206
  ReqorePanel/ReqoreContent body), sticky \`top: 0\` resolves against its
@@ -525,6 +518,18 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
525
518
  * transparently inside the parent's edit card. Default `false`.
526
519
  */
527
520
  compactNested?: boolean;
521
+ /**
522
+ * Compact mode only: props forwarded to the read-first form's outer
523
+ * `ReqorePanel` — the panel that carries the sticky toolbar header and holds
524
+ * the status boxes. Spread AFTER the engine's own defaults, so anything set
525
+ * here wins (`flat`, `raised`, `minimal`, `stickyHeader`, `label`, `icon`,
526
+ * `intent`, `padded`, `size`, …). Two props merge instead of replacing, so a
527
+ * consumer can add to the panel without dismantling the form:
528
+ * - `actions` — appended after the engine's toolbar action.
529
+ * - `contentStyle` — merged over the engine's flex-column layout.
530
+ * No-op in classic (non-compact) mode.
531
+ */
532
+ compactPanelProps?: IReqorePanelProps;
528
533
  /** Compact mode only: per-group display metadata (label / icon / subtitle /
529
534
  * order) — the server only sends the bare group key. */
530
535
  groups?: Record<string, IFormEngineGroup>;
@@ -609,6 +614,7 @@ export const FormEngine = ({
609
614
  compact,
610
615
  compactFlush = false,
611
616
  compactNested = false,
617
+ compactPanelProps,
612
618
  commitMode = 'immediate',
613
619
  expandMode = 'single',
614
620
  onCommit,
@@ -1709,8 +1715,13 @@ export const FormEngine = ({
1709
1715
  <>
1710
1716
  {(() => {
1711
1717
  const schemaMsgs = (
1712
- suppressSchemaMessages ? [] : (options?.[optionName] as any)?.messages || []
1713
- ) as { intent?: string; title?: string; content?: string }[];
1718
+ suppressSchemaMessages ?
1719
+ []
1720
+ : (options?.[optionName] as any)?.messages || []) as {
1721
+ intent?: string;
1722
+ title?: string;
1723
+ content?: string;
1724
+ }[];
1714
1725
  if (!schemaMsgs.length) return null;
1715
1726
  const items = schemaMsgs.map(({ intent, title, content }, index) => (
1716
1727
  <ReqoreMessage
@@ -2114,7 +2125,13 @@ export const FormEngine = ({
2114
2125
  ]
2115
2126
  );
2116
2127
 
2117
- const compactHeaderActions = useMemo(() => [{ as: CompactToolbar, responsive: false }], []);
2128
+ // The engine's own toolbar action, plus whatever the consumer added through
2129
+ // `compactPanelProps.actions` — appended, so an outside action can never
2130
+ // knock the form's toolbar out of the header.
2131
+ const compactHeaderActions = useMemo(
2132
+ () => [{ as: CompactToolbar, responsive: false }, ...(compactPanelProps?.actions || [])],
2133
+ [compactPanelProps?.actions]
2134
+ );
2118
2135
  const renderCompact = () => {
2119
2136
  const headerBg = `${changeDarkness(getMainBackgroundColor(theme), 0.02)}${percentToHexAlpha(88)}`;
2120
2137
  // Toolbar filters narrow the listed rows; the meter reflects the full set.
@@ -2357,22 +2374,24 @@ export const FormEngine = ({
2357
2374
  $headerBg={compactNested ? 'transparent' : headerBg}
2358
2375
  $nested={compactNested}
2359
2376
  flat
2377
+ raised
2378
+ minimal
2360
2379
  // No panel background: the form sits transparently on whatever
2361
2380
  // hosts it (page, drawer, or — for an arg_schema field — the
2362
2381
  // parent's edit card) instead of stacking its own dark surface.
2363
2382
  // The status boxes keep their own tints; the sticky toolbar keeps
2364
2383
  // its blurred header via the $headerBg override.
2365
- transparent
2366
2384
  stickyHeader={!compactNested}
2367
- padded={false}
2385
+ // Consumer overrides land last so they win over every default
2386
+ // above; `actions` and `contentStyle` below merge rather than
2387
+ // replace (see `compactPanelProps`).
2388
+ {...compactPanelProps}
2368
2389
  actions={compactHeaderActions}
2369
2390
  contentStyle={{
2370
2391
  display: 'flex',
2371
2392
  flexFlow: 'column',
2372
2393
  gap: '10px',
2373
- // Nested sub-form: no surrounding panel padding (it's flush in
2374
- // the parent card); top-level keeps a small bottom gutter.
2375
- padding: compactNested ? '0' : '0 0 12px',
2394
+ ...compactPanelProps?.contentStyle,
2376
2395
  }}
2377
2396
  >
2378
2397
  {size(groupKeys) === 0 ?