@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g124fc92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  2. package/dist/components/form/engine/CompactRow.js +130 -25
  3. package/dist/components/form/engine/CompactRow.js.map +1 -1
  4. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  5. package/dist/components/form/engine/CompactToolbar.js +78 -15
  6. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  7. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  8. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  9. package/dist/components/form/engine/FormEngine.js +260 -99
  10. package/dist/components/form/engine/FormEngine.js.map +1 -1
  11. package/dist/components/form/engine/OptionFieldMessages.d.ts +18 -2
  12. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  13. package/dist/components/form/engine/OptionFieldMessages.js +22 -3
  14. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  15. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  16. package/dist/components/form/engine/_structuredData/SchemaDataView.js +27 -1
  17. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  18. package/dist/components/form/engine/compactRowContext.d.ts +5 -0
  19. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  20. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  21. package/dist/components/form/engine/compactRowStyles.d.ts +12 -0
  22. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  23. package/dist/components/form/engine/compactRowStyles.js +72 -39
  24. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  25. package/dist/components/form/engine/compactToolbarContext.d.ts +3 -0
  26. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  27. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  28. package/dist/components/form/engine/formAppearance.d.ts +48 -0
  29. package/dist/components/form/engine/formAppearance.d.ts.map +1 -0
  30. package/dist/components/form/engine/formAppearance.js +50 -0
  31. package/dist/components/form/engine/formAppearance.js.map +1 -0
  32. package/dist/components/form/engine/readFirst.d.ts +14 -2
  33. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  34. package/dist/components/form/engine/readFirst.js +6 -2
  35. package/dist/components/form/engine/readFirst.js.map +1 -1
  36. package/dist/components/form/engine/rowFocus.d.ts +9 -1
  37. package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
  38. package/dist/components/form/engine/rowFocus.js +17 -5
  39. package/dist/components/form/engine/rowFocus.js.map +1 -1
  40. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  41. package/dist/components/form/expressions/renderExpressionToText.js +21 -0
  42. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  43. package/dist/components/form/fields/ChoiceGroupHeading.d.ts +21 -0
  44. package/dist/components/form/fields/ChoiceGroupHeading.d.ts.map +1 -0
  45. package/dist/components/form/fields/ChoiceGroupHeading.js +33 -0
  46. package/dist/components/form/fields/ChoiceGroupHeading.js.map +1 -0
  47. package/dist/components/form/fields/Field.d.ts.map +1 -1
  48. package/dist/components/form/fields/Field.js +13 -11
  49. package/dist/components/form/fields/Field.js.map +1 -1
  50. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  51. package/dist/components/form/fields/allowed-values/AllowedValues.js +20 -7
  52. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  53. package/dist/components/form/fields/allowed-values/savedValues.d.ts +30 -0
  54. package/dist/components/form/fields/allowed-values/savedValues.d.ts.map +1 -0
  55. package/dist/components/form/fields/allowed-values/savedValues.js +18 -0
  56. package/dist/components/form/fields/allowed-values/savedValues.js.map +1 -0
  57. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  58. package/dist/components/form/fields/auto/AutoFormField.js +18 -8
  59. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  60. package/dist/components/form/fields/long-string/LongString.d.ts +7 -1
  61. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  62. package/dist/components/form/fields/long-string/LongString.js +9 -5
  63. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  64. package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
  65. package/dist/components/form/fields/multi-select/MultiSelectFormField.js +6 -5
  66. package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
  67. package/dist/components/form/fields/number/Number.d.ts +4 -1
  68. package/dist/components/form/fields/number/Number.d.ts.map +1 -1
  69. package/dist/components/form/fields/number/Number.js +13 -5
  70. package/dist/components/form/fields/number/Number.js.map +1 -1
  71. package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
  72. package/dist/components/form/fields/radio-group/RadioGroup.js +13 -7
  73. package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
  74. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  75. package/dist/components/form/fields/template/TemplateField.js +71 -42
  76. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  77. package/dist/components/form/index.d.ts +2 -0
  78. package/dist/components/form/index.d.ts.map +1 -1
  79. package/dist/components/form/index.js +2 -0
  80. package/dist/components/form/index.js.map +1 -1
  81. package/dist/components/imageLightbox/ImageLightbox.d.ts.map +1 -1
  82. package/dist/components/imageLightbox/ImageLightbox.js +2 -0
  83. package/dist/components/imageLightbox/ImageLightbox.js.map +1 -1
  84. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +3 -2
  85. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -1
  86. package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -1
  87. package/dist/components/supportTicket/ReferencePicker.d.ts +2 -1
  88. package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
  89. package/dist/components/supportTicket/ReferencePicker.js +19 -13
  90. package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
  91. package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
  92. package/dist/components/supportTicket/TicketHeader.js +4 -1
  93. package/dist/components/supportTicket/TicketHeader.js.map +1 -1
  94. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
  95. package/dist/components/supportTicket/TicketReplyBox.js +29 -10
  96. package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
  97. package/dist/components/supportTicket/useLoadedImages.d.ts +10 -0
  98. package/dist/components/supportTicket/useLoadedImages.d.ts.map +1 -1
  99. package/dist/components/supportTicket/useLoadedImages.js +63 -1
  100. package/dist/components/supportTicket/useLoadedImages.js.map +1 -1
  101. package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -1
  102. package/dist/components/ticketReferences/TicketReferences.js +6 -2
  103. package/dist/components/ticketReferences/TicketReferences.js.map +1 -1
  104. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  105. package/dist/components/ticketThread/TicketThread.js +10 -16
  106. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  107. package/dist/helpers/allowedValues.d.ts +12 -0
  108. package/dist/helpers/allowedValues.d.ts.map +1 -1
  109. package/dist/helpers/allowedValues.js +20 -1
  110. package/dist/helpers/allowedValues.js.map +1 -1
  111. package/dist/helpers/fieldDescription.d.ts +16 -0
  112. package/dist/helpers/fieldDescription.d.ts.map +1 -0
  113. package/dist/helpers/fieldDescription.js +40 -0
  114. package/dist/helpers/fieldDescription.js.map +1 -0
  115. package/dist/helpers/singleLineString.d.ts.map +1 -1
  116. package/dist/helpers/singleLineString.js +1 -0
  117. package/dist/helpers/singleLineString.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/components/form/engine/CompactRow.tsx +143 -23
  120. package/src/components/form/engine/CompactToolbar.tsx +85 -16
  121. package/src/components/form/engine/FormEngine.stories.tsx +25 -13
  122. package/src/components/form/engine/FormEngine.tsx +367 -81
  123. package/src/components/form/engine/FormEngineAppearance.stories.tsx +540 -0
  124. package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +5 -1
  125. package/src/components/form/engine/OptionFieldMessages.tsx +73 -2
  126. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +17 -4
  127. package/src/components/form/engine/compactRowContext.ts +5 -0
  128. package/src/components/form/engine/compactRowStyles.ts +218 -38
  129. package/src/components/form/engine/compactToolbarContext.ts +3 -0
  130. package/src/components/form/engine/formAppearance.ts +78 -0
  131. package/src/components/form/engine/readFirst.ts +16 -3
  132. package/src/components/form/engine/rowFocus.ts +17 -6
  133. package/src/components/form/expressions/renderExpressionToText.ts +15 -0
  134. package/src/components/form/fields/ChoiceGroupHeading.tsx +38 -0
  135. package/src/components/form/fields/Field.tsx +11 -9
  136. package/src/components/form/fields/allowed-values/AllowedValues.tsx +21 -9
  137. package/src/components/form/fields/allowed-values/savedValues.ts +41 -0
  138. package/src/components/form/fields/auto/AutoFormField.tsx +17 -4
  139. package/src/components/form/fields/long-string/LongString.stories.tsx +27 -0
  140. package/src/components/form/fields/long-string/LongString.tsx +14 -3
  141. package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +5 -4
  142. package/src/components/form/fields/number/Number.tsx +19 -4
  143. package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
  144. package/src/components/form/fields/template/TemplateField.tsx +32 -4
  145. package/src/components/form/index.tsx +2 -0
  146. package/src/components/imageLightbox/ImageLightbox.tsx +2 -0
  147. package/src/components/supportTicket/InterfaceReferenceTags.tsx +3 -2
  148. package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -4
  149. package/src/components/supportTicket/ReferencePicker.tsx +22 -12
  150. package/src/components/supportTicket/TicketConversation.stories.tsx +0 -1
  151. package/src/components/supportTicket/TicketHeader.tsx +2 -1
  152. package/src/components/supportTicket/TicketReplyBox.stories.tsx +24 -21
  153. package/src/components/supportTicket/TicketReplyBox.tsx +27 -5
  154. package/src/components/supportTicket/useLoadedImages.ts +20 -0
  155. package/src/components/ticketReferences/TicketReferences.stories.tsx +26 -0
  156. package/src/components/ticketReferences/TicketReferences.tsx +12 -4
  157. package/src/components/ticketThread/TicketThread.stories.tsx +71 -2
  158. package/src/components/ticketThread/TicketThread.tsx +15 -28
  159. package/src/helpers/allowedValues.ts +19 -0
  160. package/src/helpers/fieldDescription.ts +39 -0
  161. package/src/helpers/singleLineString.ts +1 -0
