@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.
- package/.claude/CLAUDE.md +5 -0
- package/design/COMPACT_ENGINE_REDESIGN.md +156 -0
- package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +353 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +158 -101
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +122 -105
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +9 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +272 -82
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +6 -3
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +76 -49
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +19 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +22 -1
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js +94 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.js +80 -0
- package/dist/components/form/engine/variants/VariantCards.js.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts +7 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.js +138 -0
- package/dist/components/form/engine/variants/VariantFocus.js.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.js +73 -0
- package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts +13 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.js +139 -0
- package/dist/components/form/engine/variants/focusDemo.js.map +1 -0
- package/dist/components/form/engine/variants/variantModel.d.ts +70 -0
- package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantModel.js +133 -0
- package/dist/components/form/engine/variants/variantModel.js.map +1 -0
- package/dist/components/form/engine/variants/variantParts.d.ts +79 -0
- package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantParts.js +191 -0
- package/dist/components/form/engine/variants/variantParts.js.map +1 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts +3 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +5 -2
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +273 -258
- package/src/components/form/engine/CompactToolbar.tsx +112 -85
- package/src/components/form/engine/FormEngine.stories.tsx +239 -115
- package/src/components/form/engine/FormEngine.tsx +332 -83
- package/src/components/form/engine/compactRowStyles.ts +221 -144
- package/src/components/form/engine/compactToolbarContext.ts +1 -0
- package/src/components/form/engine/readFirst.ts +35 -0
- package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +119 -0
- package/src/components/form/engine/variants/VariantCalmTable.tsx +242 -0
- package/src/components/form/engine/variants/VariantCards.tsx +212 -0
- package/src/components/form/engine/variants/VariantFocus.tsx +382 -0
- package/src/components/form/engine/variants/VariantMinimal.tsx +170 -0
- package/src/components/form/engine/variants/focusDemo.ts +145 -0
- package/src/components/form/engine/variants/variantModel.ts +216 -0
- package/src/components/form/engine/variants/variantParts.tsx +313 -0
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +9 -2
- 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
|
+
};
|