@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.
- 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 +153 -94
- 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 +130 -94
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +181 -45
- 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 +70 -48
- 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 +2 -2
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +256 -234
- package/src/components/form/engine/CompactToolbar.tsx +108 -68
- package/src/components/form/engine/FormEngine.stories.tsx +127 -110
- package/src/components/form/engine/FormEngine.tsx +248 -67
- package/src/components/form/engine/compactRowStyles.ts +207 -134
- 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.tsx +5 -0
|
@@ -18,11 +18,10 @@ export const COMPACT_ROW_GAP = 14; // grid column gap
|
|
|
18
18
|
export const COMPACT_VALUE_LEFT = COMPACT_ROW_PAD_X + COMPACT_LABEL_COL + COMPACT_ROW_GAP;
|
|
19
19
|
// The surface starts a touch left of the value text, for inner left padding.
|
|
20
20
|
export const COMPACT_PANEL_LEFT = COMPACT_VALUE_LEFT - 10;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
export const GROUP_INDENT = 'clamp(8px, 3%, 32px)';
|
|
21
|
+
// Left offset for the group sub-labels / cluster header, kept equal to the row's
|
|
22
|
+
// own horizontal padding so the labels line up flush with the field labels below
|
|
23
|
+
// them (no extra gutter — the spine/rail that used a fluid indent are gone).
|
|
24
|
+
export const GROUP_INDENT = `${COMPACT_ROW_PAD_X}px`;
|
|
26
25
|
|
|
27
26
|
// Measured label column (GLOBAL). The label column sizes to the WIDEST field
|
|
28
27
|
// label across the whole form, clamped to [MIN, MAX]. FormEngine measures the
|
|
@@ -35,7 +34,6 @@ export const LABEL_COL_MIN = 120;
|
|
|
35
34
|
export const LABEL_COL_MAX = COMPACT_LABEL_COL; // 220
|
|
36
35
|
export const LABEL_COL_VAR = '--readfirst-label-col';
|
|
37
36
|
export const LABEL_COL = `var(${LABEL_COL_VAR}, ${COMPACT_LABEL_COL}px)`;
|
|
38
|
-
export const VALUE_LEFT_CSS = `calc(${LABEL_COL} + ${COMPACT_ROW_PAD_X + COMPACT_ROW_GAP}px)`;
|
|
39
37
|
export const PANEL_LEFT_CSS = `calc(${LABEL_COL} + ${COMPACT_ROW_PAD_X + COMPACT_ROW_GAP - 10}px)`;
|
|
40
38
|
|
|
41
39
|
// Glass sticky header: override `.reqore-panel-title` (the surface ReqorePanel
|
|
@@ -89,6 +87,62 @@ export const StyledGroupHeaderLine = styled.span<{ $color: string }>`
|
|
|
89
87
|
background: linear-gradient(to right, ${({ $color }) => $color}, transparent);
|
|
90
88
|
`;
|
|
91
89
|
|
|
90
|
+
// A status box (Needs attention / Set / Optional). Matches the "Focus" prototype:
|
|
91
|
+
// a barely-there accent border + a ~5%-opacity tint, NOT the loud intent border a
|
|
92
|
+
// stock ReqorePanel draws. `$accent` is the box's theme colour (warning/success/
|
|
93
|
+
// muted).
|
|
94
|
+
export const StyledStatusBox = styled(ReqorePanel)<{ $accent: string; $bg?: string }>`
|
|
95
|
+
&&& {
|
|
96
|
+
border: 1px solid ${({ $accent }) => `${$accent}33`};
|
|
97
|
+
/* $bg lets the muted "Optional" box opt into a darker, recessed surface
|
|
98
|
+
instead of the faint accent tint the coloured boxes use. */
|
|
99
|
+
background: ${({ $accent, $bg }) => $bg || `${$accent}1f`};
|
|
100
|
+
border-radius: 10px;
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
// "One of the below is required" cluster box — wraps the members of an unmet
|
|
105
|
+
// one-of required group (in the Needs-attention box) so the constraint reads as
|
|
106
|
+
// one unit. The connection rail + status nodes still render inside (they convey
|
|
107
|
+
// which member satisfies the group); this box adds the explicit heading.
|
|
108
|
+
export const StyledRequiredClusterBox = styled.div<{ $border: string; $tint: string }>`
|
|
109
|
+
border: 1px solid ${({ $border }) => $border};
|
|
110
|
+
border-radius: 8px;
|
|
111
|
+
background: ${({ $tint }) => $tint};
|
|
112
|
+
padding: 2px 6px 6px;
|
|
113
|
+
margin: 4px 0;
|
|
114
|
+
/* The members render directly here (not via the gapped group body), so give
|
|
115
|
+
them the same modest gap the rest of the rows have. Pin the divider-centring
|
|
116
|
+
var to that gap (it doesn't widen in narrow like the group body's does). */
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-flow: column;
|
|
119
|
+
--readfirst-row-gap: 4px;
|
|
120
|
+
gap: 4px;
|
|
121
|
+
`;
|
|
122
|
+
export const StyledRequiredClusterHeader = styled.div<{ $color: string }>`
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 6px;
|
|
126
|
+
padding: 8px 0 4px ${GROUP_INDENT};
|
|
127
|
+
font-size: 10px;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
letter-spacing: 1px;
|
|
130
|
+
text-transform: uppercase;
|
|
131
|
+
color: ${({ $color }) => $color};
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
// Thin schema-group sub-label inside a status box (CONNECTION / AUTHENTICATION /
|
|
135
|
+
// …). Quiet by design — the box header is the loud heading; this just keeps each
|
|
136
|
+
// field's group context as you scan. Indented to the rows' content line.
|
|
137
|
+
export const StyledStatusBoxGroupLabel = styled.div`
|
|
138
|
+
font-size: 10px;
|
|
139
|
+
font-weight: 500;
|
|
140
|
+
letter-spacing: 1px;
|
|
141
|
+
text-transform: uppercase;
|
|
142
|
+
opacity: 0.5;
|
|
143
|
+
padding: 8px 0 2px ${GROUP_INDENT};
|
|
144
|
+
`;
|
|
145
|
+
|
|
92
146
|
// Required-group "connection" rail: contiguous members are linked by a continuous
|
|
93
147
|
// vertical rail (StyledGroupBody draws it; the line bridges the row gaps and is
|
|
94
148
|
// trimmed to the first/last node). Each member carries a status node — absolutely
|
|
@@ -180,10 +234,11 @@ export const StyledCardHeading = styled.div`
|
|
|
180
234
|
min-width: 0;
|
|
181
235
|
`;
|
|
182
236
|
|
|
237
|
+
/* The card (expanded) label matches the read-row label exactly — same size /
|
|
238
|
+
weight / case — so a field's name doesn't switch styles when you open it. */
|
|
183
239
|
export const StyledCardLabel = styled.div<{ $color: string }>`
|
|
184
|
-
font-size:
|
|
185
|
-
|
|
186
|
-
letter-spacing: 0.07em;
|
|
240
|
+
font-size: 13px;
|
|
241
|
+
font-weight: 600;
|
|
187
242
|
color: ${({ $color }) => $color};
|
|
188
243
|
display: flex;
|
|
189
244
|
align-items: center;
|
|
@@ -194,18 +249,71 @@ export const StyledCardLabel = styled.div<{ $color: string }>`
|
|
|
194
249
|
so the ellipsis engages instead of overflowing. */
|
|
195
250
|
export const StyledRowValue = styled.div<{ $color: string; $empty?: boolean }>`
|
|
196
251
|
min-width: 0;
|
|
197
|
-
color: ${({ $color }) => $color};
|
|
198
|
-
font-style: ${({ $empty }) => ($empty ? 'italic' : 'normal')};
|
|
199
252
|
font-size: 13px;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
253
|
+
/* Value + inline reason(s) share the line, wrapping the reason below only when
|
|
254
|
+
it doesn't fit (the Focus prototype's layout). */
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-wrap: wrap;
|
|
257
|
+
align-items: baseline;
|
|
258
|
+
column-gap: 10px;
|
|
259
|
+
row-gap: 2px;
|
|
260
|
+
/* The muted/translucent colour applies to the VALUE TEXT only — not the whole
|
|
261
|
+
cell — so the message panels and the structured preview below render at full
|
|
262
|
+
opacity instead of inheriting the dimmed value colour. */
|
|
263
|
+
.options-readfirst-valuetext {
|
|
264
|
+
min-width: 0;
|
|
265
|
+
max-width: 100%;
|
|
266
|
+
color: ${({ $color }) => $color};
|
|
267
|
+
font-style: ${({ $empty }) => ($empty ? 'italic' : 'normal')};
|
|
268
|
+
overflow: hidden;
|
|
269
|
+
text-overflow: ellipsis;
|
|
270
|
+
white-space: nowrap;
|
|
271
|
+
}
|
|
272
|
+
.options-readfirst-reason {
|
|
273
|
+
font-style: italic;
|
|
274
|
+
font-size: 12px;
|
|
275
|
+
line-height: 1.3;
|
|
276
|
+
}
|
|
277
|
+
/* Schema message panels: full width of the value column, on their own line
|
|
278
|
+
directly beneath the value. */
|
|
279
|
+
.options-readfirst-info-panel {
|
|
280
|
+
flex-basis: 100%;
|
|
281
|
+
width: 100%;
|
|
282
|
+
display: flex;
|
|
283
|
+
flex-flow: column;
|
|
284
|
+
gap: 4px;
|
|
285
|
+
margin-top: 4px;
|
|
286
|
+
}
|
|
203
287
|
`;
|
|
204
288
|
|
|
205
289
|
export const StyledRowActions = styled.div`
|
|
206
290
|
display: flex;
|
|
207
291
|
align-items: center;
|
|
292
|
+
/* The row top-aligns its cells, so the actions sit at the row's content top.
|
|
293
|
+
Hover action buttons (revert/delete) are ~24px and would otherwise pull the
|
|
294
|
+
centred dot down with them — pin the dot to the LABEL's first line instead so
|
|
295
|
+
it stays at a single, consistent height on every row no matter what hangs
|
|
296
|
+
below the value. */
|
|
297
|
+
align-self: start;
|
|
208
298
|
gap: 6px;
|
|
299
|
+
.options-readfirst-statusdot-slot {
|
|
300
|
+
align-self: flex-start;
|
|
301
|
+
align-items: center;
|
|
302
|
+
height: 12px;
|
|
303
|
+
}
|
|
304
|
+
`;
|
|
305
|
+
|
|
306
|
+
// A single status mark pinned at the row's trailing edge: one dot, colour =
|
|
307
|
+
// severity (danger/warning/success). This is the "Focus" read-first signal that
|
|
308
|
+
// replaces the recessed value surface's intent stripe — attention dots carry a
|
|
309
|
+
// faint ring, a plain "set" dot does not. `unset` rows render no dot.
|
|
310
|
+
export const StyledStatusDot = styled.span<{ $color: string; $ring?: boolean }>`
|
|
311
|
+
width: 7px;
|
|
312
|
+
height: 7px;
|
|
313
|
+
border-radius: 50%;
|
|
314
|
+
flex: 0 0 auto;
|
|
315
|
+
background: ${({ $color }) => $color};
|
|
316
|
+
box-shadow: ${({ $color, $ring }) => ($ring ? `0 0 0 3px ${$color}22` : 'none')};
|
|
209
317
|
`;
|
|
210
318
|
|
|
211
319
|
export const StyledActionSlot = styled.span<{ $width: number }>`
|
|
@@ -220,18 +328,13 @@ export const StyledColumn = styled.div`
|
|
|
220
328
|
flex-flow: column;
|
|
221
329
|
`;
|
|
222
330
|
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
export const StyledInfoPanel = styled.div`
|
|
227
|
-
display: flex;
|
|
228
|
-
flex-flow: column;
|
|
229
|
-
gap: 4px;
|
|
230
|
-
padding: 0 10px 8px 24px;
|
|
231
|
-
`;
|
|
232
|
-
|
|
331
|
+
// The structured hash/list preview, rendered inside the value cell directly
|
|
332
|
+
// under the value summary. Full-width of the value column (it's a flex child of
|
|
333
|
+
// the wrapping value cell) so it lines up with the value, not the label gutter.
|
|
233
334
|
export const StyledRowInset = styled.div`
|
|
234
|
-
|
|
335
|
+
flex-basis: 100%;
|
|
336
|
+
width: 100%;
|
|
337
|
+
margin-top: 4px;
|
|
235
338
|
`;
|
|
236
339
|
|
|
237
340
|
// A small inline colour swatch shown before an rgbcolor value's hex string.
|
|
@@ -255,32 +358,21 @@ export const StyledGroupBody = styled.div<{
|
|
|
255
358
|
display: flex;
|
|
256
359
|
flex-flow: column;
|
|
257
360
|
position: relative;
|
|
258
|
-
gap
|
|
361
|
+
/* Exposed as a var so the inter-field divider can centre itself in the gap
|
|
362
|
+
(the gap differs wide vs narrow). */
|
|
363
|
+
--readfirst-row-gap: 4px;
|
|
364
|
+
gap: var(--readfirst-row-gap);
|
|
259
365
|
|
|
260
366
|
/* Indent each field block under the group header by a FLUID step. The %
|
|
261
367
|
resolves against this container's width (not the screen), so it tracks the
|
|
262
|
-
|
|
263
|
-
|
|
368
|
+
(Field blocks no longer get a left gutter — the spine/rail that needed it are
|
|
369
|
+
gone, so rows sit flush against the box, like the Focus prototype.) */
|
|
264
370
|
> * {
|
|
265
|
-
margin-left:
|
|
371
|
+
margin-left: 0;
|
|
266
372
|
}
|
|
267
373
|
|
|
268
|
-
/*
|
|
269
|
-
|
|
270
|
-
rail and lands on the exact same x — GROUP_INDENT minus 9px matches the rail's
|
|
271
|
-
left: -9px on each (indented) block. The rail is a descendant ::after, so it
|
|
272
|
-
paints over this spine and wins where a required cluster overlaps it. */
|
|
273
|
-
&::before {
|
|
274
|
-
content: '';
|
|
275
|
-
position: absolute;
|
|
276
|
-
left: calc(${GROUP_INDENT} - 9px);
|
|
277
|
-
top: 0;
|
|
278
|
-
bottom: 8px;
|
|
279
|
-
width: 2px;
|
|
280
|
-
background: linear-gradient(to bottom, ${({ $lineColor }) => $lineColor}, transparent);
|
|
281
|
-
pointer-events: none;
|
|
282
|
-
z-index: 0;
|
|
283
|
-
}
|
|
374
|
+
/* No group spine: the "Focus" look keeps the rows flat against the box. (The
|
|
375
|
+
required-group rail is a separate descendant ::after and still renders.) */
|
|
284
376
|
|
|
285
377
|
.readfirst-row {
|
|
286
378
|
display: grid;
|
|
@@ -291,19 +383,40 @@ export const StyledGroupBody = styled.div<{
|
|
|
291
383
|
grid wider than its container and produce a horizontal scrollbar. The 0
|
|
292
384
|
minimum lets it shrink and the value cell's ellipsis take over instead. */
|
|
293
385
|
grid-template-columns: ${LABEL_COL} minmax(0, 1fr) auto;
|
|
294
|
-
align
|
|
386
|
+
/* TOP-align cells: the label, value and status dot all start on the first
|
|
387
|
+
line, so the dot sits at a consistent place no matter how tall the value
|
|
388
|
+
(chips, wrapped text, message panels) makes the row. Rows size to content
|
|
389
|
+
(no min-height) so the inter-field gap stays uniform. */
|
|
390
|
+
align-items: start;
|
|
295
391
|
gap: 14px;
|
|
296
|
-
min-height:
|
|
297
|
-
|
|
298
|
-
~32px tall and occupy layout even at opacity 0 — with 8px padding they
|
|
299
|
-
inflated removable rows to ~48px while plain rows sat at the 38px
|
|
300
|
-
min-height. 32 + 6 = 38 keeps every one-line row the same height; the
|
|
301
|
-
min-height keeps the click target for rows with shorter content. */
|
|
302
|
-
padding: 3px 10px;
|
|
392
|
+
min-height: 26px;
|
|
393
|
+
padding: 4px 10px;
|
|
303
394
|
border-radius: 6px;
|
|
304
395
|
cursor: pointer;
|
|
305
396
|
transition: background 0.12s ease;
|
|
306
397
|
}
|
|
398
|
+
/* A dim hairline in the gap below each field so its start/end reads clearly.
|
|
399
|
+
Absolutely positioned (not a border) so it stays straight + full-width and
|
|
400
|
+
the row's rounded hover highlight is unaffected; sits in the inter-row gap. */
|
|
401
|
+
.readfirst-row::after {
|
|
402
|
+
content: '';
|
|
403
|
+
position: absolute;
|
|
404
|
+
/* Inset to the row's horizontal padding so the line spans the content, not
|
|
405
|
+
the full box edge-to-edge. */
|
|
406
|
+
left: ${COMPACT_ROW_PAD_X}px;
|
|
407
|
+
right: ${COMPACT_ROW_PAD_X}px;
|
|
408
|
+
/* Centred in the inter-field gap (which differs wide vs narrow) so the space
|
|
409
|
+
above and below each line is equal. */
|
|
410
|
+
bottom: calc(var(--readfirst-row-gap, 4px) / -2);
|
|
411
|
+
height: 1px;
|
|
412
|
+
background: ${({ $divider }) => $divider};
|
|
413
|
+
opacity: 0.5;
|
|
414
|
+
pointer-events: none;
|
|
415
|
+
z-index: 0;
|
|
416
|
+
}
|
|
417
|
+
.readfirst-row:last-child::after {
|
|
418
|
+
display: none;
|
|
419
|
+
}
|
|
307
420
|
.readfirst-row:hover {
|
|
308
421
|
background: ${({ $hover }) => $hover};
|
|
309
422
|
}
|
|
@@ -319,8 +432,10 @@ export const StyledGroupBody = styled.div<{
|
|
|
319
432
|
/* A disabled field (schema disabled flag or unmet dependencies) is not a
|
|
320
433
|
click target — no hover invite, not-allowed cursor; a lock replaces the
|
|
321
434
|
pencil. */
|
|
435
|
+
/* A disabled field reads dimmed (its name + value at 0.6) — clearly inactive. */
|
|
322
436
|
.readfirst-row-disabled {
|
|
323
437
|
cursor: not-allowed;
|
|
438
|
+
opacity: 0.6;
|
|
324
439
|
}
|
|
325
440
|
.readfirst-row-disabled:hover {
|
|
326
441
|
background: transparent;
|
|
@@ -341,15 +456,6 @@ export const StyledGroupBody = styled.div<{
|
|
|
341
456
|
background: transparent;
|
|
342
457
|
}
|
|
343
458
|
}
|
|
344
|
-
.readfirst-action {
|
|
345
|
-
opacity: 0;
|
|
346
|
-
transition: opacity 0.12s ease;
|
|
347
|
-
}
|
|
348
|
-
.readfirst-row:hover .readfirst-action,
|
|
349
|
-
.readfirst-row:focus-visible .readfirst-action {
|
|
350
|
-
opacity: 0.85;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
459
|
/* A scalar row being edited in place: the real editor replaces the value
|
|
354
460
|
cell. The row stops being a click target (the editor owns the clicks) and
|
|
355
461
|
keeps a constant active background. Vertical padding is tightened so the
|
|
@@ -396,9 +502,15 @@ export const StyledGroupBody = styled.div<{
|
|
|
396
502
|
rather than a viewport media query, so a slim desktop drawer stacks too.
|
|
397
503
|
The 3 row children are placed explicitly:
|
|
398
504
|
label (1,1) · actions (1,2) · value (2, span both). */
|
|
505
|
+
/* Generous breathing room BETWEEN fields when stacked — far easier to scan and
|
|
506
|
+
less overwhelming on a phone. */
|
|
507
|
+
&.readfirst-narrow {
|
|
508
|
+
--readfirst-row-gap: 18px;
|
|
509
|
+
}
|
|
399
510
|
&.readfirst-narrow .readfirst-row {
|
|
400
511
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
401
|
-
gap:
|
|
512
|
+
gap: 2px 14px;
|
|
513
|
+
position: relative;
|
|
402
514
|
}
|
|
403
515
|
&.readfirst-narrow .readfirst-row > :nth-child(1) {
|
|
404
516
|
grid-column: 1;
|
|
@@ -412,8 +524,26 @@ export const StyledGroupBody = styled.div<{
|
|
|
412
524
|
grid-column: 1 / -1;
|
|
413
525
|
grid-row: 2;
|
|
414
526
|
}
|
|
527
|
+
/* Read (non-editing) rows: float the trailing actions (status dot + the
|
|
528
|
+
hover-revealed delete/revert) out of the grid flow so a delete button can't
|
|
529
|
+
inflate the first row and push the value down. They're pinned to a band the
|
|
530
|
+
height of the LABEL line and vertically centred in it — so the dot always
|
|
531
|
+
sits at the same place (centred on the field name), never the bare corner. */
|
|
532
|
+
&.readfirst-narrow .readfirst-row:not(.readfirst-row-editing) > :nth-child(3) {
|
|
533
|
+
position: absolute;
|
|
534
|
+
top: 2px;
|
|
535
|
+
right: 10px;
|
|
536
|
+
height: 22px;
|
|
537
|
+
}
|
|
538
|
+
&.readfirst-narrow .readfirst-row:not(.readfirst-row-editing) > :nth-child(1) {
|
|
539
|
+
padding-right: 46px;
|
|
540
|
+
}
|
|
415
541
|
|
|
416
|
-
/*
|
|
542
|
+
/* Focus calm rows: NO recessed value surface and NO intent stripe. Row status
|
|
543
|
+
is carried by the trailing status dot; message/preview panels keep their own
|
|
544
|
+
intent backgrounds. The (now invisible) pseudo is retained only to preserve
|
|
545
|
+
the block's positioning/stacking context that the required-group rail and its
|
|
546
|
+
cluster nodes resolve against — removing it would unmoor them. */
|
|
417
547
|
> *:not(.options-readfirst-card) {
|
|
418
548
|
position: relative;
|
|
419
549
|
}
|
|
@@ -422,9 +552,7 @@ export const StyledGroupBody = styled.div<{
|
|
|
422
552
|
position: absolute;
|
|
423
553
|
inset: 0;
|
|
424
554
|
left: ${PANEL_LEFT_CSS};
|
|
425
|
-
background:
|
|
426
|
-
border-radius: 6px;
|
|
427
|
-
border-left: 3px solid var(--readfirst-stripe, transparent);
|
|
555
|
+
background: transparent;
|
|
428
556
|
pointer-events: none;
|
|
429
557
|
z-index: 0;
|
|
430
558
|
}
|
|
@@ -435,45 +563,9 @@ export const StyledGroupBody = styled.div<{
|
|
|
435
563
|
z-index: 1;
|
|
436
564
|
}
|
|
437
565
|
|
|
438
|
-
/*
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
(bottom: -8px) and the end members trim to their node centre, so it reads as a
|
|
442
|
-
single unbroken rail node-to-node. It sits in the existing left gutter (node
|
|
443
|
-
centre ~10px left of the row), so labels keep their place. The node (drawn by
|
|
444
|
-
the row) is opaque and overlaps the rail, masking it — no line through the
|
|
445
|
-
ring. The node centre sits ~19px below the block top (13px node top + 6px). */
|
|
446
|
-
.readfirst-cluster-rail::after {
|
|
447
|
-
content: '';
|
|
448
|
-
position: absolute;
|
|
449
|
-
left: -9px;
|
|
450
|
-
top: 0;
|
|
451
|
-
bottom: -8px;
|
|
452
|
-
width: 2px;
|
|
453
|
-
/* muted info — the connection is a quiet structural hint, not a loud accent */
|
|
454
|
-
background: ${({ $focus }) => `${$focus}99`};
|
|
455
|
-
box-shadow: 0 0 4px ${({ $focus }) => `${$focus}99`};
|
|
456
|
-
pointer-events: none;
|
|
457
|
-
z-index: 0;
|
|
458
|
-
}
|
|
459
|
-
.readfirst-cluster-rail.readfirst-cluster-first::after {
|
|
460
|
-
top: 19px;
|
|
461
|
-
}
|
|
462
|
-
.readfirst-cluster-rail.readfirst-cluster-last::after {
|
|
463
|
-
bottom: calc(100% - 19px);
|
|
464
|
-
}
|
|
465
|
-
/* Group fulfilled (any member set): the whole rail reads success, not just the
|
|
466
|
-
satisfying node. */
|
|
467
|
-
.readfirst-cluster-rail.readfirst-cluster-satisfied::after {
|
|
468
|
-
background: ${({ $success }) => `${$success}99`};
|
|
469
|
-
box-shadow: 0 0 4px ${({ $success }) => `${$success}99`};
|
|
470
|
-
}
|
|
471
|
-
/* Sub-panels (messages, hash preview) indent to the value column so they sit on
|
|
472
|
-
the surface, not in the bare label gutter. */
|
|
473
|
-
.options-readfirst-info-panel,
|
|
474
|
-
.options-readfirst-inset {
|
|
475
|
-
padding-left: ${VALUE_LEFT_CSS};
|
|
476
|
-
}
|
|
566
|
+
/* (The required-group connection rail was removed — the "One of the below is
|
|
567
|
+
required" box now carries the grouping.) */
|
|
568
|
+
|
|
477
569
|
/* A field's short_desc renders under its NAME (revealed by the ⓘ toggle),
|
|
478
570
|
growing the label block to multiple lines. Top-anchor those open rows so the
|
|
479
571
|
value lines up with the name rather than the middle of the taller label;
|
|
@@ -481,18 +573,14 @@ export const StyledGroupBody = styled.div<{
|
|
|
481
573
|
.readfirst-row-info-open {
|
|
482
574
|
align-items: start;
|
|
483
575
|
}
|
|
484
|
-
/* Narrow stacks label-over-value
|
|
485
|
-
|
|
576
|
+
/* Narrow stacks label-over-value: the value aligns flush UNDER the label (no
|
|
577
|
+
indent), like the Focus prototype. */
|
|
486
578
|
&.readfirst-narrow .readfirst-row > :nth-child(2) {
|
|
487
|
-
padding-left:
|
|
579
|
+
padding-left: 0;
|
|
488
580
|
}
|
|
489
581
|
&.readfirst-narrow > *:not(.options-readfirst-card)::before {
|
|
490
582
|
left: 0;
|
|
491
583
|
}
|
|
492
|
-
&.readfirst-narrow .options-readfirst-info-panel,
|
|
493
|
-
&.readfirst-narrow .options-readfirst-inset {
|
|
494
|
-
padding-left: 12px;
|
|
495
|
-
}
|
|
496
584
|
/* Touch layouts have no hover: a slot reserved for the hover-revealed edit
|
|
497
585
|
pencil is permanent dead space that insets every chip from the edge —
|
|
498
586
|
drop it (rows are tap-to-edit; the lock/add slots stay, they're static). */
|
|
@@ -500,32 +588,17 @@ export const StyledGroupBody = styled.div<{
|
|
|
500
588
|
/* !important: the slot carries an inline display for the desktop layout. */
|
|
501
589
|
display: none !important;
|
|
502
590
|
}
|
|
503
|
-
/*
|
|
504
|
-
|
|
591
|
+
/* Stacked rows: keep them tight (match the Focus prototype). The min-height is
|
|
592
|
+
dropped so a 2-line label+value row sizes to its content instead of being
|
|
593
|
+
padded out to 38px. The in-place editor keeps zero padding (below). */
|
|
505
594
|
&.readfirst-narrow .readfirst-row {
|
|
506
|
-
padding-top:
|
|
507
|
-
padding-bottom:
|
|
595
|
+
padding-top: 2px;
|
|
596
|
+
padding-bottom: 2px;
|
|
597
|
+
min-height: 0;
|
|
508
598
|
}
|
|
509
599
|
&.readfirst-narrow .readfirst-row-editing {
|
|
510
600
|
padding-top: 0;
|
|
511
601
|
padding-bottom: 0;
|
|
512
602
|
}
|
|
513
603
|
|
|
514
|
-
/* A hash block = its parent row + the revealed sub-rows. Highlight the whole
|
|
515
|
-
block as one unit on hover (rather than only the parent row), and neutralise
|
|
516
|
-
the parent row's own hover so the two don't stack into a darker band. The
|
|
517
|
-
parent row's hover actions still surface whenever the block is hovered. */
|
|
518
|
-
.options-readfirst-hash-row {
|
|
519
|
-
border-radius: 6px;
|
|
520
|
-
transition: background 0.12s ease;
|
|
521
|
-
}
|
|
522
|
-
.options-readfirst-hash-row:hover {
|
|
523
|
-
background: ${({ $hover }) => $hover};
|
|
524
|
-
}
|
|
525
|
-
.options-readfirst-hash-row:hover .readfirst-row {
|
|
526
|
-
background: transparent;
|
|
527
|
-
}
|
|
528
|
-
.options-readfirst-hash-row:hover .readfirst-action {
|
|
529
|
-
opacity: 0.85;
|
|
530
|
-
}
|
|
531
604
|
`;
|
|
@@ -26,6 +26,7 @@ export interface ICompactToolbarContext {
|
|
|
26
26
|
readOnly?: boolean;
|
|
27
27
|
// Completion meter
|
|
28
28
|
invalidCount: number;
|
|
29
|
+
attentionCount: number;
|
|
29
30
|
completion: { set: number; total: number; pct: number };
|
|
30
31
|
// Invalid-fields banner (pinned in the sticky header)
|
|
31
32
|
showInvalidOnly: boolean;
|
|
@@ -435,6 +435,41 @@ export interface IReadFirstCompletion {
|
|
|
435
435
|
pct: number;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
/** Read-first row status, shared by the status DOT (CompactRow) and the
|
|
439
|
+
* status BOX bucketing (FormEngine) so the two can never disagree:
|
|
440
|
+
* invalid — a value fails validation, or a danger message is present (red)
|
|
441
|
+
* todo — empty & required (or has a warning message), needs a value (amber)
|
|
442
|
+
* set — has a valid value (green)
|
|
443
|
+
* optional — empty & not required, or covered by a one-of sibling (calm) */
|
|
444
|
+
export type TReadFirstStatus = 'invalid' | 'todo' | 'set' | 'optional';
|
|
445
|
+
|
|
446
|
+
export const getReadFirstStatus = (s: {
|
|
447
|
+
empty: boolean;
|
|
448
|
+
required: boolean;
|
|
449
|
+
/** Empty member of a one-of required group already satisfied by a sibling. */
|
|
450
|
+
covered: boolean;
|
|
451
|
+
/** Non-empty value fails validation, or a danger message is attached. */
|
|
452
|
+
invalid: boolean;
|
|
453
|
+
/** A warning message is attached (surfaces an empty field for attention). */
|
|
454
|
+
warned: boolean;
|
|
455
|
+
}): TReadFirstStatus => {
|
|
456
|
+
if (s.invalid) return 'invalid';
|
|
457
|
+
if (!s.empty) return 'set';
|
|
458
|
+
// A one-of member covered by a satisfied sibling reads as "set" (green) — the
|
|
459
|
+
// requirement is met; the row just shows a "Covered by …" note.
|
|
460
|
+
if (s.covered) return 'set';
|
|
461
|
+
if (s.required || s.warned) return 'todo';
|
|
462
|
+
return 'optional';
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/** Coarse bucket for the three status boxes (Needs attention / Set / Optional). */
|
|
466
|
+
export const getReadFirstBucket = (
|
|
467
|
+
status: TReadFirstStatus
|
|
468
|
+
): 'attention' | 'set' | 'optional' =>
|
|
469
|
+
status === 'set' ? 'set'
|
|
470
|
+
: status === 'optional' ? 'optional'
|
|
471
|
+
: 'attention';
|
|
472
|
+
|
|
438
473
|
/** Count how many of the shown options have a value set, for the progress meter. */
|
|
439
474
|
export const getReadFirstCompletion = (
|
|
440
475
|
shownOptions: Record<string, IQorusFormField | undefined> = {}
|
|
@@ -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
|
+
};
|