@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g32331ea
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/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +128 -24
- 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 +78 -15
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +48 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +243 -85
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.d.ts +18 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +22 -3
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +27 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +5 -0
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +12 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +72 -39
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +3 -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/formAppearance.d.ts +48 -0
- package/dist/components/form/engine/formAppearance.d.ts.map +1 -0
- package/dist/components/form/engine/formAppearance.js +50 -0
- package/dist/components/form/engine/formAppearance.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +14 -2
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +6 -2
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rowFocus.d.ts +9 -1
- package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
- package/dist/components/form/engine/rowFocus.js +17 -5
- package/dist/components/form/engine/rowFocus.js.map +1 -1
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts +21 -0
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts.map +1 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js +33 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js.map +1 -0
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +5 -3
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +4 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.js +13 -7
- package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketHeader.js +4 -1
- package/dist/components/supportTicket/TicketHeader.js.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.js +29 -10
- package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +141 -22
- package/src/components/form/engine/CompactToolbar.tsx +85 -16
- package/src/components/form/engine/FormEngine.stories.tsx +25 -13
- package/src/components/form/engine/FormEngine.tsx +358 -77
- package/src/components/form/engine/FormEngineAppearance.stories.tsx +540 -0
- package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +5 -1
- package/src/components/form/engine/OptionFieldMessages.tsx +73 -2
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +17 -4
- package/src/components/form/engine/compactRowContext.ts +5 -0
- package/src/components/form/engine/compactRowStyles.ts +218 -38
- package/src/components/form/engine/compactToolbarContext.ts +3 -0
- package/src/components/form/engine/formAppearance.ts +78 -0
- package/src/components/form/engine/readFirst.ts +16 -3
- package/src/components/form/engine/rowFocus.ts +17 -6
- package/src/components/form/fields/ChoiceGroupHeading.tsx +38 -0
- package/src/components/form/fields/Field.tsx +6 -5
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +7 -4
- package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
- package/src/components/form/index.tsx +1 -0
- package/src/components/supportTicket/TicketHeader.tsx +2 -1
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +17 -2
- package/src/components/supportTicket/TicketReplyBox.tsx +27 -5
|
@@ -17,6 +17,7 @@ import React from 'react';
|
|
|
17
17
|
import styled from 'styled-components';
|
|
18
18
|
import { useContextSelector } from 'use-context-selector';
|
|
19
19
|
import { CompactRowContext } from '../compactRowContext';
|
|
20
|
+
import { useIsCalmForm } from '../formAppearance';
|
|
20
21
|
import {
|
|
21
22
|
MONO_FONT_STACK,
|
|
22
23
|
StyledCodePreview,
|
|
@@ -216,13 +217,25 @@ const StyledItemFields = styled.div<{ $border: string; $rule: boolean }>`
|
|
|
216
217
|
* case-sensitive — a service method is `onOrderStatus`, not `ONORDERSTATUS` —
|
|
217
218
|
* and the item heading is a value.
|
|
218
219
|
*/
|
|
219
|
-
const
|
|
220
|
+
const StyledFieldLabelBase = styled(StyledRowLabel)<{ $calm?: boolean }>`
|
|
220
221
|
font-size: 12px;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
/* Calm: the name as written. The calm look forces nothing into capitals, and
|
|
223
|
+
its detail pages no longer do either, so the vocabulary this label borrows
|
|
224
|
+
is sentence case there too. */
|
|
225
|
+
text-transform: ${({ $calm }) => ($calm ? 'none' : 'uppercase')};
|
|
226
|
+
letter-spacing: ${({ $calm }) => ($calm ? 'normal' : '0.06em')};
|
|
227
|
+
opacity: ${({ $calm }) => ($calm ? 0.7 : 0.55)};
|
|
224
228
|
`;
|
|
225
229
|
|
|
230
|
+
/** The record field label in the form's appearance — see `StyledFieldLabelBase`. */
|
|
231
|
+
const StyledFieldLabel = (
|
|
232
|
+
props: Omit<React.ComponentProps<typeof StyledFieldLabelBase>, '$calm'>
|
|
233
|
+
) => {
|
|
234
|
+
const calm = useIsCalmForm();
|
|
235
|
+
|
|
236
|
+
return <StyledFieldLabelBase {...props} $calm={calm} />;
|
|
237
|
+
};
|
|
238
|
+
|
|
226
239
|
const StyledFields = styled.div`
|
|
227
240
|
/* The supporting fields step in under the heading, so the heading keeps the
|
|
228
241
|
left edge to itself. This is the half that answers "the name is written in
|
|
@@ -69,6 +69,11 @@ export interface ICompactRowContext {
|
|
|
69
69
|
// display name in the read-first chip.
|
|
70
70
|
templates?: IReqoreFormTemplates;
|
|
71
71
|
expandedOptions: string[];
|
|
72
|
+
/** Fields the person has opened and left: an empty required one among them is a
|
|
73
|
+
* fault (`todo`); one they have not been to yet is only `pending`. */
|
|
74
|
+
visitedOptions: string[];
|
|
75
|
+
/** The host reported a submit attempt: every empty required field is a fault. */
|
|
76
|
+
revealRequired: boolean;
|
|
72
77
|
highlightedOptions: string[];
|
|
73
78
|
flashedOptions: string[];
|
|
74
79
|
infoPanelOverrides: Record<string, boolean>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReqoreP, ReqorePanel } from '@qoretechnologies/reqore';
|
|
2
|
-
import { GAP_FROM_SIZE, TSizes } from '@qoretechnologies/reqore/dist/constants/sizes';
|
|
2
|
+
import { GAP_FROM_SIZE, PADDING_FROM_SIZE, TSizes } from '@qoretechnologies/reqore/dist/constants/sizes';
|
|
3
3
|
import styled, { css } from 'styled-components';
|
|
4
4
|
|
|
5
5
|
// Styled primitives for the compact (read-first) rows and their editor cards.
|
|
@@ -22,6 +22,18 @@ export const COMPACT_PANEL_LEFT = COMPACT_VALUE_LEFT - 10;
|
|
|
22
22
|
// own horizontal padding so the labels line up flush with the field labels below
|
|
23
23
|
// them (no extra gutter — the spine/rail that used a fluid indent are gone).
|
|
24
24
|
export const GROUP_INDENT = `${COMPACT_ROW_PAD_X}px`;
|
|
25
|
+
/**
|
|
26
|
+
* Calm appearance: the inset of everything inside a section — rows, group
|
|
27
|
+
* headings, the one-of sentence, and the section's own header label, so all of
|
|
28
|
+
* them start on one left edge.
|
|
29
|
+
*/
|
|
30
|
+
export const CALM_SECTION_INSET = 16;
|
|
31
|
+
/**
|
|
32
|
+
* The extra inset a calm section header's label needs to land on that edge:
|
|
33
|
+
* Reqore pads a `normal` panel's title bar by `PADDING_FROM_SIZE.normal`, and
|
|
34
|
+
* the label makes up the rest itself.
|
|
35
|
+
*/
|
|
36
|
+
export const CALM_SECTION_HEADER_INSET = CALM_SECTION_INSET - PADDING_FROM_SIZE.normal;
|
|
25
37
|
|
|
26
38
|
// Measured label column (GLOBAL). The label column sizes to the WIDEST field
|
|
27
39
|
// label across the whole form, clamped to [MIN, MAX]. FormEngine measures the
|
|
@@ -202,10 +214,15 @@ export const StyledCompactPanel = styled(ReqorePanel)<{
|
|
|
202
214
|
|
|
203
215
|
// Compact group header laid out as the panel's `label`: the group name, a
|
|
204
216
|
// hairline that fades toward the status chip, and the chip pinned at the end.
|
|
205
|
-
export const StyledGroupHeader = styled.div
|
|
217
|
+
export const StyledGroupHeader = styled.div<{ $calm?: boolean }>`
|
|
206
218
|
display: flex;
|
|
207
219
|
align-items: center;
|
|
208
|
-
|
|
220
|
+
/* Calm: the label, its quiet count and (on Needs attention) the one amber
|
|
221
|
+
dot read as a single phrase, so they sit closer than the default's
|
|
222
|
+
label-and-chip pair. */
|
|
223
|
+
gap: ${({ $calm }) => ($calm ? 8 : 12)}px;
|
|
224
|
+
/* …and starts on the rows' left edge. */
|
|
225
|
+
padding-left: ${({ $calm }) => ($calm ? CALM_SECTION_HEADER_INSET : 0)}px;
|
|
209
226
|
flex: 1;
|
|
210
227
|
min-width: 0;
|
|
211
228
|
`;
|
|
@@ -225,14 +242,30 @@ export const StyledStatusBox = styled(ReqorePanel)<{
|
|
|
225
242
|
$bg?: string;
|
|
226
243
|
/** The page's own background, for the PINNED header — see below. */
|
|
227
244
|
$surface?: string;
|
|
245
|
+
/**
|
|
246
|
+
* Calm appearance: the box is an ordinary panel — Reqore paints the theme's
|
|
247
|
+
* panel surface and its one neutral hairline — so no tint, no accent edge
|
|
248
|
+
* and no recessed Optional box are drawn over it. Only the radius is kept,
|
|
249
|
+
* so a calm form has the same shape as a default one.
|
|
250
|
+
*/
|
|
251
|
+
$calm?: boolean;
|
|
228
252
|
}>`
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
${({ $calm, $accent, $bg }) =>
|
|
254
|
+
$calm ?
|
|
255
|
+
css`
|
|
256
|
+
&&& {
|
|
257
|
+
border-radius: 10px;
|
|
258
|
+
}
|
|
259
|
+
`
|
|
260
|
+
: css`
|
|
261
|
+
&&& {
|
|
262
|
+
border: 1px solid ${`${$accent}33`};
|
|
263
|
+
/* $bg lets the muted "Optional" box opt into a darker, recessed surface
|
|
264
|
+
instead of the faint accent tint the coloured boxes use. */
|
|
265
|
+
background: ${$bg || `${$accent}1f`};
|
|
266
|
+
border-radius: 10px;
|
|
267
|
+
}
|
|
268
|
+
`}
|
|
236
269
|
|
|
237
270
|
/* A PINNED header needs a surface of its own.
|
|
238
271
|
|
|
@@ -248,13 +281,16 @@ export const StyledStatusBox = styled(ReqorePanel)<{
|
|
|
248
281
|
Optional box already carries an opaque \`$bg\`, so that is used as-is.
|
|
249
282
|
|
|
250
283
|
Only when \`$surface\` is supplied — i.e. only on a form that pins at all —
|
|
251
|
-
so an unpinned box is pixel-identical to before.
|
|
252
|
-
|
|
284
|
+
so an unpinned box is pixel-identical to before.
|
|
285
|
+
|
|
286
|
+
Calm boxes carry no tint, so the pinned header is just the panel surface
|
|
287
|
+
the caller passes as \`$surface\`. */
|
|
288
|
+
${({ $surface, $accent, $bg, $calm }) =>
|
|
253
289
|
$surface ?
|
|
254
290
|
css`
|
|
255
291
|
&&& > .reqore-panel-title {
|
|
256
292
|
background-color: ${$bg || $surface};
|
|
257
|
-
${$bg ? '' : `background-image: linear-gradient(${$accent}1f, ${$accent}1f);`}
|
|
293
|
+
${$bg || $calm ? '' : `background-image: linear-gradient(${$accent}1f, ${$accent}1f);`}
|
|
258
294
|
}
|
|
259
295
|
`
|
|
260
296
|
: ''}
|
|
@@ -264,42 +300,76 @@ export const StyledStatusBox = styled(ReqorePanel)<{
|
|
|
264
300
|
// one-of required group (in the Needs-attention box) so the constraint reads as
|
|
265
301
|
// one unit. The connection rail + status nodes still render inside (they convey
|
|
266
302
|
// which member satisfies the group); this box adds the explicit heading.
|
|
267
|
-
export const StyledRequiredClusterBox = styled.div<{
|
|
268
|
-
border:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
303
|
+
export const StyledRequiredClusterBox = styled.div<{
|
|
304
|
+
$border: string;
|
|
305
|
+
$tint: string;
|
|
306
|
+
/** Calm: no box of its own — the heading and the hairlines carry the group. */
|
|
307
|
+
$calm?: boolean;
|
|
308
|
+
}>`
|
|
309
|
+
${({ $calm, $border, $tint }) =>
|
|
310
|
+
$calm ?
|
|
311
|
+
''
|
|
312
|
+
: css`
|
|
313
|
+
border: 1px solid ${$border};
|
|
314
|
+
border-radius: 8px;
|
|
315
|
+
background: ${$tint};
|
|
316
|
+
padding: 2px 6px 6px;
|
|
317
|
+
margin: 4px 0;
|
|
318
|
+
`}
|
|
273
319
|
/* The members render directly here (not via the gapped group body), so give
|
|
274
320
|
them the same modest gap the rest of the rows have. Pin the divider-centring
|
|
275
321
|
var to that gap (it doesn't widen in narrow like the group body's does). */
|
|
276
322
|
display: flex;
|
|
277
323
|
flex-flow: column;
|
|
278
|
-
--readfirst-row-gap:
|
|
279
|
-
gap:
|
|
324
|
+
--readfirst-row-gap: ${({ $calm }) => ($calm ? 0 : 4)}px;
|
|
325
|
+
gap: ${({ $calm }) => ($calm ? 0 : 4)}px;
|
|
280
326
|
`;
|
|
281
|
-
export const StyledRequiredClusterHeader = styled.div<{ $color: string }>`
|
|
327
|
+
export const StyledRequiredClusterHeader = styled.div<{ $color: string; $calm?: boolean }>`
|
|
282
328
|
display: flex;
|
|
283
329
|
align-items: center;
|
|
284
330
|
gap: 6px;
|
|
285
|
-
padding: 8px 0 4px ${GROUP_INDENT};
|
|
286
|
-
font-size: 10px;
|
|
287
|
-
font-weight: 600;
|
|
288
|
-
letter-spacing: 1px;
|
|
289
|
-
text-transform: uppercase;
|
|
290
331
|
color: ${({ $color }) => $color};
|
|
332
|
+
font-weight: 600;
|
|
333
|
+
/* Calm: an amber sentence over the rows it is about, at the rows' own text
|
|
334
|
+
size and inset — the instruction, not an upper-case eyebrow. */
|
|
335
|
+
${({ $calm }) =>
|
|
336
|
+
$calm ?
|
|
337
|
+
css`
|
|
338
|
+
padding: 12px ${CALM_SECTION_INSET}px 2px;
|
|
339
|
+
font-size: 13px;
|
|
340
|
+
`
|
|
341
|
+
: css`
|
|
342
|
+
padding: 8px 0 4px ${GROUP_INDENT};
|
|
343
|
+
font-size: 10px;
|
|
344
|
+
letter-spacing: 1px;
|
|
345
|
+
text-transform: uppercase;
|
|
346
|
+
`}
|
|
291
347
|
`;
|
|
292
348
|
|
|
293
349
|
// Thin schema-group sub-label inside a status box (CONNECTION / AUTHENTICATION /
|
|
294
350
|
// …). Quiet by design — the box header is the loud heading; this just keeps each
|
|
295
351
|
// field's group context as you scan. Indented to the rows' content line.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
352
|
+
//
|
|
353
|
+
// Calm: the group's name as written (no upper-casing), at the rows' text size in
|
|
354
|
+
// the primary text colour — the field labels below it are grey, so the heading
|
|
355
|
+
// is told apart by colour and the space above it, not by shouting.
|
|
356
|
+
export const StyledStatusBoxGroupLabel = styled.div<{ $calm?: boolean; $color?: string }>`
|
|
357
|
+
${({ $calm, $color }) =>
|
|
358
|
+
$calm ?
|
|
359
|
+
css`
|
|
360
|
+
font-size: 13px;
|
|
361
|
+
font-weight: 600;
|
|
362
|
+
color: ${$color || 'inherit'};
|
|
363
|
+
padding: 14px ${CALM_SECTION_INSET}px 2px;
|
|
364
|
+
`
|
|
365
|
+
: css`
|
|
366
|
+
font-size: 10px;
|
|
367
|
+
font-weight: 500;
|
|
368
|
+
letter-spacing: 1px;
|
|
369
|
+
text-transform: uppercase;
|
|
370
|
+
opacity: 0.5;
|
|
371
|
+
padding: 8px 0 2px ${GROUP_INDENT};
|
|
372
|
+
`}
|
|
303
373
|
`;
|
|
304
374
|
|
|
305
375
|
// Required-group "connection" rail: contiguous members are linked by a continuous
|
|
@@ -334,14 +404,24 @@ export const StyledClusterNode = styled.span<{ $color: string; $filled: boolean;
|
|
|
334
404
|
}
|
|
335
405
|
`;
|
|
336
406
|
|
|
337
|
-
export const StyledEditCard = styled.div<{ $bg: string; $border: string }>`
|
|
338
|
-
padding: 12px;
|
|
407
|
+
export const StyledEditCard = styled.div<{ $bg: string; $border: string; $calm?: boolean }>`
|
|
339
408
|
display: flex;
|
|
340
409
|
flex-flow: column;
|
|
341
410
|
gap: 8px;
|
|
342
411
|
background: ${({ $bg }) => $bg};
|
|
343
|
-
|
|
344
|
-
|
|
412
|
+
/* Calm: an open field is a row of its section, set apart by the section's own
|
|
413
|
+
hairlines and the active-row tint — not a second card drawn inside the
|
|
414
|
+
first. */
|
|
415
|
+
${({ $calm, $border }) =>
|
|
416
|
+
$calm ?
|
|
417
|
+
css`
|
|
418
|
+
padding: 12px ${CALM_SECTION_INSET}px;
|
|
419
|
+
`
|
|
420
|
+
: css`
|
|
421
|
+
padding: 12px;
|
|
422
|
+
border: 1px solid ${$border};
|
|
423
|
+
border-radius: 8px;
|
|
424
|
+
`}
|
|
345
425
|
|
|
346
426
|
/* Single-value editors (operators, long-string, markdown, richtext, byte-size)
|
|
347
427
|
carry the card's own "Clear value" action, so suppress the value input's
|
|
@@ -566,6 +646,10 @@ export const StyledGroupBody = styled.div<{
|
|
|
566
646
|
HAS to be seen — the divider tint that separates two fields is ~8% of the
|
|
567
647
|
text colour, which disappears entirely as a 1px dashed border. */
|
|
568
648
|
$moreBorder: string;
|
|
649
|
+
/** Calm appearance: label/value rows split by full-width hairlines. */
|
|
650
|
+
$calm?: boolean;
|
|
651
|
+
/** The calm section's hairline — the same edge its panel is drawn with. */
|
|
652
|
+
$hairline?: string;
|
|
569
653
|
}>`
|
|
570
654
|
display: flex;
|
|
571
655
|
flex-flow: column;
|
|
@@ -871,6 +955,102 @@ export const StyledGroupBody = styled.div<{
|
|
|
871
955
|
padding-top: 0;
|
|
872
956
|
padding-bottom: 0;
|
|
873
957
|
}
|
|
958
|
+
|
|
959
|
+
/* CALM appearance — a section of label/value rows split by hairlines.
|
|
960
|
+
|
|
961
|
+
Every entry the body holds (a row, the column an open row sits in, a
|
|
962
|
+
group heading, the one-of cluster, the "N more fields" control) is drawn
|
|
963
|
+
edge to edge with ONE full-width hairline above it, the same colour as the
|
|
964
|
+
section's own edge. The first entry's line is the one under the section
|
|
965
|
+
header (a minimal header draws none of its own). Two things take no line,
|
|
966
|
+
because a heading and the rows it heads read as one block: the entry right
|
|
967
|
+
under a group heading (or its subtitle), and the first row under the
|
|
968
|
+
cluster's sentence.
|
|
969
|
+
|
|
970
|
+
Rows lose their rounded hover pill and the inset hairline between them, and
|
|
971
|
+
take the section's 16px inset. Placed last so it wins over the narrow
|
|
972
|
+
rules above, which a phone-width calm form also matches. */
|
|
973
|
+
${({ $calm, $hairline }) =>
|
|
974
|
+
$calm ?
|
|
975
|
+
css`
|
|
976
|
+
&& {
|
|
977
|
+
--readfirst-row-gap: 0px;
|
|
978
|
+
gap: 0;
|
|
979
|
+
}
|
|
980
|
+
&& > *,
|
|
981
|
+
&& > .options-readfirst-required-cluster > * {
|
|
982
|
+
border-top: 1px solid ${$hairline};
|
|
983
|
+
}
|
|
984
|
+
&& > .options-readfirst-group-label + *,
|
|
985
|
+
&& > .options-readfirst-group-subtitle + *,
|
|
986
|
+
&& .options-readfirst-required-cluster-header,
|
|
987
|
+
&& .options-readfirst-required-cluster-header + * {
|
|
988
|
+
border-top: 0;
|
|
989
|
+
}
|
|
990
|
+
&& .readfirst-row {
|
|
991
|
+
min-height: 44px;
|
|
992
|
+
padding: 10px ${CALM_SECTION_INSET}px;
|
|
993
|
+
border-radius: 0;
|
|
994
|
+
}
|
|
995
|
+
&& .readfirst-row::after {
|
|
996
|
+
display: none;
|
|
997
|
+
}
|
|
998
|
+
/* The open row keeps the read row's band: 3px above the editor's 32px
|
|
999
|
+
control centres it where the read row centred its text. */
|
|
1000
|
+
&& .readfirst-row-editing {
|
|
1001
|
+
padding-top: 3px;
|
|
1002
|
+
padding-bottom: 10px;
|
|
1003
|
+
}
|
|
1004
|
+
/* The section's rounded bottom corners, which a full-bleed hover or an
|
|
1005
|
+
open row's tint would otherwise square off. */
|
|
1006
|
+
&& > :last-child,
|
|
1007
|
+
&& > :last-child > :last-child {
|
|
1008
|
+
border-bottom-left-radius: 9px;
|
|
1009
|
+
border-bottom-right-radius: 9px;
|
|
1010
|
+
}
|
|
1011
|
+
/* "N more fields" is a quiet last row: the hairline above it says where
|
|
1012
|
+
the section's fields end; a dashed box around it said it again. */
|
|
1013
|
+
&& .readfirst-more-row {
|
|
1014
|
+
border: 0;
|
|
1015
|
+
border-top: 1px solid ${$hairline};
|
|
1016
|
+
margin-top: 0;
|
|
1017
|
+
}
|
|
1018
|
+
&&.readfirst-narrow {
|
|
1019
|
+
--readfirst-row-gap: 0px;
|
|
1020
|
+
}
|
|
1021
|
+
&&.readfirst-narrow .readfirst-row {
|
|
1022
|
+
padding: 10px ${CALM_SECTION_INSET}px;
|
|
1023
|
+
min-height: 0;
|
|
1024
|
+
}
|
|
1025
|
+
&&.readfirst-narrow .readfirst-row-editing {
|
|
1026
|
+
padding-top: 3px;
|
|
1027
|
+
padding-bottom: 10px;
|
|
1028
|
+
}
|
|
1029
|
+
&&.readfirst-narrow .readfirst-row:not(.readfirst-row-editing) > :nth-child(3) {
|
|
1030
|
+
top: 10px;
|
|
1031
|
+
right: ${CALM_SECTION_INSET}px;
|
|
1032
|
+
}
|
|
1033
|
+
/* READ mode is one list with no section around it — usually inside a
|
|
1034
|
+
card the host already drew — so it adds no panel and no inset of its
|
|
1035
|
+
own: the rows sit flush with the host's content edge, split by the
|
|
1036
|
+
same hairlines, with no line above the first. */
|
|
1037
|
+
&&.readfirst-read-list > :first-child {
|
|
1038
|
+
border-top: 0;
|
|
1039
|
+
}
|
|
1040
|
+
&&.readfirst-read-list .readfirst-row,
|
|
1041
|
+
&&.readfirst-read-list .options-readfirst-group-label,
|
|
1042
|
+
&&.readfirst-read-list .options-readfirst-group-subtitle {
|
|
1043
|
+
padding-left: 0;
|
|
1044
|
+
padding-right: 0;
|
|
1045
|
+
margin-left: 0;
|
|
1046
|
+
}
|
|
1047
|
+
&&.readfirst-read-list.readfirst-narrow
|
|
1048
|
+
.readfirst-row:not(.readfirst-row-editing)
|
|
1049
|
+
> :nth-child(3) {
|
|
1050
|
+
right: 0;
|
|
1051
|
+
}
|
|
1052
|
+
`
|
|
1053
|
+
: ''}
|
|
874
1054
|
`;
|
|
875
1055
|
|
|
876
1056
|
// One absorbed sibling, rendered inside its host row's container rather than
|
|
@@ -35,6 +35,9 @@ export interface ICompactToolbarContext {
|
|
|
35
35
|
// Completion meter
|
|
36
36
|
invalidCount: number;
|
|
37
37
|
attentionCount: number;
|
|
38
|
+
/** How many of `attentionCount` are faults (invalid, or a required field the person
|
|
39
|
+
* has been to and left empty). Zero on a fresh form: the rest are only to fill in. */
|
|
40
|
+
attentionFaultCount: number;
|
|
38
41
|
completion: { set: number; done: number; total: number; pct: number };
|
|
39
42
|
// Invalid-fields banner (pinned in the sticky header)
|
|
40
43
|
showInvalidOnly: boolean;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { IReqoreTheme } from '@qoretechnologies/reqore/dist/constants/theme';
|
|
2
|
+
import {
|
|
3
|
+
changeDarkness,
|
|
4
|
+
changeLightness,
|
|
5
|
+
getMainBackgroundColor,
|
|
6
|
+
} from '@qoretechnologies/reqore/dist/helpers/colors';
|
|
7
|
+
import { createContext, useContext } from 'react';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* How a form looks. The look only: which fields exist, how they bucket, what a
|
|
11
|
+
* click does and what is validated are identical in every appearance.
|
|
12
|
+
*
|
|
13
|
+
* - `'default'` — the established read-first look: tinted status boxes, an
|
|
14
|
+
* intent-coloured completion meter, a status dot on every row.
|
|
15
|
+
* - `'calm'` — colour only where something needs a person. The status boxes are
|
|
16
|
+
* plain panels on the theme's panel surface with label/value rows split by
|
|
17
|
+
* hairlines; labels are grey; a missing required value is an amber word
|
|
18
|
+
* instead of a red asterisk and message; counts are quiet numbers; a healthy
|
|
19
|
+
* row carries no dot; Yes/No are plain words; nothing is forced into
|
|
20
|
+
* uppercase. Opt-in, so an app that has not asked for it does not change.
|
|
21
|
+
*/
|
|
22
|
+
export type TFormEngineAppearance = 'default' | 'calm';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The appearance in force for everything below it: the engine's own rows and
|
|
26
|
+
* toolbar, every field editor it mounts, and every nested (`arg_schema`)
|
|
27
|
+
* sub-form, which inherits it unless it is given one of its own.
|
|
28
|
+
*
|
|
29
|
+
* `FormEngine` provides it from its `appearance` prop. A host may also wrap
|
|
30
|
+
* its whole tree in it, to set the appearance once for every form — and for
|
|
31
|
+
* the field components it renders outside a form.
|
|
32
|
+
*/
|
|
33
|
+
export const FormAppearanceContext = createContext<TFormEngineAppearance | undefined>(undefined);
|
|
34
|
+
|
|
35
|
+
export const FormAppearanceProvider = FormAppearanceContext.Provider;
|
|
36
|
+
|
|
37
|
+
/** The appearance in force here; `'default'` when nothing set one. */
|
|
38
|
+
export const useFormAppearance = (): TFormEngineAppearance =>
|
|
39
|
+
useContext(FormAppearanceContext) ?? 'default';
|
|
40
|
+
|
|
41
|
+
/** Shorthand for the one question most call sites ask. */
|
|
42
|
+
export const useIsCalmForm = (): boolean => useFormAppearance() === 'calm';
|
|
43
|
+
|
|
44
|
+
/** The theme's own panel surface, when it declares one (Reqore >= 0.78.6). */
|
|
45
|
+
const getDeclaredPanelSurface = (
|
|
46
|
+
theme: IReqoreTheme
|
|
47
|
+
): { main?: string; border?: string } | undefined => {
|
|
48
|
+
const themed = theme as IReqoreTheme & {
|
|
49
|
+
panel?: { main?: string; border?: string };
|
|
50
|
+
originalMain?: string;
|
|
51
|
+
};
|
|
52
|
+
// Same rule as Reqore's `getThemePanelSurface`: a component whose `main` was
|
|
53
|
+
// moved off the original (a custom theme, an intent) keeps deriving its own.
|
|
54
|
+
return themed.panel && (!themed.originalMain || themed.main === themed.originalMain) ?
|
|
55
|
+
themed.panel
|
|
56
|
+
: undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The surface a calm section paints, matching the one Reqore gives a panel:
|
|
61
|
+
* `theme.panel.main` when the theme declares one, else the panel shade Reqore
|
|
62
|
+
* derives from `main`. Read here rather than through Reqore's own helper so the
|
|
63
|
+
* whole peer range keeps working — the helper arrived after the floor.
|
|
64
|
+
*/
|
|
65
|
+
export const getCalmPanelSurface = (theme: IReqoreTheme): string =>
|
|
66
|
+
getDeclaredPanelSurface(theme)?.main ?? changeDarkness(getMainBackgroundColor(theme), 0.03);
|
|
67
|
+
|
|
68
|
+
/** The neutral 1px edge of a calm section, and of every row inside it. */
|
|
69
|
+
export const getCalmPanelHairline = (theme: IReqoreTheme): string =>
|
|
70
|
+
getDeclaredPanelSurface(theme)?.border ?? changeLightness(getMainBackgroundColor(theme), 0.08);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What a calm form writes in place of an empty required value. Editable: what
|
|
74
|
+
* to do about it. Read-only: only what is the case, because the reader cannot
|
|
75
|
+
* add anything.
|
|
76
|
+
*/
|
|
77
|
+
export const CALM_REQUIRED_LABEL = 'Required · add a value';
|
|
78
|
+
export const CALM_REQUIRED_READ_LABEL = 'Required · not set';
|
|
@@ -854,9 +854,13 @@ export interface IReadFirstCompletion {
|
|
|
854
854
|
* status BOX bucketing (FormEngine) so the two can never disagree:
|
|
855
855
|
* invalid — a value fails validation, or a danger message is present (red)
|
|
856
856
|
* todo — empty & required (or has a warning message), needs a value (amber)
|
|
857
|
+
* pending — empty & required, but the person has not been to it yet: still to
|
|
858
|
+
* fill in, not yet a fault (neutral). A new form is not already wrong;
|
|
859
|
+
* the field becomes `todo` once they open it and leave it empty, or the
|
|
860
|
+
* host reports a submit attempt (`revealRequired`)
|
|
857
861
|
* set — has a valid value (green)
|
|
858
862
|
* optional — empty & not required, or covered by a one-of sibling (calm) */
|
|
859
|
-
export type TReadFirstStatus = 'invalid' | 'todo' | 'set' | 'optional';
|
|
863
|
+
export type TReadFirstStatus = 'invalid' | 'todo' | 'pending' | 'set' | 'optional';
|
|
860
864
|
|
|
861
865
|
export const getReadFirstStatus = (s: {
|
|
862
866
|
empty: boolean;
|
|
@@ -867,17 +871,26 @@ export const getReadFirstStatus = (s: {
|
|
|
867
871
|
invalid: boolean;
|
|
868
872
|
/** A warning message is attached (surfaces an empty field for attention). */
|
|
869
873
|
warned: boolean;
|
|
874
|
+
/**
|
|
875
|
+
* Whether the person has been to this field (opened it and left it), or the host
|
|
876
|
+
* reported a submit attempt. Until then an empty required field is `pending`, not
|
|
877
|
+
* `todo`. Unset means visited, the behaviour before this existed.
|
|
878
|
+
*/
|
|
879
|
+
visited?: boolean;
|
|
870
880
|
}): TReadFirstStatus => {
|
|
871
881
|
if (s.invalid) return 'invalid';
|
|
872
882
|
if (!s.empty) return 'set';
|
|
873
883
|
// A one-of member covered by a satisfied sibling reads as "set" (green) — the
|
|
874
884
|
// requirement is met; the row just shows a "Covered by …" note.
|
|
875
885
|
if (s.covered) return 'set';
|
|
876
|
-
if (s.
|
|
886
|
+
if (s.warned) return 'todo';
|
|
887
|
+
if (s.required) return s.visited === false ? 'pending' : 'todo';
|
|
877
888
|
return 'optional';
|
|
878
889
|
};
|
|
879
890
|
|
|
880
|
-
/** Coarse bucket for the three status boxes (Needs attention / Set / Optional).
|
|
891
|
+
/** Coarse bucket for the three status boxes (Needs attention / Set / Optional). A
|
|
892
|
+
* `pending` field is in the first box too: it still has to be filled in. The box only
|
|
893
|
+
* reads "To fill in" instead of "Needs attention" while nothing in it is a fault. */
|
|
881
894
|
export const getReadFirstBucket = (status: TReadFirstStatus): 'attention' | 'set' | 'optional' =>
|
|
882
895
|
status === 'set' ? 'set'
|
|
883
896
|
: status === 'optional' ? 'optional'
|
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* focusable element" would answer a text field with a button beside it.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
/** Controls the reader types into.
|
|
18
|
+
/** Controls the reader types into. `role="textbox"` is how an editor that is neither
|
|
19
|
+
* an input nor contenteditable says so: Monaco's edit area is a focusable div with that
|
|
20
|
+
* role (its own `<textarea>` is an `aria-hidden` IME helper). */
|
|
19
21
|
const TEXT_ENTRY_SELECTOR =
|
|
20
|
-
'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), [contenteditable="true"]';
|
|
22
|
+
'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), [contenteditable="true"], [role="textbox"]:not([aria-disabled="true"])';
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Everything else a row can offer. `[tabindex]` is what carries reqore's own
|
|
@@ -58,17 +60,26 @@ const firstMatch = (container: HTMLElement, selector: string): HTMLElement | und
|
|
|
58
60
|
* a read-only preview, or an editor that has not mounted yet (see the caller,
|
|
59
61
|
* which watches for it to arrive).
|
|
60
62
|
*
|
|
63
|
+
* `textEntryOnly` is for a field that opens as a CARD (a long text, a list, source
|
|
64
|
+
* code). A card is a form in its own right, so its first button is one of several —
|
|
65
|
+
* a toolbar's "fullscreen", a list's "add" — and nobody asked for it; a caret there
|
|
66
|
+
* turns the next space typed into a press. A card takes the caret only where text
|
|
67
|
+
* goes, and waits for it when its editor mounts late (a code editor does).
|
|
68
|
+
*
|
|
61
69
|
* @param container the row's value cell
|
|
62
70
|
*/
|
|
63
71
|
export const findRowFocusTarget = (
|
|
64
|
-
container: HTMLElement | null | undefined
|
|
72
|
+
container: HTMLElement | null | undefined,
|
|
73
|
+
{ textEntryOnly = false }: { textEntryOnly?: boolean } = {}
|
|
65
74
|
): HTMLElement | undefined => {
|
|
66
75
|
if (!container) {
|
|
67
76
|
return undefined;
|
|
68
77
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
const textEntry = firstMatch(container, TEXT_ENTRY_SELECTOR);
|
|
79
|
+
if (textEntry || textEntryOnly) {
|
|
80
|
+
return textEntry;
|
|
81
|
+
}
|
|
82
|
+
return firstMatch(container, OTHER_CONTROL_SELECTOR);
|
|
72
83
|
};
|
|
73
84
|
|
|
74
85
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ReqoreSpan } from '@qoretechnologies/reqore';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { useIsCalmForm } from '../engine/formAppearance';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The heading over a small group of choices (the checkboxes a field with three
|
|
7
|
+
* or fewer allowed values renders) — the field's own name, or "Select one:"
|
|
8
|
+
* when it has none.
|
|
9
|
+
*
|
|
10
|
+
* One component so both choice editors head their groups the same way, in
|
|
11
|
+
* either appearance: an upper-case micro label by default; in the calm look the
|
|
12
|
+
* words as written, at the small text size, because nothing in a calm form is
|
|
13
|
+
* forced into capitals.
|
|
14
|
+
*/
|
|
15
|
+
export const ChoiceGroupHeading = memo(({ children }: { children: React.ReactNode }) => {
|
|
16
|
+
const calm = useIsCalmForm();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<ReqoreSpan
|
|
20
|
+
className='reqraft-choice-group-heading'
|
|
21
|
+
effect={calm ? { opacity: 0.7, weight: 'bold' } : { opacity: 0.6, uppercase: true, weight: 'bold' }}
|
|
22
|
+
size={calm ? 'small' : 'tiny'}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</ReqoreSpan>
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The intent a picked choice in those groups takes. Blue by default; none in
|
|
31
|
+
* the calm look, where blue says only "a filter is on" and the checked mark
|
|
32
|
+
* already says which choice is picked.
|
|
33
|
+
*/
|
|
34
|
+
export const useSelectedChoiceIntent = (): ((selected: boolean) => 'info' | undefined) => {
|
|
35
|
+
const calm = useIsCalmForm();
|
|
36
|
+
|
|
37
|
+
return (selected) => (selected && !calm ? 'info' : undefined);
|
|
38
|
+
};
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
IReqoreLabelProps,
|
|
3
3
|
ReqoreCheckbox,
|
|
4
4
|
ReqoreControlGroup,
|
|
5
|
-
ReqoreSpan,
|
|
6
5
|
} from '@qoretechnologies/reqore';
|
|
7
6
|
import { IWithReqoreSize } from '@qoretechnologies/reqore/dist/types/global';
|
|
8
7
|
import { TReqoreBadge } from '@qoretechnologies/reqore/dist/components/Button';
|
|
@@ -34,6 +33,7 @@ import {
|
|
|
34
33
|
UNAVAILABLE_ITEM_CLASS,
|
|
35
34
|
} from './select/SelectCollection';
|
|
36
35
|
import { useAllowedValueAvailability } from '../engine/OptionFieldMessages';
|
|
36
|
+
import { ChoiceGroupHeading, useSelectedChoiceIntent } from './ChoiceGroupHeading';
|
|
37
37
|
import { StringFormField } from './string/String';
|
|
38
38
|
import { ArrayAutoField } from './array/ArrayAutoField';
|
|
39
39
|
import { AutoFormField } from './auto/AutoFormField';
|
|
@@ -152,6 +152,7 @@ export const FormField = <T extends TFormFieldType>({
|
|
|
152
152
|
...rest
|
|
153
153
|
}: IFormFieldProps<T>) => {
|
|
154
154
|
const id = useId();
|
|
155
|
+
const selectedChoiceIntent = useSelectedChoiceIntent();
|
|
155
156
|
const { schema: finalArgSchema, loading: argSchemaLoading } = useArgSchema(arg_schema);
|
|
156
157
|
|
|
157
158
|
const handleChange = (value: TFormFieldValueType<T>, event?: unknown) => {
|
|
@@ -435,7 +436,7 @@ export const FormField = <T extends TFormFieldType>({
|
|
|
435
436
|
label={item.display_name ?? String(itemValue)}
|
|
436
437
|
tooltip={item.short_desc || item.desc}
|
|
437
438
|
checked={checked}
|
|
438
|
-
intent={checked
|
|
439
|
+
intent={selectedChoiceIntent(checked)}
|
|
439
440
|
onClick={() => {
|
|
440
441
|
const next =
|
|
441
442
|
checked ?
|
|
@@ -653,11 +654,11 @@ export const FormField = <T extends TFormFieldType>({
|
|
|
653
654
|
if (allowed_values.length <= 3) {
|
|
654
655
|
return (
|
|
655
656
|
<ReqoreControlGroup vertical size='tiny'>
|
|
656
|
-
<
|
|
657
|
+
<ChoiceGroupHeading>
|
|
657
658
|
{/* Headed by the field's own name; "Select one:" only when it has
|
|
658
659
|
none. Same rule as FieldAllowedValuesCheckGroup. */}
|
|
659
660
|
{display_name ? `${display_name}:` : 'Select one:'}
|
|
660
|
-
</
|
|
661
|
+
</ChoiceGroupHeading>
|
|
661
662
|
{allowed_values.map((item, index) => {
|
|
662
663
|
const itemValue = item.value?.value;
|
|
663
664
|
const checked = isEqual(value, itemValue);
|
|
@@ -695,7 +696,7 @@ export const FormField = <T extends TFormFieldType>({
|
|
|
695
696
|
readOnly={!!unavailable}
|
|
696
697
|
uncheckedIcon={unavailable ? 'LockLine' : undefined}
|
|
697
698
|
checked={checked}
|
|
698
|
-
intent={checked
|
|
699
|
+
intent={selectedChoiceIntent(checked)}
|
|
699
700
|
onClick={
|
|
700
701
|
unavailable ? undefined : (
|
|
701
702
|
() => handleChange((checked ? undefined : itemValue) as TFormFieldValueType<T>)
|