@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
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Shared row model for the FormEngine compact VARIANTS playground.
3
+ *
4
+ * This is a prototyping harness: every variant renders the SAME normalized rows
5
+ * (derived from a real options schema + values via the real `readFirst`
6
+ * formatters) and differs only in layout / chrome / UX. That keeps the
7
+ * comparison about presentation, not data. Editing is intentionally stubbed
8
+ * (expand shows a placeholder) — the point is to compare the read-first view
9
+ * that currently feels crowded, then graft the winner onto the real engine.
10
+ */
11
+ import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
12
+ import {
13
+ colorToCss,
14
+ formatBytes,
15
+ formatOptionValue,
16
+ getFileSize,
17
+ getOptionGroup,
18
+ getOptionGroupLabel,
19
+ getValueType,
20
+ isOptionValueEmpty,
21
+ } from '../readFirst';
22
+ import { isValueTemplate } from '../../../../helpers/templates';
23
+
24
+ export type TVariantStatus =
25
+ | 'set' // has a valid value
26
+ | 'unset' // empty, not required — calm, no alarm
27
+ | 'todo' // required (or required-group) but unset — gentle amber
28
+ | 'invalid'; // user-entered value fails validation — red (only when surfaced)
29
+
30
+ export type TVariantValueKind =
31
+ | 'text'
32
+ | 'number'
33
+ | 'bool'
34
+ | 'color'
35
+ | 'file'
36
+ | 'template'
37
+ | 'richtext'
38
+ | 'hash'
39
+ | 'empty';
40
+
41
+ export interface IVariantValue {
42
+ kind: TVariantValueKind;
43
+ /** Human-readable summary (already formatted by readFirst). */
44
+ display: string;
45
+ /** color: css color; file: size label; hash: field count; template: label */
46
+ extra?: string;
47
+ }
48
+
49
+ export interface IVariantRow {
50
+ name: string;
51
+ label: string;
52
+ shortDesc?: string;
53
+ longDesc?: string;
54
+ required: boolean;
55
+ readOnly: boolean;
56
+ status: TVariantStatus;
57
+ /** Short reason for todo/invalid, e.g. "Text value is empty". */
58
+ reason?: string;
59
+ value: IVariantValue;
60
+ typeLabel: string;
61
+ /** required_groups this field belongs to (one-of constraints). */
62
+ requiredGroups: string[];
63
+ /** True when this field is a member of an unsatisfied one-of group. */
64
+ inUnmetGroup: boolean;
65
+ /** Raw schema + value object — passed to AutoFormField for REAL editing. */
66
+ schema: any;
67
+ field: any;
68
+ }
69
+
70
+ export interface IVariantGroup {
71
+ name: string;
72
+ label: string;
73
+ rows: IVariantRow[];
74
+ }
75
+
76
+ /** One-of required group: members + whether any is filled. */
77
+ export interface IRequiredGroupInfo {
78
+ key: string;
79
+ members: string[];
80
+ memberLabels: string[];
81
+ satisfied: boolean;
82
+ }
83
+
84
+ export interface IVariantConfig {
85
+ groupLabels?: Record<string, string>;
86
+ /** name → reason; in the engine this comes from the validity pass. */
87
+ invalidReasons?: Record<string, string>;
88
+ }
89
+
90
+ // Default (basic-fixture) invalid set — overridable via config.
91
+ const DEFAULT_INVALID: Record<string, string> = {
92
+ optionWithInvalidValue: 'Text value is empty',
93
+ schemaOption: 'Hash arguments are invalid',
94
+ };
95
+
96
+ function buildValue(field: IQorusFormField | undefined, schema: any): IVariantValue {
97
+ if (!field || isOptionValueEmpty(field.value)) return { kind: 'empty', display: '' };
98
+
99
+ const valueType = getValueType(field, schema);
100
+ const formatted = formatOptionValue(field, schema);
101
+
102
+ if (valueType === 'rgbcolor') {
103
+ return { kind: 'color', display: formatted, extra: colorToCss(field.value) };
104
+ }
105
+ if (valueType === 'file') {
106
+ const size = getFileSize(field.value);
107
+ return {
108
+ kind: 'file',
109
+ display: formatted,
110
+ extra: size !== undefined ? formatBytes(size) : undefined,
111
+ };
112
+ }
113
+ if (valueType === 'bool' || valueType === 'boolean') {
114
+ return { kind: 'bool', display: formatted };
115
+ }
116
+ if (valueType === 'hash' || valueType === 'free-hash') {
117
+ return { kind: 'hash', display: formatted };
118
+ }
119
+ if (typeof field.value === 'string' && isValueTemplate(field.value)) {
120
+ // The real chip resolves the template's display name from the templates
121
+ // list; the prototype shows a friendly label of the key.
122
+ const label =
123
+ field.value.replace(/^\$[a-z]+:/i, '').replace(/[-_]/g, ' ') || field.value;
124
+ return { kind: 'template', display: label.replace(/\b\w/g, (c) => c.toUpperCase()), extra: 'local' };
125
+ }
126
+ if (valueType === 'richtext' && Array.isArray(field.value)) {
127
+ return { kind: 'richtext', display: formatted };
128
+ }
129
+ return { kind: 'text', display: formatted };
130
+ }
131
+
132
+ export function buildVariantGroups(
133
+ options: IQorusFormSchema,
134
+ values: Record<string, IQorusFormField>,
135
+ config: IVariantConfig = {}
136
+ ): { groups: IVariantGroup[]; requiredGroups: Record<string, IRequiredGroupInfo> } {
137
+ const invalidReasons = config.invalidReasons ?? DEFAULT_INVALID;
138
+ const groupMap = new Map<string, IVariantGroup>();
139
+
140
+ // Pass 1: collect one-of required-group membership + satisfaction.
141
+ const reqGroups: Record<string, IRequiredGroupInfo> = {};
142
+ Object.entries(options).forEach(([name, schema]: [string, any]) => {
143
+ (schema?.required_groups || []).forEach((key: string) => {
144
+ if (!reqGroups[key]) reqGroups[key] = { key, members: [], memberLabels: [], satisfied: false };
145
+ reqGroups[key].members.push(name);
146
+ reqGroups[key].memberLabels.push((schema?.display_name as string) || name);
147
+ if (!isOptionValueEmpty(values[name]?.value)) reqGroups[key].satisfied = true;
148
+ });
149
+ });
150
+
151
+ Object.entries(options).forEach(([name, schema]: [string, any]) => {
152
+ const field = values[name];
153
+ const empty = isOptionValueEmpty(field?.value);
154
+ const ownGroups: string[] = schema?.required_groups || [];
155
+ // Member of a one-of group that nobody has satisfied yet.
156
+ const inUnmetGroup = ownGroups.some((k) => reqGroups[k] && !reqGroups[k].satisfied);
157
+ const required = !!schema?.required;
158
+
159
+ let status: TVariantStatus;
160
+ let reason: string | undefined;
161
+ if (invalidReasons[name]) {
162
+ status = 'invalid';
163
+ reason = invalidReasons[name];
164
+ } else if (empty && required) {
165
+ status = 'todo';
166
+ reason = 'Needs a value';
167
+ } else if (empty && inUnmetGroup) {
168
+ status = 'todo';
169
+ reason = `One of ${reqGroups[ownGroups[0]].memberLabels.join(' / ')}`;
170
+ } else if (empty) {
171
+ status = 'unset';
172
+ } else {
173
+ status = 'set';
174
+ }
175
+
176
+ const groupName = getOptionGroup(schema);
177
+ const groupLabel = config.groupLabels?.[groupName] ?? getOptionGroupLabel(groupName, undefined);
178
+ if (!groupMap.has(groupName)) {
179
+ groupMap.set(groupName, { name: groupName, label: groupLabel, rows: [] });
180
+ }
181
+ groupMap.get(groupName)!.rows.push({
182
+ name,
183
+ label: (schema?.display_name as string) || name,
184
+ shortDesc: schema?.short_desc,
185
+ longDesc: schema?.desc,
186
+ required: required || ownGroups.length > 0,
187
+ readOnly: !!schema?.readonly,
188
+ status,
189
+ reason,
190
+ value: buildValue(field, schema),
191
+ typeLabel: `${(schema?.ui_type as string) || (schema?.type as string) || 'auto'}`,
192
+ requiredGroups: ownGroups,
193
+ inUnmetGroup,
194
+ schema,
195
+ field,
196
+ });
197
+ });
198
+
199
+ // Stable group order: 'general' first, then the rest, 'optional' last.
200
+ const groups = Array.from(groupMap.values()).sort((a, b) => {
201
+ const rank = (g: string) => (g === 'general' ? 0 : g === 'optional' ? 2 : 1);
202
+ return rank(a.name) - rank(b.name);
203
+ });
204
+ return { groups, requiredGroups: reqGroups };
205
+ }
206
+
207
+ export function summarize(groups: IVariantGroup[]) {
208
+ const rows = groups.flatMap((g) => g.rows);
209
+ const total = rows.length;
210
+ const set = rows.filter((r) => r.status === 'set').length;
211
+ const todo = rows.filter((r) => r.status === 'todo').length;
212
+ const invalid = rows.filter((r) => r.status === 'invalid').length;
213
+ const attention = todo + invalid;
214
+ const pct = total ? Math.round((set / total) * 100) : 0;
215
+ return { total, set, todo, invalid, attention, pct };
216
+ }
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Shared rendering parts for the FormEngine compact VARIANTS playground.
3
+ * Value rendering is shared so every variant shows identical data; the `tone`
4
+ * prop lets a variant choose a calm (desaturated) or vivid chip treatment.
5
+ */
6
+ import {
7
+ ReqoreButton,
8
+ ReqoreCollapsibleContent,
9
+ ReqoreControlGroup,
10
+ ReqoreIcon,
11
+ ReqoreInput,
12
+ ReqoreTag,
13
+ } from '@qoretechnologies/reqore';
14
+ import { useReqoreTheme } from '@qoretechnologies/reqore/dist/hooks/useTheme';
15
+ import React from 'react';
16
+ import { AutoFormField } from '../../fields/auto/AutoFormField';
17
+ import { StructuredDataView } from '../_structuredData/StructuredDataView';
18
+ import {
19
+ buildVariantGroups,
20
+ summarize,
21
+ IVariantGroup,
22
+ IVariantRow,
23
+ IVariantValue,
24
+ TVariantStatus,
25
+ } from './variantModel';
26
+
27
+ export function useVariantColors() {
28
+ const theme = useReqoreTheme();
29
+ const intents = (theme.intents || {}) as Record<string, string>;
30
+ return {
31
+ main: theme.main as string,
32
+ text: (theme.text?.color as string) || '#e8e8e8',
33
+ muted: `${(theme.text?.color as string) || '#e8e8e8'}99`,
34
+ faint: `${(theme.text?.color as string) || '#e8e8e8'}55`,
35
+ line: `${(theme.text?.color as string) || '#ffffff'}14`,
36
+ hover: `${(theme.text?.color as string) || '#ffffff'}0c`,
37
+ surface: `${(theme.text?.color as string) || '#ffffff'}08`,
38
+ danger: intents.danger || '#a82a2a',
39
+ warning: intents.warning || '#d17c29',
40
+ success: intents.success || '#4a7110',
41
+ info: intents.info || '#3b7bbf',
42
+ custom1: intents.custom1 || '#762f7e',
43
+ };
44
+ }
45
+
46
+ export const STATUS_COLOR = (
47
+ status: TVariantStatus,
48
+ c: ReturnType<typeof useVariantColors>
49
+ ) =>
50
+ status === 'invalid' ? c.danger
51
+ : status === 'todo' ? c.warning
52
+ : status === 'set' ? c.success
53
+ : c.faint;
54
+
55
+ /** A single status mark — one dot, color = severity. Replaces stripe+box+icon. */
56
+ export const StatusDot = ({
57
+ status,
58
+ size = 7,
59
+ }: {
60
+ status: TVariantStatus;
61
+ size?: number;
62
+ }) => {
63
+ const c = useVariantColors();
64
+ if (status === 'unset') return null;
65
+ return (
66
+ <span
67
+ aria-hidden
68
+ style={{
69
+ width: size,
70
+ height: size,
71
+ borderRadius: '50%',
72
+ flex: '0 0 auto',
73
+ background: STATUS_COLOR(status, c),
74
+ boxShadow: status !== 'set' ? `0 0 0 3px ${STATUS_COLOR(status, c)}22` : undefined,
75
+ }}
76
+ />
77
+ );
78
+ };
79
+
80
+ /** The value summary. `tone='calm'` desaturates template/richtext chips. */
81
+ export const ValueView = ({
82
+ value,
83
+ tone = 'calm',
84
+ }: {
85
+ value: IVariantValue;
86
+ tone?: 'calm' | 'vivid';
87
+ }) => {
88
+ const c = useVariantColors();
89
+ const ellipsis: React.CSSProperties = {
90
+ overflow: 'hidden',
91
+ textOverflow: 'ellipsis',
92
+ whiteSpace: 'nowrap',
93
+ minWidth: 0,
94
+ };
95
+
96
+ switch (value.kind) {
97
+ case 'empty':
98
+ // A quiet dash reads as "no value" without shouting "Not set" on every row.
99
+ return <span style={{ color: c.faint }}>—</span>;
100
+
101
+ case 'color':
102
+ return (
103
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
104
+ <span
105
+ aria-hidden
106
+ style={{
107
+ width: 12,
108
+ height: 12,
109
+ borderRadius: 3,
110
+ flex: '0 0 auto',
111
+ background: value.extra,
112
+ border: `1px solid ${c.line}`,
113
+ }}
114
+ />
115
+ <span style={ellipsis}>{value.display}</span>
116
+ </span>
117
+ );
118
+
119
+ case 'file':
120
+ return (
121
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
122
+ <ReqoreIcon icon='FileLine' size='13px' style={{ color: c.muted, flexShrink: 0 }} />
123
+ <span style={ellipsis}>{value.display}</span>
124
+ {value.extra ?
125
+ <span style={{ color: c.faint, fontSize: 11, flexShrink: 0 }}>{value.extra}</span>
126
+ : null}
127
+ </span>
128
+ );
129
+
130
+ case 'bool':
131
+ return <span>{value.display}</span>;
132
+
133
+ case 'hash':
134
+ return (
135
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: c.muted }}>
136
+ <ReqoreIcon icon='BracesLine' size='12px' style={{ color: c.faint }} />
137
+ <span>{value.display}</span>
138
+ </span>
139
+ );
140
+
141
+ case 'template':
142
+ return tone === 'vivid' ?
143
+ <ReqoreTag size='small' intent='info' icon='ExchangeDollarLine' label={value.display} />
144
+ : <span
145
+ style={{
146
+ display: 'inline-flex',
147
+ alignItems: 'center',
148
+ gap: 5,
149
+ padding: '1px 7px',
150
+ borderRadius: 4,
151
+ background: c.surface,
152
+ border: `1px solid ${c.line}`,
153
+ color: c.text,
154
+ fontSize: 12,
155
+ maxWidth: '100%',
156
+ ...ellipsis,
157
+ }}
158
+ >
159
+ <ReqoreIcon icon='ExchangeDollarLine' size='11px' style={{ color: c.custom1, flexShrink: 0 }} />
160
+ <span style={ellipsis}>{value.display}</span>
161
+ </span>;
162
+
163
+ case 'richtext':
164
+ return <span style={ellipsis}>{value.display}</span>;
165
+
166
+ default:
167
+ return <span style={ellipsis}>{value.display}</span>;
168
+ }
169
+ };
170
+
171
+ /** A reusable description disclosure used by every variant (tap-friendly).
172
+ * Unifies short_desc (inline) + long desc into ONE control. */
173
+ export const useDisclosure = () => {
174
+ const [open, setOpen] = React.useState<Record<string, boolean>>({});
175
+ return {
176
+ isOpen: (id: string) => !!open[id],
177
+ toggle: (id: string) => setOpen((p) => ({ ...p, [id]: !p[id] })),
178
+ };
179
+ };
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Shared form state: filter (all / needs-attention), add-field, inline editing.
183
+ // Each variant calls useVariantForm and renders <VariantToolbar> + <InlineEdit>.
184
+ // ---------------------------------------------------------------------------
185
+ export type TVariantFilter = 'all' | 'attention';
186
+
187
+ export function useVariantForm(options: any, values: any, config?: any) {
188
+ const [filter, setFilter] = React.useState<TVariantFilter>('all');
189
+ const [query, setQuery] = React.useState('');
190
+ const [showDescriptions, setShowDescriptions] = React.useState(false);
191
+ const [editing, setEditing] = React.useState<string | null>(null);
192
+
193
+ const { groups, requiredGroups } = React.useMemo(
194
+ () => buildVariantGroups(options, values, config),
195
+ [options, values, config]
196
+ );
197
+ const summary = React.useMemo(() => summarize(groups), [groups]);
198
+
199
+ // Filtered view: text query (label match) + 'attention' (todo/invalid only).
200
+ const visibleGroups: IVariantGroup[] = React.useMemo(() => {
201
+ const q = query.trim().toLowerCase();
202
+ return groups
203
+ .map((g) => ({
204
+ ...g,
205
+ rows: g.rows.filter(
206
+ (r) =>
207
+ (filter === 'all' || r.status === 'todo' || r.status === 'invalid') &&
208
+ (!q || r.label.toLowerCase().includes(q) || r.name.toLowerCase().includes(q))
209
+ ),
210
+ }))
211
+ .filter((g) => g.rows.length);
212
+ }, [groups, filter, query]);
213
+
214
+ return {
215
+ groups,
216
+ visibleGroups,
217
+ requiredGroups,
218
+ summary,
219
+ filter,
220
+ setFilter,
221
+ toggleAttention: () => setFilter((f) => (f === 'attention' ? 'all' : 'attention')),
222
+ query,
223
+ setQuery,
224
+ showDescriptions,
225
+ toggleDescriptions: () => setShowDescriptions((v) => !v),
226
+ editing,
227
+ startEdit: (name: string) => setEditing((cur) => (cur === name ? null : name)),
228
+ stopEdit: () => setEditing(null),
229
+ };
230
+ }
231
+
232
+ export type TVariantForm = ReturnType<typeof useVariantForm>;
233
+
234
+ /** Compact toolbar: just a prominent filter field. The "needs attention" control
235
+ * lives in each variant's header (the count line is the link); all optional
236
+ * fields render inline in the Optional group, so there's no "add field" step. */
237
+ export const VariantToolbar = ({ form }: { form: TVariantForm }) => {
238
+ const c = useVariantColors();
239
+ const { summary, query, setQuery, showDescriptions, toggleDescriptions } = form;
240
+ return (
241
+ <ReqoreControlGroup verticalAlign='center' fluid>
242
+ <ReqoreInput
243
+ placeholder='Filter fields…'
244
+ icon='Search2Line'
245
+ iconColor='muted'
246
+ pill
247
+ fluid
248
+ value={query}
249
+ onChange={(e: any) => setQuery(e.currentTarget.value)}
250
+ onClearClick={() => setQuery('')}
251
+ />
252
+ {/* Show short descriptions under every field — like the compact engine's
253
+ "Show all descriptions" toggle. Icon-only; info-intent when active. */}
254
+ <ReqoreButton
255
+ fixed
256
+ flat
257
+ minimal
258
+ intent={showDescriptions ? 'info' : undefined}
259
+ active={showDescriptions}
260
+ icon={showDescriptions ? 'InformationFill' : 'InformationLine'}
261
+ tooltip={showDescriptions ? 'Hide field descriptions' : 'Show field descriptions'}
262
+ onClick={toggleDescriptions}
263
+ />
264
+ <span style={{ color: c.faint, fontSize: 12, paddingLeft: 4, whiteSpace: 'nowrap' }}>
265
+ {summary.set}/{summary.total} set
266
+ </span>
267
+ </ReqoreControlGroup>
268
+ );
269
+ };
270
+
271
+ /** Complex value preview — the SAME nested tree + "Show more" fade the compact
272
+ * engine uses, so hash/list fields show their contents immediately. */
273
+ export const ComplexPreview = ({ value, onOpen }: { value: unknown; onOpen?: () => void }) => (
274
+ <ReqoreCollapsibleContent maxCollapsedHeight={120}>
275
+ <div onClick={(e) => e.stopPropagation()}>
276
+ <StructuredDataView
277
+ value={value}
278
+ collapsibleRoot={false}
279
+ defaultExpandDepth={2}
280
+ onItemClick={onOpen}
281
+ />
282
+ </div>
283
+ </ReqoreCollapsibleContent>
284
+ );
285
+
286
+ /** Inline editor: mounts the REAL field editor (AutoFormField) for the row's
287
+ * schema + value, so clicking a field edits it for real (string, number, bool,
288
+ * colour, file, richtext, list, date, …). Local state only — the prototype
289
+ * doesn't persist; in the engine this flows through handleValueChange. */
290
+ export const InlineEdit = ({ row, onDone }: { row: IVariantRow; onDone: () => void }) => {
291
+ const [value, setValue] = React.useState(row.field?.value);
292
+ return (
293
+ <div
294
+ style={{ gridColumn: '1 / -1', display: 'flex', flexFlow: 'column', gap: 8, padding: '10px 0 12px' }}
295
+ onClick={(e) => e.stopPropagation()}
296
+ >
297
+ <AutoFormField
298
+ {...(row.schema || {})}
299
+ name={row.name}
300
+ value={value}
301
+ default_value={row.schema?.default_value}
302
+ onChange={(_n: string, v: any) => setValue(v)}
303
+ fluid
304
+ size='small'
305
+ />
306
+ <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
307
+ <ReqoreButton size='small' intent='success' icon='CheckLine' fixed onClick={onDone}>
308
+ Done
309
+ </ReqoreButton>
310
+ </div>
311
+ </div>
312
+ );
313
+ };
@@ -190,9 +190,16 @@ export const ViaFormEngine: Story = {
190
190
  },
