@qoretechnologies/reqraft 0.10.44-pr.95.gb67bfb7 → 0.10.44

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 (144) 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 +203 -98
  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 +108 -20
  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 +24 -0
  33. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  34. package/dist/components/form/engine/readFirst.js +84 -6
  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/auto/AutoFormField.d.ts.map +1 -1
  44. package/dist/components/form/fields/auto/AutoFormField.js +64 -14
  45. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  46. package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
  47. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  48. package/dist/components/form/fields/long-string/LongString.js +25 -4
  49. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  50. package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
  51. package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
  52. package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
  53. package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
  54. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  55. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
  56. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  57. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  58. package/dist/components/form/fields/template/TemplateField.js +35 -6
  59. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  60. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
  61. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
  62. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
  63. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
  64. package/dist/components/form/index.d.ts +1 -0
  65. package/dist/components/form/index.d.ts.map +1 -1
  66. package/dist/components/form/index.js +1 -0
  67. package/dist/components/form/index.js.map +1 -1
  68. package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
  69. package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
  70. package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
  71. package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
  72. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  73. package/dist/components/ticketThread/TicketThread.js +1 -1
  74. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  75. package/dist/helpers/common.d.ts +8 -1
  76. package/dist/helpers/common.d.ts.map +1 -1
  77. package/dist/helpers/common.js +9 -1
  78. package/dist/helpers/common.js.map +1 -1
  79. package/dist/helpers/optionUiTypes.d.ts +30 -0
  80. package/dist/helpers/optionUiTypes.d.ts.map +1 -0
  81. package/dist/helpers/optionUiTypes.js +148 -0
  82. package/dist/helpers/optionUiTypes.js.map +1 -0
  83. package/dist/helpers/options.d.ts +19 -0
  84. package/dist/helpers/options.d.ts.map +1 -1
  85. package/dist/helpers/options.js +25 -1
  86. package/dist/helpers/options.js.map +1 -1
  87. package/dist/helpers/providerValue.d.ts +68 -0
  88. package/dist/helpers/providerValue.d.ts.map +1 -0
  89. package/dist/helpers/providerValue.js +50 -0
  90. package/dist/helpers/providerValue.js.map +1 -0
  91. package/dist/helpers/singleLineString.d.ts +37 -0
  92. package/dist/helpers/singleLineString.d.ts.map +1 -0
  93. package/dist/helpers/singleLineString.js +66 -0
  94. package/dist/helpers/singleLineString.js.map +1 -0
  95. package/dist/helpers/templates.d.ts +100 -0
  96. package/dist/helpers/templates.d.ts.map +1 -1
  97. package/dist/helpers/templates.js +237 -4
  98. package/dist/helpers/templates.js.map +1 -1
  99. package/dist/helpers/validations.d.ts +103 -2
  100. package/dist/helpers/validations.d.ts.map +1 -1
  101. package/dist/helpers/validations.js +858 -91
  102. package/dist/helpers/validations.js.map +1 -1
  103. package/dist/index.d.ts +1 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +2 -1
  106. package/dist/index.js.map +1 -1
  107. package/package.json +1 -1
  108. package/src/components/composer/Composer.tsx +7 -3
  109. package/src/components/form/engine/CompactRow.tsx +391 -29
  110. package/src/components/form/engine/CompactToolbar.tsx +105 -98
  111. package/src/components/form/engine/FormEngine.stories.tsx +863 -12
  112. package/src/components/form/engine/FormEngine.tsx +231 -68
  113. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +123 -0
  114. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +217 -95
  115. package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
  116. package/src/components/form/engine/compactRowStyles.ts +38 -1
  117. package/src/components/form/engine/compactToolbarContext.ts +8 -0
  118. package/src/components/form/engine/readFirst.ts +125 -35
  119. package/src/components/form/fields/Field.tsx +3 -1
  120. package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
  121. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +81 -0
  122. package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
  123. package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
  124. package/src/components/form/fields/long-string/LongString.tsx +46 -5
  125. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
  126. package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
  127. package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
  128. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
  129. package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
  130. package/src/components/form/fields/template/TemplateField.tsx +47 -8
  131. package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
  132. package/src/components/form/index.tsx +1 -0
  133. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
  134. package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
  135. package/src/components/ticketThread/TicketThread.tsx +7 -0
  136. package/src/helpers/common.ts +11 -1
  137. package/src/helpers/optionUiTypes.ts +153 -0
  138. package/src/helpers/options.ts +23 -0
  139. package/src/helpers/providerValue.ts +125 -0
  140. package/src/helpers/singleLineString.ts +61 -0
  141. package/src/helpers/templates.ts +272 -3
  142. package/src/helpers/validations.ts +1177 -109
  143. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
  144. 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,7 +14,12 @@ 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';
