@qoretechnologies/reqraft 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/design/COMPACT_ENGINE_REDESIGN.md +156 -0
  2. package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +353 -0
  3. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  4. package/dist/components/form/engine/CompactRow.js +153 -94
  5. package/dist/components/form/engine/CompactRow.js.map +1 -1
  6. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  7. package/dist/components/form/engine/CompactToolbar.js +130 -94
  8. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  9. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  10. package/dist/components/form/engine/FormEngine.js +181 -45
  11. package/dist/components/form/engine/FormEngine.js.map +1 -1
  12. package/dist/components/form/engine/compactRowStyles.d.ts +6 -3
  13. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  14. package/dist/components/form/engine/compactRowStyles.js +70 -48
  15. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  16. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  17. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  18. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  19. package/dist/components/form/engine/readFirst.d.ts +19 -0
  20. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  21. package/dist/components/form/engine/readFirst.js +22 -1
  22. package/dist/components/form/engine/readFirst.js.map +1 -1
  23. package/dist/components/form/engine/variants/VariantCalmTable.d.ts +6 -0
  24. package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -0
  25. package/dist/components/form/engine/variants/VariantCalmTable.js +94 -0
  26. package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -0
  27. package/dist/components/form/engine/variants/VariantCards.d.ts +6 -0
  28. package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -0
  29. package/dist/components/form/engine/variants/VariantCards.js +80 -0
  30. package/dist/components/form/engine/variants/VariantCards.js.map +1 -0
  31. package/dist/components/form/engine/variants/VariantFocus.d.ts +7 -0
  32. package/dist/components/form/engine/variants/VariantFocus.d.ts.map +1 -0
  33. package/dist/components/form/engine/variants/VariantFocus.js +138 -0
  34. package/dist/components/form/engine/variants/VariantFocus.js.map +1 -0
  35. package/dist/components/form/engine/variants/VariantMinimal.d.ts +6 -0
  36. package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -0
  37. package/dist/components/form/engine/variants/VariantMinimal.js +73 -0
  38. package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -0
  39. package/dist/components/form/engine/variants/focusDemo.d.ts +13 -0
  40. package/dist/components/form/engine/variants/focusDemo.d.ts.map +1 -0
  41. package/dist/components/form/engine/variants/focusDemo.js +139 -0
  42. package/dist/components/form/engine/variants/focusDemo.js.map +1 -0
  43. package/dist/components/form/engine/variants/variantModel.d.ts +70 -0
  44. package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -0
  45. package/dist/components/form/engine/variants/variantModel.js +133 -0
  46. package/dist/components/form/engine/variants/variantModel.js.map +1 -0
  47. package/dist/components/form/engine/variants/variantParts.d.ts +79 -0
  48. package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -0
  49. package/dist/components/form/engine/variants/variantParts.js +191 -0
  50. package/dist/components/form/engine/variants/variantParts.js.map +1 -0
  51. package/dist/components/form/fields/auto/AutoFormField.d.ts +3 -0
  52. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  53. package/dist/components/form/fields/auto/AutoFormField.js +2 -2
  54. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/components/form/engine/CompactRow.tsx +256 -234
  57. package/src/components/form/engine/CompactToolbar.tsx +108 -68
  58. package/src/components/form/engine/FormEngine.stories.tsx +127 -110
  59. package/src/components/form/engine/FormEngine.tsx +248 -67
  60. package/src/components/form/engine/compactRowStyles.ts +207 -134
  61. package/src/components/form/engine/compactToolbarContext.ts +1 -0
  62. package/src/components/form/engine/readFirst.ts +35 -0
  63. package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +119 -0
  64. package/src/components/form/engine/variants/VariantCalmTable.tsx +242 -0
  65. package/src/components/form/engine/variants/VariantCards.tsx +212 -0
  66. package/src/components/form/engine/variants/VariantFocus.tsx +382 -0
  67. package/src/components/form/engine/variants/VariantMinimal.tsx +170 -0
  68. package/src/components/form/engine/variants/focusDemo.ts +145 -0
  69. package/src/components/form/engine/variants/variantModel.ts +216 -0
  70. package/src/components/form/engine/variants/variantParts.tsx +313 -0
  71. package/src/components/form/fields/auto/AutoFormField.tsx +5 -0