191
191
  async play({ canvasElement }) {
192
192
  const canvas = within(canvasElement);
193
- await expect(await canvas.findByText('My Auto Field')).toBeInTheDocument();
193
+ // Generous timeout: findByText defaults to 1s, which flakes under CI load
194
+ // while the engine boots the auto field's type picker (the rest of the suite
195
+ // waits ~10s).
196
+ await expect(
197
+ await canvas.findByText('My Auto Field', undefined, { timeout: 10000 })
198
+ ).toBeInTheDocument();
194
199
  // The auto field renders its type picker inside the engine-driven form.
195
- await expect(await canvas.findByText('Please select data type')).toBeInTheDocument();
200
+ await expect(
201
+ await canvas.findByText('Please select data type', undefined, { timeout: 10000 })
202
+ ).toBeInTheDocument();
196
203
  },
197
204
  };
198
205
 
@@ -71,6 +71,9 @@ export interface IAutoFieldProps
71
71
  uniqueName?: string;
72
72
 
73
73
  arg_schema?: string | IOptionsSchema;
74
+ /** Render the arg_schema sub-form in compact (read-first) mode, matching the
75
+ * parent engine. */
76
+ compact?: boolean;
74
77
  path?: string;
75
78
  column?: boolean;
76
79
  level?: number;
@@ -137,6 +140,7 @@ function AutoField<T = any>({
137
140
  noSoft,
138
141
  path,
139
142
  arg_schema,
143
+ compact,
140
144
  column,
141
145
  level = 0,
142
146
  canBeNull,
@@ -540,6 +544,10 @@ function AutoField<T = any>({
540
544
  <FormEngine
541
545
  wrapperPadding='top'
542
546
  flat
547
+ compact={compact}
548
+ // Embedded sub-form: no scroll context of its own, so its toolbar
549
+ // isn't sticky and its header stays transparent (no dark backdrop).
550
+ compactNested
543
551
  name={name}
544
552
  uniqueName={uniqueName}
545
553
  options={finalArgSchema}