@@ -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 StyledFieldLabel = styled(StyledRowLabel)`
220
+ const StyledFieldLabelBase = styled(StyledRowLabel)<{ $calm?: boolean }>`
220
221
  font-size: 12px;
221
- text-transform: uppercase;
222
- letter-spacing: 0.06em;
223
- opacity: 0.55;
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
- gap: 12px;
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
- border: 1px solid ${({ $accent }) => `${$accent}33`};
231
- /* $bg lets the muted "Optional" box opt into a darker, recessed surface
232
- instead of the faint accent tint the coloured boxes use. */
233
- background: ${({ $accent, $bg }) => $bg || `${$accent}1f`};
234
- border-radius: 10px;
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
- ${({ $surface, $accent, $bg }) =>
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<{ $border: string; $tint: string }>`
268
- border: 1px solid ${({ $border }) => $border};
269
- border-radius: 8px;
270
- background: ${({ $tint }) => $tint};
271
- padding: 2px 6px 6px;
272
- margin: 4px 0;
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: 4px;
279
- gap: 4px;
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
- export const StyledStatusBoxGroupLabel = styled.div`
297
- font-size: 10px;
298
- font-weight: 500;
299
- letter-spacing: 1px;
300
- text-transform: uppercase;
301
- opacity: 0.5;
302
- padding: 8px 0 2px ${GROUP_INDENT};
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
- border: 1px solid ${({ $border }) => $border};
344
- border-radius: 8px;
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.required || s.warned) return 'todo';
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
- return (
70
- firstMatch(container, TEXT_ENTRY_SELECTOR) ?? firstMatch(container, OTHER_CONTROL_SELECTOR)
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
  /**
@@ -4,6 +4,7 @@
4
4
  // can wait uses it: Explain and the Text view's Preview wait for the server's
5
5
  // `dpql/renderExpression` (see `useRenderExpression`). Pure: no
6
6
  // transport/socket import, so it stays out of the LSP/nanoid dependency graph.
7
+ import { richtextToString } from '../../../helpers/common';
7
8
  import { IExpression, IExpressionSchema, IExpressionValue } from './types';
8
9
 
9
10
  /** A symbol made only of non-word characters renders infix (`a == b`). */
@@ -72,8 +73,22 @@ const readArgValue = (arg: unknown): unknown =>
72
73
  ? (arg as { value: unknown }).value
73
74
  : arg;
74
75
 
76
+ /** A rich-text value: the editor's document, a list of element nodes with children. */
77
+ const isRichTextDocument = (value: unknown): value is unknown[] =>
78
+ Array.isArray(value) &&
79
+ value.length > 0 &&
80
+ value.every(
81
+ (node) =>
82
+ !!node && typeof node === 'object' && Array.isArray((node as { children?: unknown }).children)
83
+ );
84
+
75
85
  const renderLiteral = (value: unknown): string => {
76
86
  if (value === undefined || value === null) return 'null';
87
+ /* Rich text is the string it reads as, its template references written out as `$…`.
88
+ Printed as an object it was the editor's node tree — `+([{"type":"paragraph",
89
+ "children":[…]}], …)` on a Qog's Message Content row — which is no value anyone
90
+ wrote, and not what the server renders either. */
91
+ if (isRichTextDocument(value)) return JSON.stringify(richtextToString(value as never));
77
92
  if (typeof value === 'string') return JSON.stringify(value);
78
93
  if (typeof value === 'object') return JSON.stringify(value);
79
94
  return String(value);
@@ -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
+ };