@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,119 @@
1
+ import { ReqoreP, ReqorePanel } from '@qoretechnologies/reqore';
2
+ import { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { IQorusFormField } from '@qoretechnologies/ts-toolkit';
4
+ import { basicFormValue, getBasicFormOptions } from '../__fixtures__/basicFormOptions';
5
+ import {
6
+ focusDemoGroupLabels,
7
+ focusDemoInvalid,
8
+ focusDemoValue,
9
+ getFocusDemoOptions,
10
+ } from './focusDemo';
11
+ import { VariantCalmTable } from './VariantCalmTable';
12
+ import { VariantCards } from './VariantCards';
13
+ import { VariantFocus } from './VariantFocus';
14
+ import { VariantMinimal } from './VariantMinimal';
15
+
16
+ /**
17
+ * VARIANTS PLAYGROUND for the compact FormEngine read-first view.
18
+ * All four render the SAME schema + values as `Form/Engine/FormEngine › Basic`
19
+ * (the reference screenshot), so the comparison is purely about layout / chrome
20
+ * / UX. Resize the Storybook viewport (or the canvas) to ~390px to check phone.
21
+ * See design/FORM_ENGINE_COMPACT_UX_PLAN.md for the rationale behind each.
22
+ */
23
+ const options = getBasicFormOptions();
24
+
25
+ // Merge declared default_values into the values map so "set via default" fields
26
+ // (number=42, bool=true, the file) render as set — matching the real engine.
27
+ const values: Record<string, IQorusFormField> = (() => {
28
+ const merged: Record<string, IQorusFormField> = { ...(basicFormValue as any) };
29
+ Object.entries(options).forEach(([name, schema]: [string, any]) => {
30
+ if (!merged[name] && schema?.default_value) merged[name] = schema.default_value;
31
+ });
32
+ return merged;
33
+ })();
34
+
35
+ // Fill the available width (the IDE form panel context); no artificial cap.
36
+ // Focus uses the richer demo schema (named groups, a one-of required group,
37
+ // complex nested fields) to show full engine parity.
38
+ const focusOptions = getFocusDemoOptions();
39
+ const focusValues: Record<string, IQorusFormField> = (() => {
40
+ const merged: Record<string, IQorusFormField> = { ...focusDemoValue };
41
+ Object.entries(focusOptions).forEach(([name, schema]: [string, any]) => {
42
+ if (!merged[name] && schema?.default_value) merged[name] = schema.default_value;
43
+ });
44
+ return merged;
45
+ })();
46
+ const focusConfig = { groupLabels: focusDemoGroupLabels, invalidReasons: focusDemoInvalid };
47
+
48
+ const Frame = ({ children }: { children: React.ReactNode }) => (
49
+ <div style={{ width: '100%', padding: '16px 32px', boxSizing: 'border-box' }}>{children}</div>
50
+ );
51
+
52
+ const meta = {
53
+ title: 'Form/Engine/FormEngine Variants',
54
+ parameters: { layout: 'fullscreen', chromatic: { viewports: [1440, 390] } },
55
+ } satisfies Meta;
56
+
57
+ export default meta;
58
+ type Story = StoryObj<typeof meta>;
59
+
60
+ export const V1CalmTable: Story = {
61
+ name: '1 · Calm Table',
62
+ render: () => (
63
+ <Frame>
64
+ <VariantCalmTable options={options} values={values} />
65
+ </Frame>
66
+ ),
67
+ };
68
+
69
+ export const V2Cards: Story = {
70
+ name: '2 · Cards',
71
+ render: () => (
72
+ <Frame>
73
+ <VariantCards options={options} values={values} />
74
+ </Frame>
75
+ ),
76
+ };
77
+
78
+ export const V3Focus: Story = {
79
+ name: '3 · Focus',
80
+ render: () => (
81
+ <Frame>
82
+ <VariantFocus options={focusOptions} values={focusValues} config={focusConfig} />
83
+ </Frame>
84
+ ),
85
+ };
86
+
87
+ export const V4Minimal: Story = {
88
+ name: '4 · Minimal',
89
+ render: () => (
90
+ <Frame>
91
+ <VariantMinimal options={options} values={values} />
92
+ </Frame>
93
+ ),
94
+ };
95
+
96
+ /** All four stacked, labelled — for a quick scroll-through comparison. */
97
+ export const CompareAll: Story = {
98
+ name: '★ Compare all',
99
+ render: () => {
100
+ const items: [string, React.ReactNode][] = [
101
+ ['1 · Calm Table', <VariantCalmTable key='v1' options={options} values={values} />],
102
+ ['2 · Cards', <VariantCards key='v2' options={options} values={values} />],
103
+ ['3 · Focus', <VariantFocus key='v3' options={focusOptions} values={focusValues} config={focusConfig} />],
104
+ ['4 · Minimal', <VariantMinimal key='v4' options={options} values={values} />],
105
+ ];
106
+ return (
107
+ <div style={{ maxWidth: 1100, margin: '0 auto', padding: 16, display: 'flex', flexFlow: 'column', gap: 28 }}>
108
+ {items.map(([label, node]) => (
109
+ <ReqorePanel key={label} label={label} flat minimal collapsible>
110
+ <div style={{ padding: 8 }}>{node}</div>
111
+ </ReqorePanel>
112
+ ))}
113
+ <ReqoreP size='small' effect={{ opacity: 0.5 }}>
114
+ Same data as “FormEngine › Basic”. Resize the viewport to ~390px to test phone layout.
115
+ </ReqoreP>
116
+ </div>
117
+ );
118
+ },
119
+ };
@@ -0,0 +1,242 @@
1
+ /**
2
+ * VARIANT 1 — "Calm Table"
3
+ *
4
+ * Direction: treat it like a clean data table. One status channel (segmented
5
+ * meter), one mark per row (a status dot), whitespace instead of borders/boxes.
6
+ * Errors are quiet inline text, not full-width red boxes. WHOLE-ROW hover.
7
+ * ONE unified description control (tap to reveal short+long inline). Click a row
8
+ * to edit it inline. Responsive: collapses to stacked label/value on phone.
9
+ */
10
+ import { ReqoreIcon, ReqoreP } from '@qoretechnologies/reqore';
11
+ import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
12
+ import styled from 'styled-components';
13
+ import {
14
+ InlineEdit,
15
+ StatusDot,
16
+ TVariantForm,
17
+ ValueView,
18
+ VariantToolbar,
19
+ useDisclosure,
20
+ useVariantColors,
21
+ useVariantForm,
22
+ } from './variantParts';
23
+
24
+ const Wrap = styled.div<{ $line: string; $hover: string; $muted: string; $faint: string }>`
25
+ display: flex;
26
+ flex-flow: column;
27
+ gap: 22px;
28
+ font-size: 13px;
29
+
30
+ .vct-row {
31
+ display: grid;
32
+ grid-template-columns: minmax(180px, 320px) minmax(0, 1fr) auto;
33
+ align-items: center;
34
+ gap: 18px;
35
+ min-height: 40px;
36
+ padding: 6px 10px;
37
+ border-radius: 8px;
38
+ cursor: pointer;
39
+ transition: background 0.12s ease;
40
+ }
41
+ /* WHOLE-row hover (fixes: hover used to only tint the label cell) */
42
+ .vct-row:hover,
43
+ .vct-row:focus-within {
44
+ background: ${({ $hover }) => $hover};
45
+ }
46
+ .vct-label {
47
+ font-weight: 600;
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 6px;
51
+ min-width: 0;
52
+ }
53
+ .vct-actions {
54
+ display: inline-flex;
55
+ align-items: center;
56
+ gap: 10px;
57
+ opacity: 0.85;
58
+ }
59
+ .vct-desc {
60
+ grid-column: 1 / -1;
61
+ padding: 2px 2px 6px 0;
62
+ color: ${({ $muted }) => $muted};
63
+ }
64
+ .vct-reason {
65
+ font-size: 12px;
66
+ margin-left: 8px;
67
+ }
68
+ .vct-info {
69
+ background: none;
70
+ border: none;
71
+ color: ${({ $faint }) => $faint};
72
+ cursor: pointer;
73
+ padding: 4px;
74
+ display: inline-flex;
75
+ border-radius: 6px;
76
+ }
77
+ .vct-info:hover {
78
+ color: ${({ $muted }) => $muted};
79
+ }
80
+
81
+ @media (max-width: 620px) {
82
+ .vct-row {
83
+ grid-template-columns: 1fr auto;
84
+ grid-template-areas: 'label actions' 'value value';
85
+ row-gap: 2px;
86
+ }
87
+ .vct-label {
88
+ grid-area: label;
89
+ }
90
+ .vct-value {
91
+ grid-area: value;
92
+ }
93
+ .vct-actions {
94
+ grid-area: actions;
95
+ }
96
+ }
97
+ `;
98
+
99
+ const Meter = ({ form }: { form: TVariantForm }) => {
100
+ const c = useVariantColors();
101
+ const s = form.summary;
102
+ const filtering = form.filter === 'attention';
103
+ return (
104
+ <div style={{ display: 'flex', flexFlow: 'column', gap: 8 }}>
105
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: 12, color: c.muted }}>
106
+ <ReqoreP
107
+ size='small'
108
+ effect={{
109
+ uppercase: true,
110
+ spaced: 1,
111
+ weight: 'bold',
112
+ color: (s.attention ? c.warning : c.success) as never,
113
+ }}
114
+ >
115
+ {s.attention ? 'Draft' : 'Ready'}
116
+ </ReqoreP>
117
+ <span>
118
+ {s.set} of {s.total} set
119
+ </span>
120
+ {s.attention ?
121
+ <button
122
+ type='button'
123
+ onClick={form.toggleAttention}
124
+ style={{
125
+ marginLeft: 'auto',
126
+ background: 'none',
127
+ border: 'none',
128
+ cursor: 'pointer',
129
+ color: c.warning,
130
+ fontSize: 12,
131
+ textDecoration: 'underline',
132
+ textUnderlineOffset: 3,
133
+ }}
134
+ >
135
+ {filtering ? '← show all fields' : `${s.attention} need attention →`}
136
+ </button>
137
+ : <span style={{ color: c.success, marginLeft: 'auto' }}>All clear</span>}
138
+ </div>
139
+ <div style={{ display: 'flex', height: 6, borderRadius: 3, overflow: 'hidden', background: c.line }}>
140
+ <div style={{ width: `${(s.set / s.total) * 100}%`, background: c.success }} />
141
+ <div style={{ width: `${(s.attention / s.total) * 100}%`, background: c.warning }} />
142
+ </div>
143
+ </div>
144
+ );
145
+ };
146
+
147
+ export const VariantCalmTable = ({
148
+ options,
149
+ values,
150
+ }: {
151
+ options: IQorusFormSchema;
152
+ values: Record<string, IQorusFormField>;
153
+ }) => {
154
+ const c = useVariantColors();
155
+ const form = useVariantForm(options, values);
156
+ const disc = useDisclosure();
157
+
158
+ return (
159
+ <Wrap $line={c.line} $hover={c.hover} $muted={c.muted} $faint={c.faint}>
160
+ <Meter form={form} />
161
+ <VariantToolbar form={form} />
162
+ {form.visibleGroups.map((g) => (
163
+ <div key={g.name} style={{ display: 'flex', flexFlow: 'column', gap: 2 }}>
164
+ <div
165
+ style={{
166
+ display: 'flex',
167
+ alignItems: 'center',
168
+ gap: 10,
169
+ padding: '0 10px 6px',
170
+ borderBottom: `1px solid ${c.line}`,
171
+ marginBottom: 4,
172
+ }}
173
+ >
174
+ <ReqoreP effect={{ weight: 'bold' }}>{g.label}</ReqoreP>
175
+ <span style={{ color: c.faint, fontSize: 12 }}>{g.rows.length}</span>
176
+ </div>
177
+ {g.rows.map((r) => {
178
+ const hasDesc = !!(r.shortDesc || r.longDesc);
179
+ const open = disc.isOpen(r.name);
180
+ const editing = form.editing === r.name;
181
+ return (
182
+ <div
183
+ key={r.name}
184
+ className='vct-row'
185
+ role='button'
186
+ tabIndex={0}
187
+ aria-label={r.label}
188
+ onClick={() => !r.readOnly && form.startEdit(r.name)}
189
+ >
190
+ <span className='vct-label' style={{ color: c.text }}>
191
+ {r.label}
192
+ {r.required ?
193
+ <ReqoreIcon icon='Asterisk' size='9px' style={{ color: c.danger }} />
194
+ : null}
195
+ </span>
196
+ <span className='vct-value' style={{ minWidth: 0, color: c.muted }}>
197
+ <ValueView value={r.value} />
198
+ {r.status === 'invalid' || r.status === 'todo' ?
199
+ <span
200
+ className='vct-reason'
201
+ style={{ color: r.status === 'invalid' ? c.danger : c.warning }}
202
+ >
203
+ {r.reason}
204
+ </span>
205
+ : null}
206
+ </span>
207
+ <span className='vct-actions'>
208
+ <StatusDot status={r.status} />
209
+ {hasDesc ?
210
+ <button
211
+ type='button'
212
+ className='vct-info'
213
+ aria-label='Toggle description'
214
+ aria-expanded={open}
215
+ onClick={(e) => {
216
+ e.stopPropagation();
217
+ disc.toggle(r.name);
218
+ }}
219
+ >
220
+ <ReqoreIcon icon={open ? 'InformationFill' : 'InformationLine'} size='14px' />
221
+ </button>
222
+ : null}
223
+ </span>
224
+ {hasDesc && open ?
225
+ <div className='vct-desc'>
226
+ {r.shortDesc ? <div>{r.shortDesc}</div> : null}
227
+ {r.longDesc ?
228
+ <div style={{ opacity: 0.8, marginTop: r.shortDesc ? 4 : 0 }}>
229
+ {r.longDesc.replace(/[#`*]/g, '')}
230
+ </div>
231
+ : null}
232
+ </div>
233
+ : null}
234
+ {editing ? <InlineEdit row={r} onDone={form.stopEdit} /> : null}
235
+ </div>
236
+ );
237
+ })}
238
+ </div>
239
+ ))}
240
+ </Wrap>
241
+ );
242
+ };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * VARIANT 2 — "Cards / Stack"
3
+ *
4
+ * Direction: breathing room over density. Each field is a subtle card with a
5
+ * generous tap target (phone-friendly). Status is shown with a single left
6
+ * accent — but ONLY for genuinely invalid (touched) fields; to-dos get a calm
7
+ * amber label dot, unset fields stay neutral. The description control sits on
8
+ * the LEFT under the label (where the text appears). Click a card to edit inline.
9
+ */
10
+ import { ReqoreIcon, ReqoreP } from '@qoretechnologies/reqore';
11
+ import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
12
+ import styled from 'styled-components';
13
+ import {
14
+ InlineEdit,
15
+ STATUS_COLOR,
16
+ ValueView,
17
+ VariantToolbar,
18
+ useDisclosure,
19
+ useVariantColors,
20
+ useVariantForm,
21
+ } from './variantParts';
22
+
23
+ const Wrap = styled.div<{ $surface: string; $line: string; $hover: string }>`
24
+ display: flex;
25
+ flex-flow: column;
26
+ gap: 26px;
27
+ font-size: 13px;
28
+
29
+ .vc-card {
30
+ position: relative;
31
+ display: grid;
32
+ grid-template-columns: minmax(200px, 0.7fr) minmax(0, 1.3fr);
33
+ gap: 18px;
34
+ padding: 14px 16px;
35
+ border-radius: 10px;
36
+ background: ${({ $surface }) => $surface};
37
+ cursor: pointer;
38
+ transition: background 0.12s ease;
39
+ overflow: hidden;
40
+ }
41
+ .vc-card:hover,
42
+ .vc-card:focus-within {
43
+ background: ${({ $hover }) => $hover};
44
+ }
45
+ .vc-accent {
46
+ position: absolute;
47
+ left: 0;
48
+ top: 0;
49
+ bottom: 0;
50
+ width: 3px;
51
+ }
52
+ .vc-name {
53
+ font-weight: 600;
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 7px;
57
+ }
58
+ .vc-descbtn {
59
+ background: none;
60
+ border: none;
61
+ padding: 0;
62
+ margin-top: 6px;
63
+ color: inherit;
64
+ cursor: pointer;
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 5px;
68
+ font-size: 12px;
69
+ }
70
+ .vc-value {
71
+ align-self: center;
72
+ min-width: 0;
73
+ }
74
+
75
+ @media (max-width: 620px) {
76
+ .vc-card {
77
+ grid-template-columns: 1fr;
78
+ gap: 8px;
79
+ padding: 14px;
80
+ }
81
+ }
82
+ `;
83
+
84
+ export const VariantCards = ({
85
+ options,
86
+ values,
87
+ }: {
88
+ options: IQorusFormSchema;
89
+ values: Record<string, IQorusFormField>;
90
+ }) => {
91
+ const c = useVariantColors();
92
+ const form = useVariantForm(options, values);
93
+ const s = form.summary;
94
+ const disc = useDisclosure();
95
+
96
+ return (
97
+ <Wrap $surface={c.surface} $line={c.line} $hover={c.hover}>
98
+ <div style={{ display: 'flex', flexFlow: 'column', gap: 10 }}>
99
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: 12 }}>
100
+ <ReqoreP size='big' effect={{ weight: 'bold' }}>
101
+ {s.pct}% complete
102
+ </ReqoreP>
103
+ <span style={{ color: c.muted, fontSize: 12 }}>
104
+ {s.set}/{s.total} set
105
+ </span>
106
+ {s.attention ?
107
+ <button
108
+ type='button'
109
+ onClick={form.toggleAttention}
110
+ style={{
111
+ background: 'none',
112
+ border: 'none',
113
+ cursor: 'pointer',
114
+ color: c.warning,
115
+ fontSize: 12,
116
+ textDecoration: 'underline',
117
+ textUnderlineOffset: 3,
118
+ }}
119
+ >
120
+ {form.filter === 'attention' ? '← show all' : `${s.attention} need attention →`}
121
+ </button>
122
+ : null}
123
+ </div>
124
+ <div style={{ height: 4, borderRadius: 2, background: c.line, overflow: 'hidden' }}>
125
+ <div style={{ width: `${s.pct}%`, height: '100%', background: c.success }} />
126
+ </div>
127
+ </div>
128
+ <VariantToolbar form={form} />
129
+
130
+ {form.visibleGroups.map((g) => (
131
+ <div key={g.name} style={{ display: 'flex', flexFlow: 'column', gap: 10 }}>
132
+ <ReqoreP effect={{ weight: 'bold', uppercase: true, spaced: 1 }} size='small'>
133
+ {g.label}
134
+ </ReqoreP>
135
+ <div style={{ display: 'flex', flexFlow: 'column', gap: 8 }}>
136
+ {g.rows.map((r) => {
137
+ const hasDesc = !!(r.shortDesc || r.longDesc);
138
+ const open = disc.isOpen(r.name);
139
+ const editing = form.editing === r.name;
140
+ return (
141
+ <div
142
+ key={r.name}
143
+ className='vc-card'
144
+ role='button'
145
+ tabIndex={0}
146
+ onClick={() => !r.readOnly && form.startEdit(r.name)}
147
+ >
148
+ {r.status === 'invalid' ?
149
+ <span className='vc-accent' style={{ background: c.danger }} />
150
+ : null}
151
+ <div>
152
+ <span className='vc-name' style={{ color: c.text }}>
153
+ {r.status === 'todo' || r.status === 'invalid' ?
154
+ <span
155
+ aria-hidden
156
+ style={{
157
+ width: 6,
158
+ height: 6,
159
+ borderRadius: '50%',
160
+ background: STATUS_COLOR(r.status, c),
161
+ }}
162
+ />
163
+ : null}
164
+ {r.label}
165
+ {r.required ?
166
+ <ReqoreIcon icon='Asterisk' size='9px' style={{ color: c.danger }} />
167
+ : null}
168
+ </span>
169
+ {hasDesc ?
170
+ <button
171
+ type='button'
172
+ className='vc-descbtn'
173
+ style={{ color: c.faint }}
174
+ aria-expanded={open}
175
+ onClick={(e) => {
176
+ e.stopPropagation();
177
+ disc.toggle(r.name);
178
+ }}
179
+ >
180
+ <ReqoreIcon icon={open ? 'ArrowUpSLine' : 'QuestionLine'} size='12px' />
181
+ {open ? 'Hide info' : 'Info'}
182
+ </button>
183
+ : null}
184
+ {open ?
185
+ <div style={{ color: c.muted, marginTop: 6, fontSize: 12, lineHeight: 1.5 }}>
186
+ {r.shortDesc ? <div>{r.shortDesc}</div> : null}
187
+ {r.longDesc ?
188
+ <div style={{ opacity: 0.85, marginTop: r.shortDesc ? 4 : 0 }}>
189
+ {r.longDesc.replace(/[#`*]/g, '')}
190
+ </div>
191
+ : null}
192
+ </div>
193
+ : null}
194
+ </div>
195
+ <div className='vc-value' style={{ color: c.muted }}>
196
+ <ValueView value={r.value} />
197
+ {r.status === 'invalid' ?
198
+ <div style={{ color: c.danger, fontSize: 12, marginTop: 4 }}>{r.reason}</div>
199
+ : r.status === 'todo' ?
200
+ <div style={{ color: c.warning, fontSize: 12, marginTop: 4 }}>{r.reason}</div>
201
+ : null}
202
+ </div>
203
+ {editing ? <InlineEdit row={r} onDone={form.stopEdit} /> : null}
204
+ </div>
205
+ );
206
+ })}
207
+ </div>
208
+ </div>
209
+ ))}
210
+ </Wrap>
211
+ );
212
+ };