@qoretechnologies/reqraft 0.10.43-pr.95.gf7f712f → 0.10.43

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 (152) hide show
  1. package/.claude/CLAUDE.md +12 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
  4. package/dist/components/composer/Composer.d.ts.map +1 -1
  5. package/dist/components/composer/Composer.js +5 -1
  6. package/dist/components/composer/Composer.js.map +1 -1
  7. package/dist/components/form/engine/CompactRow.d.ts +20 -1
  8. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  9. package/dist/components/form/engine/CompactRow.js +279 -41
  10. package/dist/components/form/engine/CompactRow.js.map +1 -1
  11. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  12. package/dist/components/form/engine/CompactToolbar.js +72 -72
  13. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  14. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  15. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  16. package/dist/components/form/engine/FormEngine.js +163 -75
  17. package/dist/components/form/engine/FormEngine.js.map +1 -1
  18. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  19. package/dist/components/form/engine/_structuredData/SchemaDataView.js +117 -88
  20. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  21. package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
  22. package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
  23. package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
  24. package/dist/components/form/engine/_structuredData/structuredData.js.map +1 -1
  25. package/dist/components/form/engine/compactRowStyles.d.ts +2 -0
  26. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  27. package/dist/components/form/engine/compactRowStyles.js +21 -4
  28. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  29. package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
  30. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  31. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  32. package/dist/components/form/engine/readFirst.d.ts +57 -1
  33. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  34. package/dist/components/form/engine/readFirst.js +141 -4
  35. package/dist/components/form/engine/readFirst.js.map +1 -1
  36. package/dist/components/form/fields/Field.d.ts.map +1 -1
  37. package/dist/components/form/fields/Field.js +1 -1
  38. package/dist/components/form/fields/Field.js.map +1 -1
  39. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
  40. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  41. package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
  42. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  43. package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
  44. package/dist/components/form/fields/array/ArrayAuto.js +44 -26
  45. package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
  46. package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
  47. package/dist/components/form/fields/array/ArrayAutoField.js +42 -25
  48. package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
  49. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  50. package/dist/components/form/fields/auto/AutoFormField.js +64 -14
  51. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  52. package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
  53. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  54. package/dist/components/form/fields/long-string/LongString.js +25 -4
  55. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  56. package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
  57. package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
  58. package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
  59. package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
  60. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  61. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
  62. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  63. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  64. package/dist/components/form/fields/template/TemplateField.js +35 -6
  65. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  66. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
  67. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
  68. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
  69. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
  70. package/dist/components/form/index.d.ts +1 -0
  71. package/dist/components/form/index.d.ts.map +1 -1
  72. package/dist/components/form/index.js +1 -0
  73. package/dist/components/form/index.js.map +1 -1
  74. package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
  75. package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
  76. package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
  77. package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
  78. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  79. package/dist/components/ticketThread/TicketThread.js +1 -1
  80. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  81. package/dist/helpers/common.d.ts +8 -1
  82. package/dist/helpers/common.d.ts.map +1 -1
  83. package/dist/helpers/common.js +9 -1
  84. package/dist/helpers/common.js.map +1 -1
  85. package/dist/helpers/optionUiTypes.d.ts +30 -0
  86. package/dist/helpers/optionUiTypes.d.ts.map +1 -0
  87. package/dist/helpers/optionUiTypes.js +148 -0
  88. package/dist/helpers/optionUiTypes.js.map +1 -0
  89. package/dist/helpers/options.d.ts +19 -0
  90. package/dist/helpers/options.d.ts.map +1 -1
  91. package/dist/helpers/options.js +25 -1
  92. package/dist/helpers/options.js.map +1 -1
  93. package/dist/helpers/providerValue.d.ts +68 -0
  94. package/dist/helpers/providerValue.d.ts.map +1 -0
  95. package/dist/helpers/providerValue.js +50 -0
  96. package/dist/helpers/providerValue.js.map +1 -0
  97. package/dist/helpers/singleLineString.d.ts +37 -0
  98. package/dist/helpers/singleLineString.d.ts.map +1 -0
  99. package/dist/helpers/singleLineString.js +66 -0
  100. package/dist/helpers/singleLineString.js.map +1 -0
  101. package/dist/helpers/templates.d.ts +100 -0
  102. package/dist/helpers/templates.d.ts.map +1 -1
  103. package/dist/helpers/templates.js +237 -4
  104. package/dist/helpers/templates.js.map +1 -1
  105. package/dist/helpers/validations.d.ts +103 -2
  106. package/dist/helpers/validations.d.ts.map +1 -1
  107. package/dist/helpers/validations.js +858 -91
  108. package/dist/helpers/validations.js.map +1 -1
  109. package/dist/index.d.ts +1 -1
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +2 -1
  112. package/dist/index.js.map +1 -1
  113. package/package.json +1 -1
  114. package/src/components/composer/Composer.tsx +7 -3
  115. package/src/components/form/engine/CompactRow.tsx +391 -29
  116. package/src/components/form/engine/CompactToolbar.tsx +105 -98
  117. package/src/components/form/engine/FormEngine.stories.tsx +863 -12
  118. package/src/components/form/engine/FormEngine.tsx +183 -33
  119. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +123 -0
  120. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +232 -159
  121. package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
  122. package/src/components/form/engine/compactRowStyles.ts +38 -1
  123. package/src/components/form/engine/compactToolbarContext.ts +8 -0
  124. package/src/components/form/engine/readFirst.ts +166 -5
  125. package/src/components/form/fields/Field.tsx +3 -1
  126. package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
  127. package/src/components/form/fields/array/ArrayAuto.tsx +23 -4
  128. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +134 -0
  129. package/src/components/form/fields/array/ArrayAutoField.tsx +22 -4
  130. package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
  131. package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
  132. package/src/components/form/fields/long-string/LongString.tsx +46 -5
  133. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
  134. package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
  135. package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
  136. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
  137. package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
  138. package/src/components/form/fields/template/TemplateField.tsx +47 -8
  139. package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
  140. package/src/components/form/index.tsx +1 -0
  141. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
  142. package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
  143. package/src/components/ticketThread/TicketThread.tsx +7 -0
  144. package/src/helpers/common.ts +11 -1
  145. package/src/helpers/optionUiTypes.ts +153 -0
  146. package/src/helpers/options.ts +23 -0
  147. package/src/helpers/providerValue.ts +125 -0
  148. package/src/helpers/singleLineString.ts +61 -0
  149. package/src/helpers/templates.ts +272 -3
  150. package/src/helpers/validations.ts +1177 -109
  151. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
  152. package/src/index.tsx +1 -0