17
+ import {
18
+ MONO_FONT_STACK,
19
+ StyledCodePreview,
20
+ StyledRowLabel,
21
+ StyledRowValue,
22
+ } from '../compactRowStyles';
12
23
  import {
13
24
  fieldLabel,
14
25
  findAllowedValueOption,
@@ -19,7 +30,7 @@ import {
19
30
  recordIdentity,
20
31
  titleKeyFor,
21
32
  } from '../readFirst';
22
- import { isUiEncodedValue } from './structuredData';
33
+ import { isEmptyUiEnvelope, isUiEncodedValue } from './structuredData';
23
34
 
24
35
  /**
25
36
  * A read-only rendering of a value THROUGH the schema that describes it.
@@ -35,10 +46,11 @@ import { isUiEncodedValue } from './structuredData';
35
46
  * So a described value is rendered the way the form asked for it, in the form's
36
47
  * own clothes:
37
48
  *
38
- * - Field names use `StyledRowLabel` and values use `StyledRowValue` — the very
39
- * components the option rows above are built from. Not a copy of their CSS: a
40
- * copy is a second definition of "what a form label looks like", and the two
41
- * 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.
42
54
  * - Values are formatted by the same `formatOptionValue` the rows use, so an
43
55
  * `allowed_values` entry resolves to its display name, a bool to Yes/No, a
44
56
  * date to the app's format. The same words appear in the summary, the preview
@@ -138,10 +150,38 @@ const StyledItemTitle = styled.div<{ $color: string; $mono: boolean; $accent: st
138
150
  ${({ $mono }) => ($mono ? `font-family: ${MONO_FONT_STACK}; font-size: 15px;` : '')}
139
151
  `;
140
152
 
141
- /** The rule tying an item's fields to its number. */
142
- const StyledItemFields = styled.div<{ $border: string }>`
143
- border-left: 1px solid ${({ $border }) => $border};
144
- 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;
145
185
  min-width: 0;
146
186
  `;
147
187
 
@@ -153,6 +193,30 @@ const StyledItemFields = styled.div<{ $border: string }>`
153
193
  * and leave the items ragged against each other, which reads as a rendering
154
194
  * fault rather than as a list.
155
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
+
156
220
  const StyledFields = styled.div`
157
221
  /* The supporting fields step in under the heading, so the heading keeps the
158
222
  left edge to itself. This is the half that answers "the name is written in
@@ -242,6 +306,12 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
242
306
  * the `%` ceiling stops one long name from taking the whole row when the inset
243
307
  * is narrow — a phone, or a deeply nested level.
244
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
+
245
315
  const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
246
316
  const inner = unwrap(value);
247
317
  const records = (Array.isArray(inner) ? inner.map(unwrap) : [inner]).filter(isRecord);
@@ -253,9 +323,26 @@ const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
253
323
  );
254
324
  return Math.max(widest, own);
255
325
  }, 0);
256
- 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';
257
334
  };
258
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
+
259
346
  /** One field's value, rendered the way its row above renders it. */
260
347
  const FieldValue = ({
261
348
  field,
@@ -301,7 +388,22 @@ const FieldValue = ({
301
388
  style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
302
389
  />
303
390
  : null}
304
- {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}
305
407
  </span>
306
408
  }
307
409
  </StyledRowValue>
@@ -326,92 +428,109 @@ const SchemaRecord = ({
326
428
  // The SAME host renderer the row above uses for a `code-editor` field. Read
327
429
  // from context rather than threaded through props: a nested level is arbitrarily
328
430
  // deep, and the renderer is a property of the form, not of any one level.
329
- const codePreviewRenderer = useContextSelector(
330
- CompactRowContext,
331
- (v) => v.codePreviewRenderer
332
- );
431
+ const codePreviewRenderer = useContextSelector(CompactRowContext, (v) => v.codePreviewRenderer);
333
432
  const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
334
433
 
335
434
  return (
336
- <StyledFields className='schema-view-fields'>
337
- {orderedKeys(record, schema)
338
- .filter((key) => key !== skipKey)
339
- .map((key) => {
340
- const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
341
- const nested = (fieldSchema as { arg_schema?: IQorusFormSchema } | undefined)?.arg_schema;
342
- const value = unwrap(record[key]);
343
- const label = fieldLabel(key, fieldSchema);
344
- const labelWithType =
345
- showTypes && fieldSchema?.type ? `${label} · ${fieldSchema.type}` : label;
346
-
347
- // A field that describes its own level renders AS that level, indented
348
- // under its name — the same decision, one step down.
349
- if (nested && (isRecord(value) || Array.isArray(value))) {
350
- return (
351
- <React.Fragment key={key}>
352
- <StyledNestedLabel>
353
- <StyledRowLabel $color={colors.key}>{labelWithType}</StyledRowLabel>
354
- </StyledNestedLabel>
355
- <StyledNested $border={colors.border}>
356
- <SchemaLevel value={value} schema={nested} showTypes={showTypes} colors={colors} />
357
- </StyledNested>
358
- </React.Fragment>
359
- );
360
- }
361
-
362
- // A code value is source, and source is read as source: the host's renderer
363
- // gives it the syntax highlighting (and read-only LSP hover) it has in the
364
- // Source Code field, and "Show more" keeps a long body from taking over the
365
- // preview. Flattening it to one mono line — which is what a plain value cell
366
- // does — throws all of that away for a field whose whole content is code.
367
- if (isCodeField(fieldSchema) && typeof value === 'string') {
368
- return (
369
- <React.Fragment key={key}>
370
- <StyledNestedLabel>
371
- <StyledRowLabel $color={colors.key}>{labelWithType}</StyledRowLabel>
372
- </StyledNestedLabel>
373
- <StyledCodeCell onClick={(event) => event.stopPropagation()}>
374
- <ReqoreCollapsibleContent
375
- maxCollapsedHeight={96}
376
- buttonProps={{ className: 'options-readfirst-viewmore' }}
377
- >
378
- {codePreviewRenderer ?
379
- codePreviewRenderer({
380
- value,
381
- name: key,
382
- schema: fieldSchema,
383
- options: schema,
384
- values: record as never,
385
- })
386
- : <StyledCodePreview
387
- className='options-readfirst-code'
388
- $bg={cHover}
389
- $border={`${colors.border}88`}
390
- $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' }}
391
496
  >
392
- {value}
393
- </StyledCodePreview>
394
- }
395
- </ReqoreCollapsibleContent>
396
- </StyledCodeCell>
397
- </React.Fragment>
398
- );
399
- }
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
+ }
400
519
 
401
- return (
402
- <React.Fragment key={key}>
403
- <StyledRowLabel $color={colors.key} title={label}>
404
- {labelWithType}
405
- </StyledRowLabel>
406
- <FieldValue
407
- field={{ type: fieldSchema?.type, value } as IQorusFormField}
408
- fieldSchema={fieldSchema}
409
- color={colors.key}
410
- />
411
- </React.Fragment>
412
- );
413
- })}
414
- </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>
415
534
  );
416
535
  };
417
536
 
@@ -437,7 +556,10 @@ const SchemaLevel = ({
437
556
  return (
438
557
  <StyledItem key={index} className='schema-view-item'>
439
558
  <StyledMarker $color={colors.muted}>{index + 1}.</StyledMarker>
440
- <StyledItemFields $border={colors.border}>
559
+ <StyledItemFields
560
+ $border={colors.border}
561
+ $rule={hasContentUnderHeading(record, schema)}
562
+ >
441
563
  {isRecord(record) ?
442
564
  <>
443
565
  {(() => {
@@ -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;