@@ -0,0 +1,382 @@
1
+ /**
2
+ * VARIANT 3 — "Focus" (production-leaning)
3
+ *
4
+ * The original Focus structure — three expandable boxes (Needs attention / Set /
5
+ * Optional) — with the engine's real functionality layered in:
6
+ * • REQUIRED (one-of) GROUPS highlighted as a "pick one" cluster in the box,
7
+ * • schema GROUPS as thin Minimal-style labels INSIDE the Set / Optional boxes,
8
+ * • a "Descriptions" toggle (short_desc under every field),
9
+ * • COMPLEX fields showing their nested value inline with Show-more,
10
+ * • real inline editing (AutoFormField) on click.
11
+ *
12
+ * Attention box and the rest are EXCLUSIVE: a field needing attention sits in the
13
+ * box until resolved, then drops into Set (or Optional) below.
14
+ */
15
+ import { ReqoreIcon, ReqoreP } from '@qoretechnologies/reqore';
16
+ import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
17
+ import React from 'react';
18
+ import styled from 'styled-components';
19
+ import { IVariantGroup, IVariantRow, TVariantStatus } from './variantModel';
20
+ import {
21
+ ComplexPreview,
22
+ InlineEdit,
23
+ STATUS_COLOR,
24
+ StatusDot,
25
+ TVariantForm,
26
+ ValueView,
27
+ VariantToolbar,
28
+ useVariantColors,
29
+ useVariantForm,
30
+ } from './variantParts';
31
+
32
+ const Wrap = styled.div<{ $hover: string; $faint: string }>`
33
+ display: flex;
34
+ flex-flow: column;
35
+ gap: 18px;
36
+ font-size: 13px;
37
+
38
+ .vf-row {
39
+ display: grid;
40
+ grid-template-columns: minmax(190px, 320px) minmax(0, 1fr) auto;
41
+ column-gap: 16px;
42
+ row-gap: 2px;
43
+ align-items: center;
44
+ /* Single-line rows: centre the content within the min-height. */
45
+ align-content: center;
46
+ min-height: 38px;
47
+ padding: 6px 12px;
48
+ border-radius: 8px;
49
+ cursor: pointer;
50
+ }
51
+ /* Rows with a description / preview / editor below: pin the name+value to the
52
+ top instead of centring the whole block. */
53
+ .vf-row.vf-tall {
54
+ align-content: start;
55
+ }
56
+ .vf-row:hover,
57
+ .vf-row[aria-expanded='true'] {
58
+ background: ${({ $hover }) => $hover};
59
+ }
60
+ .vf-section {
61
+ border-radius: 12px;
62
+ padding: 6px;
63
+ }
64
+ .vf-sechead {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 8px;
68
+ padding: 8px 12px;
69
+ cursor: pointer;
70
+ user-select: none;
71
+ }
72
+ .vf-grouplabel {
73
+ font-size: 10px;
74
+ letter-spacing: 1px;
75
+ text-transform: uppercase;
76
+ padding: 10px 0 2px 12px;
77
+ }
78
+ .vf-cluster {
79
+ border-radius: 10px;
80
+ padding: 4px 4px 6px;
81
+ margin: 2px 0;
82
+ }
83
+ .vf-desc {
84
+ grid-column: 1 / -1;
85
+ padding: 0 0 2px;
86
+ font-size: 12px;
87
+ line-height: 1.45;
88
+ }
89
+ .vf-preview {
90
+ grid-column: 2 / -1;
91
+ padding: 2px 0 4px;
92
+ }
93
+ @media (max-width: 640px) {
94
+ .vf-row {
95
+ grid-template-columns: 1fr auto;
96
+ grid-template-areas: 'label dot' 'value value';
97
+ row-gap: 2px;
98
+ }
99
+ .vf-label {
100
+ grid-area: label;
101
+ }
102
+ .vf-value {
103
+ grid-area: value;
104
+ }
105
+ .vf-dot {
106
+ grid-area: dot;
107
+ }
108
+ .vf-preview {
109
+ grid-column: 1 / -1;
110
+ }
111
+ }
112
+ `;
113
+
114
+ const Row = ({ r, form, showDesc }: { r: IVariantRow; form: TVariantForm; showDesc: boolean }) => {
115
+ const c = useVariantColors();
116
+ const editing = form.editing === r.name;
117
+ const isHash = r.value.kind === 'hash';
118
+ // A row is "tall" (has something below the name/value line) when editing, when
119
+ // it's a complex preview, or when descriptions are shown for it.
120
+ const tall = editing || isHash || (showDesc && !!r.shortDesc);
121
+ return (
122
+ <div
123
+ className={tall ? 'vf-row vf-tall' : 'vf-row'}
124
+ role='button'
125
+ tabIndex={0}
126
+ aria-expanded={editing}
127
+ onClick={() => !r.readOnly && form.startEdit(r.name)}
128
+ >
129
+ <span
130
+ className='vf-label'
131
+ style={{ fontWeight: 600, color: c.text, display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}
132
+ >
133
+ {r.label}
134
+ {r.required ?
135
+ <ReqoreIcon icon='Asterisk' size='9px' style={{ color: c.danger }} />
136
+ : null}
137
+ </span>
138
+ <span className='vf-value' style={{ color: c.muted, minWidth: 0 }}>
139
+ <ValueView value={r.value} />
140
+ {r.reason && r.status !== 'set' ?
141
+ <span style={{ color: STATUS_COLOR(r.status, c), fontSize: 12, marginLeft: 8 }}>
142
+ {r.reason}
143
+ </span>
144
+ : null}
145
+ </span>
146
+ <span className='vf-dot' style={{ display: 'inline-flex', justifyContent: 'flex-end' }}>
147
+ <StatusDot status={r.status} />
148
+ </span>
149
+ {showDesc && r.shortDesc && !editing ?
150
+ <div className='vf-desc' style={{ color: c.faint }}>
151
+ {r.shortDesc}
152
+ </div>
153
+ : null}
154
+ {isHash && !editing ?
155
+ <div className='vf-preview'>
156
+ <ComplexPreview value={r.field?.value} onOpen={() => !r.readOnly && form.startEdit(r.name)} />
157
+ </div>
158
+ : null}
159
+ {editing ?
160
+ <>
161
+ {r.shortDesc ? <div className='vf-desc' style={{ color: c.faint }}>{r.shortDesc}</div> : null}
162
+ <InlineEdit row={r} onDone={form.stopEdit} />
163
+ </>
164
+ : null}
165
+ </div>
166
+ );
167
+ };
168
+
169
+ /** A one-of required group, highlighted: "pick one of these". */
170
+ const RequiredCluster = ({
171
+ rows,
172
+ form,
173
+ showDesc,
174
+ }: {
175
+ rows: IVariantRow[];
176
+ form: TVariantForm;
177
+ showDesc: boolean;
178
+ }) => {
179
+ const c = useVariantColors();
180
+ const tint = c.warning;
181
+ return (
182
+ <div className='vf-cluster' style={{ background: `${tint}10`, border: `1px solid ${tint}33` }}>
183
+ {/* Matches the thin group-label style (.vf-grouplabel) for compactness. */}
184
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 0 2px 12px' }}>
185
+ <ReqoreIcon icon='LinkM' size='11px' style={{ color: tint }} />
186
+ <span style={{ fontSize: 10, letterSpacing: 1, textTransform: 'uppercase', color: tint }}>
187
+ One of the below is required
188
+ </span>
189
+ </div>
190
+ {rows.map((r) => <Row key={r.name} r={r} form={form} showDesc={showDesc} />)}
191
+ </div>
192
+ );
193
+ };
194
+
195
+ const Section = ({
196
+ title,
197
+ intent,
198
+ count,
199
+ children,
200
+ defaultOpen = true,
201
+ }: {
202
+ title: string;
203
+ intent: string;
204
+ count: number;
205
+ children: React.ReactNode;
206
+ defaultOpen?: boolean;
207
+ }) => {
208
+ const [open, setOpen] = React.useState(defaultOpen);
209
+ if (!count) return null;
210
+ return (
211
+ <div className='vf-section' style={{ background: `${intent}0e`, border: `1px solid ${intent}22` }}>
212
+ <div className='vf-sechead' onClick={() => setOpen((o) => !o)}>
213
+ <ReqoreIcon icon={open ? 'ArrowDownSLine' : 'ArrowRightSLine'} size='15px' />
214
+ <ReqoreP effect={{ weight: 'bold' }}>{title}</ReqoreP>
215
+ <span
216
+ style={{
217
+ background: `${intent}22`,
218
+ color: intent,
219
+ borderRadius: 20,
220
+ padding: '1px 9px',
221
+ fontSize: 12,
222
+ fontWeight: 700,
223
+ }}
224
+ >
225
+ {count}
226
+ </span>
227
+ </div>
228
+ {open ? <div>{children}</div> : null}
229
+ </div>
230
+ );
231
+ };
232
+
233
+ /** Rows of a given status, grouped by schema group with thin Minimal-style
234
+ * labels. Used inside the Set and Optional boxes. */
235
+ const GroupedRows = ({
236
+ groups,
237
+ status,
238
+ skip,
239
+ form,
240
+ showDesc,
241
+ }: {
242
+ groups: IVariantGroup[];
243
+ status: TVariantStatus;
244
+ skip: Set<string>;
245
+ form: TVariantForm;
246
+ showDesc: boolean;
247
+ }) => {
248
+ const c = useVariantColors();
249
+ const sections = groups
250
+ .map((g) => ({ g, rows: g.rows.filter((r) => r.status === status && !skip.has(r.name)) }))
251
+ .filter((s) => s.rows.length);
252
+ return (
253
+ <>
254
+ {sections.map(({ g, rows }) => (
255
+ <div key={g.name}>
256
+ {/* only label when there's more than one group's worth, else it's noise */}
257
+ {sections.length > 1 ?
258
+ <div className='vf-grouplabel' style={{ color: c.faint }}>
259
+ {g.label}
260
+ </div>
261
+ : null}
262
+ {rows.map((r) => <Row key={r.name} r={r} form={form} showDesc={showDesc} />)}
263
+ </div>
264
+ ))}
265
+ </>
266
+ );
267
+ };
268
+
269
+ export const VariantFocus = ({
270
+ options,
271
+ values,
272
+ config,
273
+ }: {
274
+ options: IQorusFormSchema;
275
+ values: Record<string, IQorusFormField>;
276
+ config?: any;
277
+ }) => {
278
+ const c = useVariantColors();
279
+ const form = useVariantForm(options, values, config);
280
+ const s = form.summary;
281
+ const showDesc = form.showDescriptions;
282
+
283
+ const allRows = form.visibleGroups.flatMap((g) => g.rows);
284
+ const unmetGroups = Object.values(form.requiredGroups).filter((g) => !g.satisfied);
285
+ const unmetMembers = new Set<string>();
286
+ unmetGroups.forEach((g) => g.members.forEach((m) => unmetMembers.add(m)));
287
+
288
+ // Attention = invalid + individually-required-todo (NOT one-of members, which
289
+ // show as their cluster) + the unmet one-of clusters.
290
+ const attentionRows = allRows.filter(
291
+ (r) => (r.status === 'invalid' || r.status === 'todo') && !unmetMembers.has(r.name)
292
+ );
293
+ const attentionCount = attentionRows.length + unmetGroups.length;
294
+ const skip = new Set<string>(attentionRows.map((r) => r.name));
295
+ unmetMembers.forEach((n) => skip.add(n));
296
+
297
+ const setCount = allRows.filter((r) => r.status === 'set' && !skip.has(r.name)).length;
298
+ const optionalCount = allRows.filter((r) => r.status === 'unset' && !skip.has(r.name)).length;
299
+
300
+ // Group the attention items by schema group (placing each one-of cluster under
301
+ // its members' group) so the attention box reads like the Set / Optional boxes.
302
+ const clusterGroupName = (members: string[]) =>
303
+ form.visibleGroups.find((g) => g.rows.some((r) => r.name === members[0]))?.name;
304
+ const attnGroups = form.visibleGroups
305
+ .map((g) => ({
306
+ g,
307
+ rows: g.rows.filter(
308
+ (r) => (r.status === 'invalid' || r.status === 'todo') && !unmetMembers.has(r.name)
309
+ ),
310
+ clusters: unmetGroups.filter((rg) => clusterGroupName(rg.members) === g.name),
311
+ }))
312
+ .filter((x) => x.rows.length || x.clusters.length);
313
+
314
+ return (
315
+ <Wrap $hover={c.hover} $faint={c.faint}>
316
+ {/* Cards-style header */}
317
+ <div style={{ display: 'flex', flexFlow: 'column', gap: 10 }}>
318
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' }}>
319
+ <ReqoreP size='big' effect={{ weight: 'bold' }}>
320
+ {s.pct}% complete
321
+ </ReqoreP>
322
+ <span style={{ color: c.muted, fontSize: 12 }}>
323
+ {s.set}/{s.total} set
324
+ </span>
325
+ {s.attention ?
326
+ <button
327
+ type='button'
328
+ onClick={form.toggleAttention}
329
+ style={{
330
+ background: 'none',
331
+ border: 'none',
332
+ cursor: 'pointer',
333
+ color: c.warning,
334
+ fontSize: 12,
335
+ textDecoration: 'underline',
336
+ textUnderlineOffset: 3,
337
+ }}
338
+ >
339
+ {form.filter === 'attention' ? '← show all' : `${s.attention} need attention →`}
340
+ </button>
341
+ : null}
342
+ </div>
343
+ <div style={{ height: 4, borderRadius: 2, background: c.line, overflow: 'hidden', display: 'flex' }}>
344
+ <div style={{ width: `${(s.set / s.total) * 100}%`, background: c.success }} />
345
+ <div style={{ width: `${(s.attention / s.total) * 100}%`, background: c.warning }} />
346
+ </div>
347
+ </div>
348
+ <VariantToolbar form={form} />
349
+
350
+ {/* The signature: Needs attention / Set / Optional expandable boxes —
351
+ attention items grouped by schema group, like the other boxes. */}
352
+ <Section title='Needs attention' intent={c.warning} count={attentionCount}>
353
+ {attnGroups.map(({ g, rows, clusters }) => (
354
+ <div key={g.name}>
355
+ {attnGroups.length > 1 ?
356
+ <div className='vf-grouplabel' style={{ color: c.faint }}>
357
+ {g.label}
358
+ </div>
359
+ : null}
360
+ {clusters.map((cl) => (
361
+ <RequiredCluster
362
+ key={cl.key}
363
+ rows={allRows.filter((r) => cl.members.includes(r.name))}
364
+ form={form}
365
+ showDesc={showDesc}
366
+ />
367
+ ))}
368
+ {rows.map((r) => <Row key={r.name} r={r} form={form} showDesc={showDesc} />)}
369
+ </div>
370
+ ))}
371
+ </Section>
372
+
373
+ <Section title='Set' intent={c.success} count={setCount}>
374
+ <GroupedRows groups={form.visibleGroups} status='set' skip={skip} form={form} showDesc={showDesc} />
375
+ </Section>
376
+
377
+ <Section title='Optional — not set' intent={c.muted} count={optionalCount} defaultOpen={false}>
378
+ <GroupedRows groups={form.visibleGroups} status='unset' skip={skip} form={form} showDesc={showDesc} />
379
+ </Section>
380
+ </Wrap>
381
+ );
382
+ };
@@ -0,0 +1,170 @@
1
+ /**
2
+ * VARIANT 4 — "Minimal" (Notion / Linear inspired)
3
+ *
4
+ * Direction: the lightest possible treatment. No surfaces, no boxes, no group
5
+ * panels — just a quiet `label · value` list with a hairline status tick on the
6
+ * left edge. TAP a row to reveal its description and edit it inline — NO hover
7
+ * dependency, so it behaves identically on phone. Densest of the four.
8
+ */
9
+ import { ReqoreIcon, ReqoreP } from '@qoretechnologies/reqore';
10
+ import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
11
+ import React from 'react';
12
+ import styled from 'styled-components';
13
+ import {
14
+ InlineEdit,
15
+ STATUS_COLOR,
16
+ ValueView,
17
+ VariantToolbar,
18
+ useVariantColors,
19
+ useVariantForm,
20
+ } from './variantParts';
21
+
22
+ const Wrap = styled.div<{ $hover: string; $line: string; $faint: string }>`
23
+ display: flex;
24
+ flex-flow: column;
25
+ gap: 4px;
26
+ font-size: 13px;
27
+
28
+ .vm-group {
29
+ font-size: 11px;
30
+ letter-spacing: 1px;
31
+ text-transform: uppercase;
32
+ padding: 16px 0 6px 14px;
33
+ }
34
+ .vm-row {
35
+ display: grid;
36
+ grid-template-columns: minmax(180px, 320px) minmax(0, 1fr);
37
+ gap: 14px;
38
+ align-items: baseline;
39
+ padding: 7px 12px 7px 14px;
40
+ border-left: 2px solid transparent;
41
+ cursor: pointer;
42
+ border-radius: 0 6px 6px 0;
43
+ }
44
+ .vm-row[aria-expanded='true'] {
45
+ background: ${({ $hover }) => $hover};
46
+ }
47
+ @media (hover: hover) {
48
+ .vm-row:hover {
49
+ background: ${({ $hover }) => $hover};
50
+ }
51
+ }
52
+ .vm-label {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 6px;
56
+ min-width: 0;
57
+ }
58
+ .vm-detail {
59
+ grid-column: 1 / -1;
60
+ color: ${({ $faint }) => $faint};
61
+ padding: 4px 0 2px;
62
+ font-size: 12px;
63
+ line-height: 1.5;
64
+ }
65
+ @media (max-width: 620px) {
66
+ .vm-row {
67
+ grid-template-columns: 1fr;
68
+ gap: 2px;
69
+ }
70
+ }
71
+ `;
72
+
73
+ export const VariantMinimal = ({
74
+ options,
75
+ values,
76
+ }: {
77
+ options: IQorusFormSchema;
78
+ values: Record<string, IQorusFormField>;
79
+ }) => {
80
+ const c = useVariantColors();
81
+ const form = useVariantForm(options, values);
82
+ const s = form.summary;
83
+
84
+ return (
85
+ <Wrap $hover={c.hover} $line={c.line} $faint={c.faint}>
86
+ <div
87
+ style={{
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ gap: 10,
91
+ padding: '0 12px 10px',
92
+ borderBottom: `1px solid ${c.line}`,
93
+ }}
94
+ >
95
+ <span style={{ width: 8, height: 8, borderRadius: '50%', background: s.attention ? c.warning : c.success }} />
96
+ <ReqoreP effect={{ weight: 'bold' }}>{s.pct}%</ReqoreP>
97
+ <span style={{ color: c.muted, fontSize: 12 }}>{s.set}/{s.total} set</span>
98
+ {s.attention ?
99
+ <button
100
+ type='button'
101
+ onClick={form.toggleAttention}
102
+ style={{
103
+ background: 'none',
104
+ border: 'none',
105
+ cursor: 'pointer',
106
+ color: c.warning,
107
+ fontSize: 12,
108
+ textDecoration: 'underline',
109
+ textUnderlineOffset: 3,
110
+ }}
111
+ >
112
+ {form.filter === 'attention' ? '← all' : `${s.attention} to resolve →`}
113
+ </button>
114
+ : null}
115
+ </div>
116
+ <VariantToolbar form={form} />
117
+
118
+ {form.visibleGroups.map((g) => (
119
+ <React.Fragment key={g.name}>
120
+ <div className='vm-group' style={{ color: c.faint }}>
121
+ {g.label}
122
+ </div>
123
+ {g.rows.map((r) => {
124
+ const editing = form.editing === r.name;
125
+ const hasDetail = !!(r.shortDesc || r.longDesc);
126
+ return (
127
+ <div
128
+ key={r.name}
129
+ className='vm-row'
130
+ role='button'
131
+ tabIndex={0}
132
+ aria-expanded={editing}
133
+ style={{ borderLeftColor: r.status === 'set' || r.status === 'unset' ? 'transparent' : STATUS_COLOR(r.status, c) }}
134
+ onClick={() => !r.readOnly && form.startEdit(r.name)}
135
+ >
136
+ <span className='vm-label' style={{ color: c.text, fontWeight: 500 }}>
137
+ {r.label}
138
+ {r.required ?
139
+ <ReqoreIcon icon='Asterisk' size='8px' style={{ color: c.danger }} />
140
+ : null}
141
+ </span>
142
+ <span style={{ color: c.muted, minWidth: 0, display: 'inline-flex', alignItems: 'baseline', gap: 8 }}>
143
+ <ValueView value={r.value} />
144
+ {r.status === 'invalid' || r.status === 'todo' ?
145
+ <span style={{ color: STATUS_COLOR(r.status, c), fontSize: 12 }}>· {r.reason}</span>
146
+ : null}
147
+ </span>
148
+ {editing ?
149
+ <>
150
+ {hasDetail ?
151
+ <div className='vm-detail'>
152
+ {r.shortDesc ? <div>{r.shortDesc}</div> : null}
153
+ {r.longDesc ?
154
+ <div style={{ marginTop: r.shortDesc ? 4 : 0 }}>
155
+ {r.longDesc.replace(/[#`*]/g, '')}
156
+ </div>
157
+ : null}
158
+ </div>
159
+ : null}
160
+ <InlineEdit row={r} onDone={form.stopEdit} />
161
+ </>
162
+ : null}
163
+ </div>
164
+ );
165
+ })}
166
+ </React.Fragment>
167
+ ))}
168
+ </Wrap>
169
+ );
170
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * A richer, realistic demo schema for the Focus-Pro variant — exercises the
3
+ * features the basic fixture lacks: NAMED groups, a one-of REQUIRED GROUP,
4
+ * a complex nested hash (Show-more preview), allowed-values, files/lists, plus
5
+ * a mix of set / invalid / required-unset / optional states. Modelled loosely
6
+ * on a Qorus datasource connection config.
7
+ */
8
+ import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
9
+
10
+ export const getFocusDemoOptions = (): IQorusFormSchema =>
11
+ ({
12
+ // --- Connection -------------------------------------------------------
13
+ url: {
14
+ type: 'string',
15
+ ui_type: 'string',
16
+ display_name: 'Connection URL',
17
+ short_desc: 'The full datasource URL (driver://user@host:port/db).',
18
+ desc: 'Accepts any Qore datasource URL. Templates like `$config:db-url` are supported.',
19
+ group: 'connection',
20
+ required: true,
21
+ supports_templates: true,
22
+ },
23
+ port: {
24
+ type: 'int',
25
+ ui_type: 'number',
26
+ display_name: 'Port',
27
+ short_desc: 'TCP port of the database server.',
28
+ group: 'connection',
29
+ default_value: { type: 'int', value: 5432 },
30
+ },
31
+ database: {
32
+ type: 'string',
33
+ ui_type: 'string',
34
+ display_name: 'Database name',
35
+ short_desc: 'Name of the schema/database to connect to.',
36
+ group: 'connection',
37
+ required: true,
38
+ },
39
+ // --- Authentication (one-of required group) ---------------------------
40
+ token: {
41
+ type: 'string',
42
+ ui_type: 'string',
43
+ display_name: 'API token',
44
+ short_desc: 'Bearer token — use this OR a username/password.',
45
+ group: 'authentication',
46
+ required_groups: ['auth'],
47
+ supports_templates: true,
48
+ },
49
+ username: {
50
+ type: 'string',
51
+ ui_type: 'string',
52
+ display_name: 'Username',
53
+ short_desc: 'Database user — paired with a password.',
54
+ group: 'authentication',
55
+ required_groups: ['auth'],
56
+ },
57
+ password: {
58
+ type: 'string',
59
+ ui_type: 'string',
60
+ display_name: 'Password',
61
+ short_desc: 'Database password.',
62
+ group: 'authentication',
63
+ required_groups: ['auth'],
64
+ supports_templates: true,
65
+ },
66
+ // --- Advanced ---------------------------------------------------------
67
+ options: {
68
+ type: 'hash',
69
+ ui_type: 'hash',
70
+ display_name: 'Driver options',
71
+ short_desc: 'Extra key/value options passed to the driver.',
72
+ group: 'advanced',
73
+ },
74
+ poolSize: {
75
+ type: 'int',
76
+ ui_type: 'number',
77
+ display_name: 'Pool size',
78
+ short_desc: 'Max simultaneous connections.',
79
+ group: 'advanced',
80
+ default_value: { type: 'int', value: 10 },
81
+ },
82
+ timeout: {
83
+ type: 'int',
84
+ ui_type: 'number',
85
+ display_name: 'Timeout (s)',
86
+ short_desc: 'Connection timeout in seconds.',
87
+ group: 'advanced',
88
+ },
89
+ logLevel: {
90
+ type: 'string',
91
+ ui_type: 'string',
92
+ display_name: 'Log level',
93
+ short_desc: 'Verbosity of the connection log.',
94
+ group: 'advanced',
95
+ allowed_values: [
96
+ { value: 'error', display_name: 'Error' },
97
+ { value: 'info', display_name: 'Info' },
98
+ { value: 'debug', display_name: 'Debug' },
99
+ ],
100
+ },
101
+ // --- Optional (non-preselected — render in the Optional group) ---------
102
+ sslCert: {
103
+ type: 'file',
104
+ ui_type: 'file',
105
+ display_name: 'SSL certificate',
106
+ short_desc: 'Client certificate for mutual TLS.',
107
+ },
108
+ tags: {
109
+ type: 'list',
110
+ ui_type: 'list',
111
+ display_name: 'Tags',
112
+ short_desc: 'Free-form labels for this connection.',
113
+ },
114
+ }) as unknown as IQorusFormSchema;
115
+
116
+ export const focusDemoValue: Record<string, IQorusFormField> = {
117
+ url: { type: 'string', value: 'pgsql://hq.qoretechnologies.com:5432/omq' },
118
+ // database: intentionally unset → required to-do
119
+ // auth group: intentionally none set → one-of unsatisfied → attention
120
+ options: {
121
+ type: 'hash',
122
+ value: {
123
+ sslmode: { type: 'string', value: 'require' },
124
+ application_name: { type: 'string', value: 'qorus-ide' },
125
+ connect_timeout: { type: 'int', value: 8 },
126
+ },
127
+ },
128
+ logLevel: { type: 'string', value: 'info' },
129
+ // timeout: invalid (entered 0)
130
+ timeout: { type: 'int', value: 0 as any },
131
+ };
132
+
133
+ // Demo-only invalid reasons (in the engine these come from the validity pass).
134
+ export const focusDemoInvalid: Record<string, string> = {
135
+ timeout: 'Must be greater than 0',
136
+ };
137
+
138
+ // Display labels for the named groups.
139
+ export const focusDemoGroupLabels: Record<string, string> = {
140
+ connection: 'Connection',
141
+ authentication: 'Authentication',
142
+ advanced: 'Advanced',
143
+ optional: 'Optional',
144
+ general: 'General',
145
+ };