@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,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
+ };