@@ -1,4 +1,10 @@
1
- import { ReqoreCollapsibleContent, ReqoreIcon, ReqoreP, ReqoreTag } from '@qoretechnologies/reqore';
1
+ import {
2
+ ReqoreCollapsibleContent,
3
+ ReqoreIcon,
4
+ ReqoreLink,
5
+ ReqoreP,
6
+ ReqoreTag,
7
+ } from '@qoretechnologies/reqore';
2
8
  import {
3
9
  IQorusFormField,
4
10
  IQorusFormSchema,
@@ -8,9 +14,23 @@ import React from 'react';
8
14
  import styled from 'styled-components';
9
15
  import { useContextSelector } from 'use-context-selector';
10
16
  import { CompactRowContext } from '../compactRowContext';
11
- import { MONO_FONT_STACK, StyledCodePreview, StyledRowLabel, StyledRowValue } from '../compactRowStyles';
12
- import { findAllowedValueOption, formatOptionValue, getAllowedValueImage } from '../readFirst';
13
- import { isUiEncodedValue } from './structuredData';
17
+ import {
18
+ MONO_FONT_STACK,
19
+ StyledCodePreview,
20
+ StyledRowLabel,
21
+ StyledRowValue,
22
+ } from '../compactRowStyles';
23
+ import {
24
+ fieldLabel,
25
+ findAllowedValueOption,
26
+ formatOptionValue,
27
+ getAllowedValueImage,
28
+ isCodeField,
29
+ orderedKeys,
30
+ recordIdentity,
31
+ titleKeyFor,
32
+ } from '../readFirst';
33
+ import { isEmptyUiEnvelope, isUiEncodedValue } from './structuredData';
14
34
 
15
35
  /**
16
36
  * A read-only rendering of a value THROUGH the schema that describes it.
@@ -26,10 +46,11 @@ import { isUiEncodedValue } from './structuredData';
26
46
  * So a described value is rendered the way the form asked for it, in the form's
27
47
  * own clothes:
28
48
  *
29
- * - Field names use `StyledRowLabel` and values use `StyledRowValue` — the very
30
- * components the option rows above are built from. Not a copy of their CSS: a
31
- * copy is a second definition of "what a form label looks like", and the two
32
- * drift the first time either is touched.
49
+ * - Field names use `StyledFieldLabel` (a `StyledRowLabel` in the read-only
50
+ * eyebrow treatment) and values use `StyledRowValue` — the very components the
51
+ * option rows above are built from. Not a copy of their CSS: a copy is a
52
+ * second definition of "what a form label looks like", and the two drift the
53
+ * first time either is touched.
33
54
  * - Values are formatted by the same `formatOptionValue` the rows use, so an
34
55
  * `allowed_values` entry resolves to its display name, a bool to Yes/No, a
35
56
  * date to the app's format. The same words appear in the summary, the preview
@@ -129,10 +150,38 @@ const StyledItemTitle = styled.div<{ $color: string; $mono: boolean; $accent: st
129
150
  ${({ $mono }) => ($mono ? `font-family: ${MONO_FONT_STACK}; font-size: 15px;` : '')}
130
151
  `;
131
152
 
132
- /** The rule tying an item's fields to its number. */
133
- const StyledItemFields = styled.div<{ $border: string }>`
134
- border-left: 1px solid ${({ $border }) => $border};
135
- padding-left: 10px;
153
+ /**
154
+ * Does this record have anything to show BELOW its heading?
155
+ *
156
+ * The heading consumes the identifying field, so a record holding only that has
157
+ * an empty body — and a rule drawn down an empty body is a line to nowhere.
158
+ */
159
+ const hasContentUnderHeading = (record: unknown, schema?: IQorusFormSchema): boolean => {
160
+ if (!isRecord(record)) {
161
+ return true;
162
+ }
163
+ const titleKey = titleKeyFor(record, schema);
164
+
165
+ return Object.entries(record).some(
166
+ ([key, value]) => key !== titleKey && value !== undefined && value !== null && value !== ''
167
+ );
168
+ };
169
+
170
+ /** The rule tying an item's fields to its number.
171
+ *
172
+ * Same 3px as the heading's accent bar directly above it: at 1px the two read as
173
+ * two different devices stacked on each other rather than one rule running down
174
+ * the item, and the padding drops to 8px so the text stays on the same column.
175
+ *
176
+ * The rule is drawn only when something is actually under the heading. The
177
+ * heading is pulled left of the rule by its own negative margin, so an item
178
+ * whose record holds nothing but its identity showed a 1-2px sliver of line and
179
+ * nothing else — which reads as a rendering artefact, because that is what it
180
+ * was. It keeps a transparent border in that case so the column does not shift.
181
+ */
182
+ const StyledItemFields = styled.div<{ $border: string; $rule: boolean }>`
183
+ border-left: 3px solid ${({ $border, $rule }) => ($rule ? $border : 'transparent')};
184
+ padding-left: 8px;
136
185
  min-width: 0;
137
186
  `;
138
187
 
@@ -144,6 +193,30 @@ const StyledItemFields = styled.div<{ $border: string }>`
144
193
  * and leave the items ragged against each other, which reads as a rendering
145
194
  * fault rather than as a list.
146
195
  */
196
+ /**
197
+ * A record field's name, in the detail-page vocabulary.
198
+ *
199
+ * Uppercase, tracked and dimmed, so a field name reads as furniture and the
200
+ * VALUE beside it is what the eye lands on. Same treatment the read-only
201
+ * interface panels use, which is the point: an in-editor list of records and a
202
+ * detail-page panel showing the same shape should not look like two components.
203
+ *
204
+ * A variant rather than a change to `StyledRowLabel`: that one is the FORM's own
205
+ * option-row label, and the form's fields are not read-only furniture — they are
206
+ * the things you are about to edit. Composing it keeps the colour, weight base
207
+ * and wrapping behaviour in one place instead of copying them.
208
+ *
209
+ * Applies to the label only. A VALUE is never uppercased: values are frequently
210
+ * case-sensitive — a service method is `onOrderStatus`, not `ONORDERSTATUS` —
211
+ * and the item heading is a value.
212
+ */
213
+ const StyledFieldLabel = styled(StyledRowLabel)`
214
+ font-size: 12px;
215
+ text-transform: uppercase;
216
+ letter-spacing: 0.06em;
217
+ opacity: 0.55;
218
+ `;
219
+
147
220
  const StyledFields = styled.div`
148
221
  /* The supporting fields step in under the heading, so the heading keeps the
149
222
  left edge to itself. This is the half that answers "the name is written in
@@ -224,59 +297,6 @@ const unwrap = (item: unknown): unknown => (isUiEncodedValue(item) ? item.value
224
297
  const isRecord = (value: unknown): value is Record<string, unknown> =>
225
298
  !!value && typeof value === 'object' && !Array.isArray(value);
226
299
 
227
- /** Whether a value is worth a row of its own. `false` and `0` are values. */
228
- const isSet = (value: unknown): boolean =>
229
- value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && !value.length);
230
-
231
- /**
232
- * The keys to render, in the order to render them: the schema's own order first
233
- * (that is the order the form puts the fields in), then anything stored that the
234
- * schema does not mention, so undescribed data is shown rather than dropped.
235
- */
236
- const orderedKeys = (record: Record<string, unknown>, schema: IQorusFormSchema): string[] => {
237
- const described = Object.keys(schema).filter((key) => isSet(unwrap(record[key])));
238
- const extra = Object.keys(record).filter((key) => !(key in schema) && isSet(unwrap(record[key])));
239
- return [...described, ...extra];
240
- };
241
-
242
- /** A field whose content is source code. `ui_type` is what the form renders by,
243
- * so it is what the preview reads by too — the storage type is just `string`. */
244
- const isCodeField = (fieldSchema: TQorusFormFieldSchema | undefined): boolean => {
245
- const uiType = (fieldSchema as { ui_type?: string } | undefined)?.ui_type;
246
- return uiType === 'code-editor';
247
- };
248
-
249
- /**
250
- * The field whose value heads the item — the first DECLARED one holding a plain
251
- * scalar.
252
- *
253
- * Schema order, not value order: the first declared field is the one the form
254
- * puts at the top of an item, which is the one that says which item it is. A
255
- * code body or a nested level is skipped, not because it is unimportant but
256
- * because it has no one-line form — it belongs in the rows below where it can
257
- * actually be read.
258
- *
259
- * One definition, used both to RENDER the heading and to omit that field from
260
- * the rows. Computing it twice is how the heading and the rows start disagreeing
261
- * about which field was promoted, and the item shows its name twice or not at
262
- * all.
263
- */
264
- const titleKeyFor = (
265
- record: Record<string, unknown>,
266
- schema: IQorusFormSchema
267
- ): string | undefined =>
268
- orderedKeys(record, schema).find((key) => {
269
- const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
270
- if (isCodeField(fieldSchema)) {
271
- return false;
272
- }
273
- const raw = unwrap(record[key]);
274
- return typeof raw === 'string' || typeof raw === 'number';
275
- });
276
-
277
- const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined): string =>
278
- (fieldSchema as { display_name?: string } | undefined)?.display_name || key;
279
-
280
300
  /**
281
301
  * The width of the shared label column, measured from the longest name that will
282
302
  * actually be rendered.
@@ -286,6 +306,12 @@ const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined)
286
306
  * the `%` ceiling stops one long name from taking the whole row when the inset
287
307
  * is narrow — a phone, or a deeply nested level.
288
308
  */
309
+ /** Approximate rendered width of one label character, in px. See `labelColumn`. */
310
+ const LABEL_CHAR_PX = 9.5;
311
+
312
+ /** Gap to the value, so the longest label is not flush against it. */
313
+ const LABEL_SLACK_PX = 14;
314
+
289
315
  const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
290
316
  const inner = unwrap(value);
291
317
  const records = (Array.isArray(inner) ? inner.map(unwrap) : [inner]).filter(isRecord);
@@ -297,9 +323,26 @@ const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
297
323
  );
298
324
  return Math.max(widest, own);
299
325
  }, 0);
300
- return longest ? `min(${longest + 1}ch, 45%)` : '140px';
326
+ // px, not `ch`. `ch` is the base font's `0` advance and knows nothing about
327
+ // the label's own treatment — uppercase, semibold, `letter-spacing: 0.06em`,
328
+ // at 12px — so it under-measures by about a tenth. With `overflow-wrap:
329
+ // anywhere` on the label, an under-measured column does not ellipsise, it
330
+ // BREAKS THE NAME IN HALF: `LEGACY_FLAG` came out over two lines the moment
331
+ // the labels became uppercase. Erring high costs a few px of gap; erring low
332
+ // costs a broken word.
333
+ return longest ? `min(${Math.ceil(longest * LABEL_CHAR_PX + LABEL_SLACK_PX)}px, 45%)` : '140px';
301
334
  };
302
335
 
336
+ /**
337
+ * `true` when a value is an address a browser can open.
338
+ *
339
+ * Deliberately per VALUE, not per field: a field named `url` frequently holds
340
+ * something that is not one — a bare path, a regex, a template — and offering
341
+ * those a link promises a destination there is not.
342
+ */
343
+ const isNavigableUrl = (value: unknown): value is string =>
344
+ typeof value === 'string' && /^(https?|wss?|ftps?):\/\//i.test(value);
345
+
303
346
  /** One field's value, rendered the way its row above renders it. */
304
347
  const FieldValue = ({
305
348
  field,
@@ -345,7 +388,22 @@ const FieldValue = ({
345
388
  style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
346
389
  />
347
390
  : null}
348
- {formatted}
391
+ {isNavigableUrl(field.value) ?
392
+ // The address IS the link — it used to render as inert text with a
393
+ // separate icon button beside it to open it, which is two things
394
+ // where there is one. ReqoreLink renders a real anchor, so
395
+ // middle-click and "copy link address" still work;
396
+ // `stopPropagation` because the row around it toggles.
397
+ <ReqoreLink
398
+ className='schema-view-open-url'
399
+ href={field.value}
400
+ target='_blank'
401
+ rel='noreferrer noopener'
402
+ onClick={(event: React.MouseEvent) => event.stopPropagation()}
403
+ >
404
+ {formatted}
405
+ </ReqoreLink>
406
+ : formatted}
349
407
  </span>
350
408
  }
351
409
  </StyledRowValue>
@@ -370,92 +428,109 @@ const SchemaRecord = ({
370
428
  // The SAME host renderer the row above uses for a `code-editor` field. Read
371
429
  // from context rather than threaded through props: a nested level is arbitrarily
372
430
  // deep, and the renderer is a property of the form, not of any one level.
373
- const codePreviewRenderer = useContextSelector(
374
- CompactRowContext,
375
- (v) => v.codePreviewRenderer
376
- );
431
+ const codePreviewRenderer = useContextSelector(CompactRowContext, (v) => v.codePreviewRenderer);
377
432
  const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
378
433
 
379
434
  return (
380
- <StyledFields className='schema-view-fields'>
381
- {orderedKeys(record, schema)
382
- .filter((key) => key !== skipKey)
383
- .map((key) => {
384
- const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
385
- const nested = (fieldSchema as { arg_schema?: IQorusFormSchema } | undefined)?.arg_schema;
386
- const value = unwrap(record[key]);
387
- const label = fieldLabel(key, fieldSchema);
388
- const labelWithType =
389
- showTypes && fieldSchema?.type ? `${label} · ${fieldSchema.type}` : label;
390
-
391
- // A field that describes its own level renders AS that level, indented
392
- // under its name — the same decision, one step down.
393
- if (nested && (isRecord(value) || Array.isArray(value))) {
394
- return (
395
- <React.Fragment key={key}>
396
- <StyledNestedLabel>
397
- <StyledRowLabel $color={colors.key}>{labelWithType}</StyledRowLabel>
398
- </StyledNestedLabel>
399
- <StyledNested $border={colors.border}>
400
- <SchemaLevel value={value} schema={nested} showTypes={showTypes} colors={colors} />
401
- </StyledNested>
402
- </React.Fragment>
403
- );
404
- }
405
-
406
- // A code value is source, and source is read as source: the host's renderer
407
- // gives it the syntax highlighting (and read-only LSP hover) it has in the
408
- // Source Code field, and "Show more" keeps a long body from taking over the
409
- // preview. Flattening it to one mono line — which is what a plain value cell
410
- // does — throws all of that away for a field whose whole content is code.
411
- if (isCodeField(fieldSchema) && typeof value === 'string') {
412
- return (
413
- <React.Fragment key={key}>
414
- <StyledNestedLabel>
415
- <StyledRowLabel $color={colors.key}>{labelWithType}</StyledRowLabel>
416
- </StyledNestedLabel>
417
- <StyledCodeCell onClick={(event) => event.stopPropagation()}>
418
- <ReqoreCollapsibleContent
419
- maxCollapsedHeight={96}
420
- buttonProps={{ className: 'options-readfirst-viewmore' }}
421
- >
422
- {codePreviewRenderer ?
423
- codePreviewRenderer({
424
- value,
425
- name: key,
426
- schema: fieldSchema,
427
- options: schema,
428
- values: record as never,
429
- })
430
- : <StyledCodePreview
431
- className='options-readfirst-code'
432
- $bg={cHover}
433
- $border={`${colors.border}88`}
434
- $fg={colors.key}
435
+ <StyledFields className='schema-view-fields'>
436
+ {orderedKeys(record, schema)
437
+ .filter((key) => key !== skipKey)
438
+ .map((key) => {
439
+ const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
440
+ const nested = (fieldSchema as { arg_schema?: IQorusFormSchema } | undefined)?.arg_schema;
441
+ // A schema-declared field the form materialised but never filled in
442
+ // arrives as `{type: 'int'}` with no `value` key. `unwrap` cannot see
443
+ // that — `isUiEncodedValue` requires a `value` — so it fell through as
444
+ // CONTENT and was wrapped as one below, and `formatOptionValue` counted
445
+ // the envelope's own `type` key: an untouched Runtime Defaults read
446
+ // "Timeout (Seconds) 1 field" three times over while the sub-form it
447
+ // previews correctly said 0/3 set. `fieldSchema` is what licenses the
448
+ // reading: the schema declares this field, so a `{type}`-only object
449
+ // under it is an empty envelope, never somebody's data.
450
+ const raw = record[key];
451
+ // `!nested` is the discriminator, and it is not a detail: a field that
452
+ // declares its OWN level describes what is inside it, so `{type: 'default'}`
453
+ // there is content — the scheme's Scheme Type, reading "Default RBAC" —
454
+ // not an envelope. Only a leaf can be empty in this sense.
455
+ const value =
456
+ fieldSchema && !nested && isEmptyUiEnvelope(raw) ? undefined : unwrap(raw);
457
+ const label = fieldLabel(key, fieldSchema);
458
+ const labelWithType =
459
+ showTypes && fieldSchema?.type ? `${label} · ${fieldSchema.type}` : label;
460
+
461
+ // A field that describes its own level renders AS that level, indented
462
+ // under its name — the same decision, one step down.
463
+ if (nested && (isRecord(value) || Array.isArray(value))) {
464
+ return (
465
+ <React.Fragment key={key}>
466
+ <StyledNestedLabel>
467
+ <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
468
+ </StyledNestedLabel>
469
+ <StyledNested $border={colors.border}>
470
+ <SchemaLevel
471
+ value={value}
472
+ schema={nested}
473
+ showTypes={showTypes}
474
+ colors={colors}
475
+ />
476
+ </StyledNested>
477
+ </React.Fragment>
478
+ );
479
+ }
480
+
481
+ // A code value is source, and source is read as source: the host's renderer
482
+ // gives it the syntax highlighting (and read-only LSP hover) it has in the
483
+ // Source Code field, and "Show more" keeps a long body from taking over the
484
+ // preview. Flattening it to one mono line — which is what a plain value cell
485
+ // does — throws all of that away for a field whose whole content is code.
486
+ if (isCodeField(fieldSchema) && typeof value === 'string') {
487
+ return (
488
+ <React.Fragment key={key}>
489
+ <StyledNestedLabel>
490
+ <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
491
+ </StyledNestedLabel>
492
+ <StyledCodeCell onClick={(event) => event.stopPropagation()}>
493
+ <ReqoreCollapsibleContent
494
+ maxCollapsedHeight={96}
495
+ buttonProps={{ className: 'options-readfirst-viewmore' }}
435
496
  >
436
- {value}
437
- </StyledCodePreview>
438
- }
439
- </ReqoreCollapsibleContent>
440
- </StyledCodeCell>
441
- </React.Fragment>
442
- );
443
- }
497
+ {codePreviewRenderer ?
498
+ codePreviewRenderer({
499
+ value,
500
+ name: key,
501
+ schema: fieldSchema,
502
+ options: schema,
503
+ values: record as never,
504
+ })
505
+ : <StyledCodePreview
506
+ className='options-readfirst-code'
507
+ $bg={cHover}
508
+ $border={`${colors.border}88`}
509
+ $fg={colors.key}
510
+ >
511
+ {value}
512
+ </StyledCodePreview>
513
+ }
514
+ </ReqoreCollapsibleContent>
515
+ </StyledCodeCell>
516
+ </React.Fragment>
517
+ );
518
+ }
444
519
 
445
- return (
446
- <React.Fragment key={key}>
447
- <StyledRowLabel $color={colors.key} title={label}>
448
- {labelWithType}
449
- </StyledRowLabel>
450
- <FieldValue
451
- field={{ type: fieldSchema?.type, value } as IQorusFormField}
452
- fieldSchema={fieldSchema}
453
- color={colors.key}
454
- />
455
- </React.Fragment>
456
- );
457
- })}
458
- </StyledFields>
520
+ return (
521
+ <React.Fragment key={key}>
522
+ <StyledFieldLabel $color={colors.key} title={label}>
523
+ {labelWithType}
524
+ </StyledFieldLabel>
525
+ <FieldValue
526
+ field={{ type: fieldSchema?.type, value } as IQorusFormField}
527
+ fieldSchema={fieldSchema}
528
+ color={colors.key}
529
+ />
530
+ </React.Fragment>
531
+ );
532
+ })}
533
+ </StyledFields>
459
534
  );
460
535
  };
461
536
 
@@ -481,16 +556,17 @@ const SchemaLevel = ({
481
556
  return (
482
557
  <StyledItem key={index} className='schema-view-item'>
483
558
  <StyledMarker $color={colors.muted}>{index + 1}.</StyledMarker>
484
- <StyledItemFields $border={colors.border}>
559
+ <StyledItemFields
560
+ $border={colors.border}
561
+ $rule={hasContentUnderHeading(record, schema)}
562
+ >
485
563
  {isRecord(record) ?
486
564
  <>
487
565
  {(() => {
488
- const titleKey = titleKeyFor(record, schema);
489
- if (!titleKey) {
566
+ const identity = recordIdentity(record, schema);
567
+ if (!identity) {
490
568
  return null;
491
569
  }
492
- const fieldSchema = schema[titleKey] as TQorusFormFieldSchema | undefined;
493
- const raw = unwrap(record[titleKey]);
494
570
  return (
495
571
  <StyledItemTitle
496
572
  className='schema-view-item-title'
@@ -498,14 +574,11 @@ const SchemaLevel = ({
498
574
  // A literal keeps the mono treatment it has as a value —
499
575
  // promoting it must not change what it IS. A chosen
500
576
  // label ("Default RBAC") is prose and stays prose.
501
- $mono={!findAllowedValueOption(raw, fieldSchema)}
577
+ $mono={identity.mono}
502
578
  $accent={colors.accent}
503
- title={fieldLabel(titleKey, fieldSchema)}
579
+ title={identity.label}
504
580
  >
505
- {formatOptionValue(
506
- { type: fieldSchema?.type, value: raw } as IQorusFormField,
507
- fieldSchema
508
- )}
581
+ {identity.text}
509
582
  </StyledItemTitle>
510
583
  );
511
584
  })()}
@@ -52,6 +52,26 @@ export const isRecord = (value: unknown): value is Record<string, unknown> =>
52
52
  export const isStructuredContainerValue = (value: unknown): boolean =>
53
53
  Array.isArray(value) || isRecord(value);
54
54
 
55
+ /**
56
+ * A field the form materialised but left UNSET: `{type: 'int'}`, no `value` key.
57
+ *
58
+ * `fixOptions` omits the key entirely when the value is undefined, so this is
59
+ * what an untouched sub-field of a schema-declared hash looks like on the wire.
60
+ * It is an EMPTY ENVELOPE, never content — and every renderer has to agree on
61
+ * that, because the ones that do not count the envelope's own `type` key and
62
+ * report an unset field as "1 field" while the sub-form correctly says 0 set.
63
+ *
64
+ * Deliberately separate from {@link isUiEncodedValue}, which requires a `value`
65
+ * and therefore cannot see this shape. Callers gate on knowing the field is
66
+ * schema-declared: without a schema saying so, a `{type: …}` object could
67
+ * legitimately be somebody's data.
68
+ */
69
+ export const isEmptyUiEnvelope = (value: unknown): boolean =>
70
+ isRecord(value) &&
71
+ typeof value.type === 'string' &&
72
+ !('value' in value) &&
73
+ Object.keys(value).every((key) => UI_ENVELOPE_KEYS.has(key));
74
+
55
75
  export const isUiEncodedValue = (value: unknown): value is IUiEncodedValue => {
56
76
  if (!isRecord(value) || typeof value.type !== 'string' || !('value' in value)) {
57
77
  return false;
@@ -61,6 +61,9 @@ export const PANEL_LEFT_CSS = `calc(${LABEL_COL} + ${COMPACT_ROW_PAD_X + COMPACT
61
61
  export const StyledCompactPanel = styled(ReqorePanel)<{
62
62
  $headerBg: string;
63
63
  $nested?: boolean;
64
+ /** True when the toolbar renders no search row (hidden via `compactToolbar`,
65
+ * self-hidden on a single-option form, or no toolbar at all). */
66
+ $tightHeader?: boolean;
64
67
  }>`
65
68
  > .reqore-panel-title {
66
69
  /* The blur + translateZ exist only to make the STICKY top-level toolbar ghost
@@ -71,7 +74,17 @@ export const StyledCompactPanel = styled(ReqorePanel)<{
71
74
  'backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); transform: translateZ(0);'
72
75
  )}
73
76
  padding-top: ${GAP_FROM_SIZE[HEADER_GAP]}px;
74
- padding-bottom: ${GAP_FROM_SIZE[HEADER_GAP]}px;
77
+ /* A header with a SEARCH ROW keeps the \`big\` gap below it — a tall
78
+ interactive control earns separation from the content it filters. A
79
+ header without one (search hidden via \`compactToolbar\`, self-hidden on
80
+ a single-option form, or no toolbar at all) is a thin strip, and at
81
+ \`big\` its padding stacked onto the panel content's own 8px into a 26px
82
+ dead band between the completion meter and the first status box —
83
+ rejected on build #140 as "unnecessarily big". \`normal\` lands it at
84
+ the boxes' own 10px rhythm, and scoping the change here keeps every
85
+ search-bearing form pixel-identical. */
86
+ padding-bottom: ${({ $tightHeader }) =>
87
+ $tightHeader ? GAP_FROM_SIZE.normal : GAP_FROM_SIZE[HEADER_GAP]}px;
75
88
  }
76
89
 
77
90
  /* Group framing. The HORIZONTAL rule is a real element in the header
@@ -690,3 +703,27 @@ export const StyledGroupBody = styled.div<{
690
703
  padding-bottom: 0;
691
704
  }
692
705
  `;
706
+
707
+ // One absorbed sibling, rendered inside its host row's container rather than
708
+ // on a row of its own (see `absorb_fields`). The pair this exists for is a code
709
+ // editor and its language: reading them as two unrelated rows one above the
710
+ // other said they were separate decisions, and they are not.
711
+ //
712
+ // Laid out as label-then-control on one line so it reads as a property of the
713
+ // element it now sits in, and separated from the editor below it by a hairline
714
+ // rather than a gap — the two share one container, so the seam has to look like
715
+ // a division inside it and not like two stacked things.
716
+ export const StyledAbsorbedField = styled.div`
717
+ display: flex;
718
+ align-items: center;
719
+ gap: 10px;
720
+ min-width: 0;
721
+ padding-bottom: 8px;
722
+ margin-bottom: 8px;
723
+ border-bottom: 1px solid ${({ theme }) => `${theme.main}22`};
724
+ `;
725
+
726
+ export const StyledAbsorbedLabel = styled(ReqoreP)`
727
+ flex: 0 0 auto;
728
+ margin: 0;
729
+ `;
@@ -24,6 +24,14 @@ export interface ICompactToolbarOptionalField {
24
24
  */
25
25
  export interface ICompactToolbarContext {
26
26
  readOnly?: boolean;
27
+ /** Which parts to render, already resolved from the engine's `compactToolbar`
28
+ * prop — every key present, no undefined to re-default here. */
29
+ parts: {
30
+ completion: boolean;
31
+ search: boolean;
32
+ fields: boolean;
33
+ help: boolean;
34
+ };
27
35
  // Completion meter
28
36
  invalidCount: number;
29
37
  attentionCount: number;