@qoretechnologies/reqraft 0.10.39-pr.99.gdbafa34 → 0.10.39

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 (110) hide show
  1. package/.claude/CLAUDE.md +12 -0
  2. package/dist/components/codeSize/index.d.ts +37 -0
  3. package/dist/components/codeSize/index.d.ts.map +1 -0
  4. package/dist/components/codeSize/index.js +87 -0
  5. package/dist/components/codeSize/index.js.map +1 -0
  6. package/dist/components/form/engine/CompactRow.d.ts +20 -1
  7. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  8. package/dist/components/form/engine/CompactRow.js +365 -30
  9. package/dist/components/form/engine/CompactRow.js.map +1 -1
  10. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  11. package/dist/components/form/engine/CompactToolbar.js +6 -8
  12. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  13. package/dist/components/form/engine/FormEngine.d.ts +24 -45
  14. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  15. package/dist/components/form/engine/FormEngine.js +211 -84
  16. package/dist/components/form/engine/FormEngine.js.map +1 -1
  17. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts +37 -0
  18. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -0
  19. package/dist/components/form/engine/_structuredData/SchemaDataView.js +335 -0
  20. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -0
  21. package/dist/components/form/engine/compactRowStyles.d.ts +13 -0
  22. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  23. package/dist/components/form/engine/compactRowStyles.js +30 -5
  24. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  25. package/dist/components/form/engine/compactToolbarContext.d.ts +0 -8
  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/readFirst.d.ts +73 -1
  29. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  30. package/dist/components/form/engine/readFirst.js +175 -5
  31. package/dist/components/form/engine/readFirst.js.map +1 -1
  32. package/dist/components/form/fields/Field.d.ts.map +1 -1
  33. package/dist/components/form/fields/Field.js +1 -1
  34. package/dist/components/form/fields/Field.js.map +1 -1
  35. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
  36. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  37. package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
  38. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  39. package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
  40. package/dist/components/form/fields/array/ArrayAuto.js +51 -21
  41. package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
  42. package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
  43. package/dist/components/form/fields/array/ArrayAutoField.js +42 -25
  44. package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
  45. package/dist/components/form/fields/auto/AutoFormField.d.ts +4 -0
  46. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  47. package/dist/components/form/fields/auto/AutoFormField.js +29 -4
  48. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  49. package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
  50. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  51. package/dist/components/form/fields/long-string/LongString.js +25 -4
  52. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  53. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  54. package/dist/components/form/fields/template/TemplateField.js +12 -2
  55. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  56. package/dist/helpers/common.d.ts +8 -1
  57. package/dist/helpers/common.d.ts.map +1 -1
  58. package/dist/helpers/common.js +9 -1
  59. package/dist/helpers/common.js.map +1 -1
  60. package/dist/helpers/optionUiTypes.d.ts +30 -0
  61. package/dist/helpers/optionUiTypes.d.ts.map +1 -0
  62. package/dist/helpers/optionUiTypes.js +148 -0
  63. package/dist/helpers/optionUiTypes.js.map +1 -0
  64. package/dist/helpers/options.d.ts +19 -0
  65. package/dist/helpers/options.d.ts.map +1 -1
  66. package/dist/helpers/options.js +25 -1
  67. package/dist/helpers/options.js.map +1 -1
  68. package/dist/helpers/providerValue.d.ts +68 -0
  69. package/dist/helpers/providerValue.d.ts.map +1 -0
  70. package/dist/helpers/providerValue.js +50 -0
  71. package/dist/helpers/providerValue.js.map +1 -0
  72. package/dist/helpers/singleLineString.d.ts +37 -0
  73. package/dist/helpers/singleLineString.d.ts.map +1 -0
  74. package/dist/helpers/singleLineString.js +66 -0
  75. package/dist/helpers/singleLineString.js.map +1 -0
  76. package/dist/helpers/validations.d.ts +85 -2
  77. package/dist/helpers/validations.d.ts.map +1 -1
  78. package/dist/helpers/validations.js +802 -76
  79. package/dist/helpers/validations.js.map +1 -1
  80. package/dist/index.d.ts +1 -0
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +1 -0
  83. package/dist/index.js.map +1 -1
  84. package/package.json +1 -1
  85. package/src/components/codeSize/index.tsx +79 -0
  86. package/src/components/form/engine/CompactRow.tsx +450 -31
  87. package/src/components/form/engine/CompactToolbar.tsx +16 -21
  88. package/src/components/form/engine/FormEngine.stories.tsx +804 -17
  89. package/src/components/form/engine/FormEngine.tsx +207 -105
  90. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +233 -0
  91. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +630 -0
  92. package/src/components/form/engine/compactRowStyles.ts +44 -2
  93. package/src/components/form/engine/compactToolbarContext.ts +0 -8
  94. package/src/components/form/engine/readFirst.ts +201 -5
  95. package/src/components/form/fields/Field.tsx +3 -1
  96. package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
  97. package/src/components/form/fields/array/ArrayAuto.tsx +35 -4
  98. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +134 -0
  99. package/src/components/form/fields/array/ArrayAutoField.tsx +22 -4
  100. package/src/components/form/fields/auto/AutoFormField.tsx +30 -1
  101. package/src/components/form/fields/long-string/LongString.tsx +46 -5
  102. package/src/components/form/fields/template/TemplateField.tsx +16 -1
  103. package/src/helpers/common.ts +11 -1
  104. package/src/helpers/optionUiTypes.ts +153 -0
  105. package/src/helpers/options.ts +23 -0
  106. package/src/helpers/providerValue.ts +125 -0
  107. package/src/helpers/singleLineString.ts +61 -0
  108. package/src/helpers/validations.ts +1105 -80
  109. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
  110. package/src/index.tsx +1 -0
@@ -0,0 +1,630 @@
1
+ import {
2
+ ReqoreCollapsibleContent,
3
+ ReqoreIcon,
4
+ ReqoreLink,
5
+ ReqoreP,
6
+ ReqoreTag,
7
+ } from '@qoretechnologies/reqore';
8
+ import {
9
+ IQorusFormField,
10
+ IQorusFormSchema,
11
+ TQorusFormFieldSchema,
12
+ } from '@qoretechnologies/ts-toolkit';
13
+ import React from 'react';
14
+ import styled from 'styled-components';
15
+ import { useContextSelector } from 'use-context-selector';
16
+ import { CompactRowContext } from '../compactRowContext';
17
+ import { MONO_FONT_STACK, StyledCodePreview, StyledRowLabel, StyledRowValue } from '../compactRowStyles';
18
+ import {
19
+ fieldLabel,
20
+ findAllowedValueOption,
21
+ formatOptionValue,
22
+ getAllowedValueImage,
23
+ isCodeField,
24
+ orderedKeys,
25
+ recordIdentity,
26
+ titleKeyFor,
27
+ } from '../readFirst';
28
+ import { isUiEncodedValue } from './structuredData';
29
+
30
+ /**
31
+ * A read-only rendering of a value THROUGH the schema that describes it.
32
+ *
33
+ * `StructuredDataView` renders data nobody has described: it has to announce
34
+ * what it found ("Object · 2 fields"), print raw keys, and colour values by
35
+ * inferred type, because inference is all it has. That is the right rendering
36
+ * for untyped data and the wrong one for ours — a form field with an
37
+ * `arg_schema` is a value whose shape, names and choices are all known in
38
+ * advance, and showing it as a data tree asks the reader to decode a structure
39
+ * the form already knows how to say out loud.
40
+ *
41
+ * So a described value is rendered the way the form asked for it, in the form's
42
+ * own clothes:
43
+ *
44
+ * - Field names use `StyledFieldLabel` (a `StyledRowLabel` in the read-only
45
+ * eyebrow treatment) and values use `StyledRowValue` — the very components the
46
+ * option rows above are built from. Not a copy of their CSS: a copy is a
47
+ * second definition of "what a form label looks like", and the two drift the
48
+ * first time either is touched.
49
+ * - Values are formatted by the same `formatOptionValue` the rows use, so an
50
+ * `allowed_values` entry resolves to its display name, a bool to Yes/No, a
51
+ * date to the app's format. The same words appear in the summary, the preview
52
+ * and the editor.
53
+ * - Fields appear in the schema's order — the order the form puts them in.
54
+ *
55
+ * A list renders as numbered items: the number sits in a left margin with a rule
56
+ * running down beside the fields belonging to it, and the item's first field
57
+ * starts on the number's own line. The rule is what makes a list of multi-field
58
+ * items readable — without it, "which item does this field belong to?" is
59
+ * answered only by counting.
60
+ *
61
+ * Two deliberate properties:
62
+ *
63
+ * - **Nothing is hidden.** A key the schema does not describe is still shown,
64
+ * under its raw name, after the described ones. A preview that quietly dropped
65
+ * data would be worse than a raw tree, not better.
66
+ * - **It degrades, it does not fail.** With no schema for a level, the caller
67
+ * keeps using `StructuredDataView` — undescribed data still deserves the
68
+ * renderer built for undescribed data.
69
+ */
70
+
71
+ /** Width of the shared label column, set once on the root (see `labelColumn`). */
72
+ const LABEL_COL_VAR = '--schema-view-label-col';
73
+
74
+ const StyledSchemaView = styled.div`
75
+ display: flex;
76
+ flex-direction: column;
77
+ gap: 6px;
78
+ min-width: 0;
79
+ `;
80
+
81
+ /**
82
+ * One list item: its number in the left margin, its fields beside it.
83
+ *
84
+ * `align-items: start` over a single grid row is what puts the first field on
85
+ * the number's line — the fields are one block starting at the top, not a grid
86
+ * row each, so nothing can push them down past their own marker.
87
+ */
88
+ const StyledItem = styled.div`
89
+ display: grid;
90
+ grid-template-columns: auto minmax(0, 1fr);
91
+ column-gap: 8px;
92
+ align-items: start;
93
+ min-width: 0;
94
+ `;
95
+
96
+ /** The item number. Furniture, so it reads quieter than the data it indexes;
97
+ * `tabular-nums` keeps "9." and "10." the same width. */
98
+ const StyledMarker = styled.div<{ $color: string }>`
99
+ color: ${({ $color }) => $color};
100
+ font-size: 12px;
101
+ font-variant-numeric: tabular-nums;
102
+ line-height: 20px;
103
+ white-space: nowrap;
104
+ `;
105
+
106
+ /**
107
+ * The item's identifying value, set as its heading.
108
+ *
109
+ * The first field a schema declares is the one that says WHICH item this is — a
110
+ * method's name, a scheme's type — and it was reading as just another row: same
111
+ * size, same weight, sat behind its own label. Scanning seven methods meant
112
+ * reading fourteen lines to find seven names.
113
+ *
114
+ * Promoting the value (not the label) is what makes the list scannable: the eye
115
+ * lands on `init`, `onOrderStatus`, `onConnect` down the left edge, and the
116
+ * supporting fields stay where they were. The label is not lost, it moves to the
117
+ * title attribute — a heading that needs a caption is not a heading.
118
+ */
119
+ const StyledItemTitle = styled.div<{ $color: string; $mono: boolean; $accent: string }>`
120
+ color: ${({ $color }) => $color};
121
+ /* An accent bar and a tinted surface, so the name is found by shape and not
122
+ only by size — at seven items, scanning a column of identical-weight text is
123
+ the thing this is trying to remove. The bar sits where the item's own rule
124
+ runs, so the heading reads as the start of that item rather than as a
125
+ decoration floating beside it. */
126
+ display: inline-block;
127
+ background: ${({ $accent }) => `${$accent}1f`};
128
+ border-left: 3px solid ${({ $accent }) => $accent};
129
+ border-radius: 0 4px 4px 0;
130
+ padding: 1px 8px;
131
+ margin-left: -11px;
132
+ /* Outranks the field labels beneath it, which are 600 at 13px.
133
+ One step was not enough: at 14px the name and the "Description" label under
134
+ it read as the same thing, and the reported symptom was exactly that — the
135
+ name looked like another label. Three points and a full weight above the
136
+ labels is what separates "which item is this" from "what is in it". */
137
+ font-weight: 700;
138
+ font-size: 16px;
139
+ line-height: 22px;
140
+ margin-bottom: 3px;
141
+ min-width: 0;
142
+ overflow-wrap: anywhere;
143
+ /* A literal keeps its mono face, one notch down: mono runs visually larger at
144
+ the same pixel size, so 15px here sits level with 16px prose. */
145
+ ${({ $mono }) => ($mono ? `font-family: ${MONO_FONT_STACK}; font-size: 15px;` : '')}
146
+ `;
147
+
148
+ /**
149
+ * Does this record have anything to show BELOW its heading?
150
+ *
151
+ * The heading consumes the identifying field, so a record holding only that has
152
+ * an empty body — and a rule drawn down an empty body is a line to nowhere.
153
+ */
154
+ const hasContentUnderHeading = (record: unknown, schema?: IQorusFormSchema): boolean => {
155
+ if (!isRecord(record)) {
156
+ return true;
157
+ }
158
+ const titleKey = titleKeyFor(record, schema);
159
+
160
+ return Object.entries(record).some(
161
+ ([key, value]) =>
162
+ key !== titleKey && value !== undefined && value !== null && value !== ''
163
+ );
164
+ };
165
+
166
+ /** The rule tying an item's fields to its number.
167
+ *
168
+ * Same 3px as the heading's accent bar directly above it: at 1px the two read as
169
+ * two different devices stacked on each other rather than one rule running down
170
+ * the item, and the padding drops to 8px so the text stays on the same column.
171
+ *
172
+ * The rule is drawn only when something is actually under the heading. The
173
+ * heading is pulled left of the rule by its own negative margin, so an item
174
+ * whose record holds nothing but its identity showed a 1-2px sliver of line and
175
+ * nothing else — which reads as a rendering artefact, because that is what it
176
+ * was. It keeps a transparent border in that case so the column does not shift.
177
+ */
178
+ const StyledItemFields = styled.div<{ $border: string; $rule: boolean }>`
179
+ border-left: 3px solid ${({ $border, $rule }) => ($rule ? $border : 'transparent')};
180
+ padding-left: 8px;
181
+ min-width: 0;
182
+ `;
183
+
184
+ /**
185
+ * A record's fields: one shared label column, values in the next.
186
+ *
187
+ * The column width is a variable set once on the root, so every field of every
188
+ * item lines up on one edge. Sizing per record would align an item's own fields
189
+ * and leave the items ragged against each other, which reads as a rendering
190
+ * fault rather than as a list.
191
+ */
192
+ /**
193
+ * A record field's name, in the detail-page vocabulary.
194
+ *
195
+ * Uppercase, tracked and dimmed, so a field name reads as furniture and the
196
+ * VALUE beside it is what the eye lands on. Same treatment the read-only
197
+ * interface panels use, which is the point: an in-editor list of records and a
198
+ * detail-page panel showing the same shape should not look like two components.
199
+ *
200
+ * A variant rather than a change to `StyledRowLabel`: that one is the FORM's own
201
+ * option-row label, and the form's fields are not read-only furniture — they are
202
+ * the things you are about to edit. Composing it keeps the colour, weight base
203
+ * and wrapping behaviour in one place instead of copying them.
204
+ *
205
+ * Applies to the label only. A VALUE is never uppercased: values are frequently
206
+ * case-sensitive — a service method is `onOrderStatus`, not `ONORDERSTATUS` —
207
+ * and the item heading is a value.
208
+ */
209
+ const StyledFieldLabel = styled(StyledRowLabel)`
210
+ font-size: 12px;
211
+ text-transform: uppercase;
212
+ letter-spacing: 0.06em;
213
+ opacity: 0.55;
214
+ `;
215
+
216
+ const StyledFields = styled.div`
217
+ /* The supporting fields step in under the heading, so the heading keeps the
218
+ left edge to itself. This is the half that answers "the name is written in
219
+ the position of the label": with the labels indented, it no longer is. */
220
+ padding-left: 14px;
221
+ display: grid;
222
+ grid-template-columns: var(${LABEL_COL_VAR}, 140px) minmax(0, 1fr);
223
+ column-gap: 10px;
224
+ row-gap: 2px;
225
+ align-items: baseline;
226
+ min-width: 0;
227
+
228
+ /* A row's value truncates because a row is one line high. The preview is the
229
+ place that shows what the row could not, so a long value wraps here instead
230
+ of ellipsising — otherwise opening the preview would reveal exactly as
231
+ little as the line above it. */
232
+ .options-readfirst-valuetext {
233
+ white-space: normal;
234
+ overflow: visible;
235
+ text-overflow: clip;
236
+ overflow-wrap: anywhere;
237
+ }
238
+
239
+ /* Mono faces run visually larger than the UI face at the same pixel size, so
240
+ a literal set at the row's 13px looks a step bolder than the label naming
241
+ it. A hair smaller puts the two back on the same footing. */
242
+ .schema-view-data {
243
+ font-size: 12.5px;
244
+ }
245
+ `;
246
+
247
+ /** A nested level spans both columns and indents under its own name. */
248
+ const StyledNested = styled.div<{ $border: string }>`
249
+ grid-column: 1 / -1;
250
+ margin-left: 2px;
251
+ padding-left: 10px;
252
+ border-left: 1px solid ${({ $border }) => $border};
253
+ min-width: 0;
254
+ `;
255
+
256
+ const StyledNestedLabel = styled.div`
257
+ grid-column: 1 / -1;
258
+ `;
259
+
260
+ /** A code value is a block, not a cell: it spans the grid under its own name. */
261
+ const StyledCodeCell = styled.div`
262
+ grid-column: 1 / -1;
263
+ min-width: 0;
264
+ margin: 2px 0 4px;
265
+ `;
266
+
267
+ export interface ISchemaDataViewProps {
268
+ /** The stored value, with or without `{type, value}` envelopes. */
269
+ value: unknown;
270
+ /** The schema describing this level — a field's `arg_schema`. */
271
+ schema: IQorusFormSchema;
272
+ /** Show each field's declared type beside its name. */
273
+ showTypes?: boolean;
274
+ /**
275
+ * Colours, taken from the row so the preview matches its surroundings.
276
+ *
277
+ * `accent` marks the identifying heading. It comes from the consuming row's
278
+ * THEME rather than a literal here, so an app that registers its own intent
279
+ * (BRAND_DESIGN §1 names qorus-ide's brand purple as exactly this case) gets
280
+ * its own colour without this component knowing anything about it. It is only
281
+ * ever a fill or a bar, never applied to text: an intent used as text fails
282
+ * contrast — a muted label measures 1.3:1.
283
+ */
284
+ colors: { key: string; muted: string; border: string; accent: string };
285
+ onItemClick?: () => void;
286
+ }
287
+
288
+ /** Look inside a `{type, value}` envelope, once. `isUiEncodedValue` is the
289
+ * engine's single authority on what an envelope is — a looser test would unwrap
290
+ * a real user hash that happens to carry a `value` key and drop its siblings. */
291
+ const unwrap = (item: unknown): unknown => (isUiEncodedValue(item) ? item.value : item);
292
+
293
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
294
+ !!value && typeof value === 'object' && !Array.isArray(value);
295
+
296
+ /**
297
+ * The width of the shared label column, measured from the longest name that will
298
+ * actually be rendered.
299
+ *
300
+ * A fixed width is a guess in both directions: too wide and a name sits far from
301
+ * its value, too narrow and every name ellipsises. Measuring fits the content;
302
+ * the `%` ceiling stops one long name from taking the whole row when the inset
303
+ * is narrow — a phone, or a deeply nested level.
304
+ */
305
+ /** Approximate rendered width of one label character, in px. See `labelColumn`. */
306
+ const LABEL_CHAR_PX = 9.5;
307
+
308
+ /** Gap to the value, so the longest label is not flush against it. */
309
+ const LABEL_SLACK_PX = 14;
310
+
311
+ const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
312
+ const inner = unwrap(value);
313
+ const records = (Array.isArray(inner) ? inner.map(unwrap) : [inner]).filter(isRecord);
314
+ const longest = records.reduce((widest, record) => {
315
+ const own = orderedKeys(record, schema).reduce(
316
+ (max, key) =>
317
+ Math.max(max, fieldLabel(key, schema[key] as TQorusFormFieldSchema | undefined).length),
318
+ 0
319
+ );
320
+ return Math.max(widest, own);
321
+ }, 0);
322
+ // px, not `ch`. `ch` is the base font's `0` advance and knows nothing about
323
+ // the label's own treatment — uppercase, semibold, `letter-spacing: 0.06em`,
324
+ // at 12px — so it under-measures by about a tenth. With `overflow-wrap:
325
+ // anywhere` on the label, an under-measured column does not ellipsise, it
326
+ // BREAKS THE NAME IN HALF: `LEGACY_FLAG` came out over two lines the moment
327
+ // the labels became uppercase. Erring high costs a few px of gap; erring low
328
+ // costs a broken word.
329
+ return longest ? `min(${Math.ceil(longest * LABEL_CHAR_PX + LABEL_SLACK_PX)}px, 45%)` : '140px';
330
+ };
331
+
332
+ /**
333
+ * `true` when a value is an address a browser can open.
334
+ *
335
+ * Deliberately per VALUE, not per field: a field named `url` frequently holds
336
+ * something that is not one — a bare path, a regex, a template — and offering
337
+ * those a link promises a destination there is not.
338
+ */
339
+ const isNavigableUrl = (value: unknown): value is string =>
340
+ typeof value === 'string' && /^(https?|wss?|ftps?):\/\//i.test(value);
341
+
342
+ /** One field's value, rendered the way its row above renders it. */
343
+ const FieldValue = ({
344
+ field,
345
+ fieldSchema,
346
+ color,
347
+ }: {
348
+ field: IQorusFormField;
349
+ fieldSchema: TQorusFormFieldSchema | undefined;
350
+ color: string;
351
+ }) => {
352
+ const formatted = formatOptionValue(field, fieldSchema);
353
+ // An allowed value that carries a logo shows it here too, exactly as the row
354
+ // above does — the preview is the same value, so it gets the same treatment.
355
+ const image = getAllowedValueImage(field.value, fieldSchema);
356
+
357
+ // Data reads in mono; a chosen label does not.
358
+ //
359
+ // "qorus-session" is a literal the author typed — an identifier where the
360
+ // characters themselves matter, and where a proportional font makes `l`/`1`
361
+ // and `O`/`0` a guess. "Default RBAC" is not data at all: it is the name of a
362
+ // choice, written by whoever wrote the schema, and setting it in mono would
363
+ // dress up a label as a value. Resolving through `allowed_values` is what
364
+ // separates the two, and it is the same lookup that produced the text.
365
+ const isChosenLabel = !!findAllowedValueOption(field.value, fieldSchema);
366
+
367
+ return (
368
+ <StyledRowValue $color={color}>
369
+ {field.value === true || field.value === false ?
370
+ <ReqoreTag
371
+ size='small'
372
+ minimal
373
+ intent={field.value === true ? 'success' : 'danger'}
374
+ label={formatted}
375
+ />
376
+ : <span
377
+ className={`options-readfirst-valuetext${isChosenLabel ? '' : ' schema-view-data'}`}
378
+ style={isChosenLabel ? undefined : { fontFamily: MONO_FONT_STACK }}
379
+ >
380
+ {image ?
381
+ <ReqoreIcon
382
+ image={image}
383
+ size='14px'
384
+ style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
385
+ />
386
+ : null}
387
+ {isNavigableUrl(field.value) ?
388
+ // The address IS the link — it used to render as inert text with a
389
+ // separate icon button beside it to open it, which is two things
390
+ // where there is one. ReqoreLink renders a real anchor, so
391
+ // middle-click and "copy link address" still work;
392
+ // `stopPropagation` because the row around it toggles.
393
+ <ReqoreLink
394
+ className='schema-view-open-url'
395
+ href={field.value}
396
+ target='_blank'
397
+ rel='noreferrer noopener'
398
+ onClick={(event: React.MouseEvent) => event.stopPropagation()}
399
+ >
400
+ {formatted}
401
+ </ReqoreLink>
402
+ : formatted}
403
+ </span>
404
+ }
405
+ </StyledRowValue>
406
+ );
407
+ };
408
+
409
+ /** One described hash: its fields as label/value pairs in the shared columns. */
410
+ const SchemaRecord = ({
411
+ record,
412
+ schema,
413
+ showTypes,
414
+ colors,
415
+ skipKey,
416
+ }: {
417
+ record: Record<string, unknown>;
418
+ schema: IQorusFormSchema;
419
+ showTypes?: boolean;
420
+ colors: ISchemaDataViewProps['colors'];
421
+ /** Rendered as the item's heading already, so it must not repeat as a row. */
422
+ skipKey?: string;
423
+ }) => {
424
+ // The SAME host renderer the row above uses for a `code-editor` field. Read
425
+ // from context rather than threaded through props: a nested level is arbitrarily
426
+ // deep, and the renderer is a property of the form, not of any one level.
427
+ const codePreviewRenderer = useContextSelector(
428
+ CompactRowContext,
429
+ (v) => v.codePreviewRenderer
430
+ );
431
+ const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
432
+
433
+ return (
434
+ <StyledFields className='schema-view-fields'>
435
+ {orderedKeys(record, schema)
436
+ .filter((key) => key !== skipKey)
437
+ .map((key) => {
438
+ const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
439
+ const nested = (fieldSchema as { arg_schema?: IQorusFormSchema } | undefined)?.arg_schema;
440
+ const value = unwrap(record[key]);
441
+ const label = fieldLabel(key, fieldSchema);
442
+ const labelWithType =
443
+ showTypes && fieldSchema?.type ? `${label} · ${fieldSchema.type}` : label;
444
+
445
+ // A field that describes its own level renders AS that level, indented
446
+ // under its name — the same decision, one step down.
447
+ if (nested && (isRecord(value) || Array.isArray(value))) {
448
+ return (
449
+ <React.Fragment key={key}>
450
+ <StyledNestedLabel>
451
+ <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
452
+ </StyledNestedLabel>
453
+ <StyledNested $border={colors.border}>
454
+ <SchemaLevel value={value} schema={nested} showTypes={showTypes} colors={colors} />
455
+ </StyledNested>
456
+ </React.Fragment>
457
+ );
458
+ }
459
+
460
+ // A code value is source, and source is read as source: the host's renderer
461
+ // gives it the syntax highlighting (and read-only LSP hover) it has in the
462
+ // Source Code field, and "Show more" keeps a long body from taking over the
463
+ // preview. Flattening it to one mono line — which is what a plain value cell
464
+ // does — throws all of that away for a field whose whole content is code.
465
+ if (isCodeField(fieldSchema) && typeof value === 'string') {
466
+ return (
467
+ <React.Fragment key={key}>
468
+ <StyledNestedLabel>
469
+ <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
470
+ </StyledNestedLabel>
471
+ <StyledCodeCell onClick={(event) => event.stopPropagation()}>
472
+ <ReqoreCollapsibleContent
473
+ maxCollapsedHeight={96}
474
+ buttonProps={{ className: 'options-readfirst-viewmore' }}
475
+ >
476
+ {codePreviewRenderer ?
477
+ codePreviewRenderer({
478
+ value,
479
+ name: key,
480
+ schema: fieldSchema,
481
+ options: schema,
482
+ values: record as never,
483
+ })
484
+ : <StyledCodePreview
485
+ className='options-readfirst-code'
486
+ $bg={cHover}
487
+ $border={`${colors.border}88`}
488
+ $fg={colors.key}
489
+ >
490
+ {value}
491
+ </StyledCodePreview>
492
+ }
493
+ </ReqoreCollapsibleContent>
494
+ </StyledCodeCell>
495
+ </React.Fragment>
496
+ );
497
+ }
498
+
499
+ return (
500
+ <React.Fragment key={key}>
501
+ <StyledFieldLabel $color={colors.key} title={label}>
502
+ {labelWithType}
503
+ </StyledFieldLabel>
504
+ <FieldValue
505
+ field={{ type: fieldSchema?.type, value } as IQorusFormField}
506
+ fieldSchema={fieldSchema}
507
+ color={colors.key}
508
+ />
509
+ </React.Fragment>
510
+ );
511
+ })}
512
+ </StyledFields>
513
+ );
514
+ };
515
+
516
+ /** A level: a list of numbered items, or a single described hash. */
517
+ const SchemaLevel = ({
518
+ value,
519
+ schema,
520
+ showTypes,
521
+ colors,
522
+ }: {
523
+ value: unknown;
524
+ schema: IQorusFormSchema;
525
+ showTypes?: boolean;
526
+ colors: ISchemaDataViewProps['colors'];
527
+ }) => {
528
+ const inner = unwrap(value);
529
+
530
+ if (Array.isArray(inner)) {
531
+ return (
532
+ <>
533
+ {inner.map((item, index) => {
534
+ const record = unwrap(item);
535
+ return (
536
+ <StyledItem key={index} className='schema-view-item'>
537
+ <StyledMarker $color={colors.muted}>{index + 1}.</StyledMarker>
538
+ <StyledItemFields
539
+ $border={colors.border}
540
+ $rule={hasContentUnderHeading(record, schema)}
541
+ >
542
+ {isRecord(record) ?
543
+ <>
544
+ {(() => {
545
+ const identity = recordIdentity(record, schema);
546
+ if (!identity) {
547
+ return null;
548
+ }
549
+ return (
550
+ <StyledItemTitle
551
+ className='schema-view-item-title'
552
+ $color={colors.key}
553
+ // A literal keeps the mono treatment it has as a value —
554
+ // promoting it must not change what it IS. A chosen
555
+ // label ("Default RBAC") is prose and stays prose.
556
+ $mono={identity.mono}
557
+ $accent={colors.accent}
558
+ title={identity.label}
559
+ >
560
+ {identity.text}
561
+ </StyledItemTitle>
562
+ );
563
+ })()}
564
+ <SchemaRecord
565
+ record={record}
566
+ schema={schema}
567
+ showTypes={showTypes}
568
+ colors={colors}
569
+ skipKey={titleKeyFor(record, schema)}
570
+ />
571
+ </>
572
+ : <StyledRowValue $color={colors.key}>
573
+ <span
574
+ className='options-readfirst-valuetext schema-view-data'
575
+ style={{ fontFamily: MONO_FONT_STACK }}
576
+ >
577
+ {formatOptionValue({ value: record } as IQorusFormField)}
578
+ </span>
579
+ </StyledRowValue>
580
+ }
581
+ </StyledItemFields>
582
+ </StyledItem>
583
+ );
584
+ })}
585
+ </>
586
+ );
587
+ }
588
+
589
+ if (isRecord(inner)) {
590
+ return <SchemaRecord record={inner} schema={schema} showTypes={showTypes} colors={colors} />;
591
+ }
592
+
593
+ return (
594
+ <ReqoreP size='small' style={{ color: colors.key }}>
595
+ {formatOptionValue({ value: inner } as IQorusFormField)}
596
+ </ReqoreP>
597
+ );
598
+ };
599
+
600
+ export const SchemaDataView = React.memo(
601
+ ({ value, schema, showTypes, colors, onItemClick }: ISchemaDataViewProps) => (
602
+ <StyledSchemaView
603
+ className='schema-data-view'
604
+ onClick={onItemClick}
605
+ style={{ [LABEL_COL_VAR]: labelColumn(value, schema) } as React.CSSProperties}
606
+ >
607
+ <SchemaLevel value={value} schema={schema} showTypes={showTypes} colors={colors} />
608
+ </StyledSchemaView>
609
+ )
610
+ );
611
+
612
+ SchemaDataView.displayName = 'SchemaDataView';
613
+
614
+ /**
615
+ * Whether a value can be rendered through a schema at all.
616
+ *
617
+ * A schema describes fields, so there has to be a described level to show: a
618
+ * hash, or a list of them. A list of scalars has no field names to use and falls
619
+ * back to the untyped renderer, which is the right tool for it.
620
+ */
621
+ export const canRenderWithSchema = (value: unknown, schema: unknown): boolean => {
622
+ if (!schema || typeof schema !== 'object' || !Object.keys(schema).length) {
623
+ return false;
624
+ }
625
+ const inner = unwrap(value);
626
+ if (Array.isArray(inner)) {
627
+ return inner.some((item) => isRecord(unwrap(item)));
628
+ }
629
+ return isRecord(inner);
630
+ };
@@ -320,6 +320,12 @@ export const StyledRowValue = styled.div<{ $color: string; $empty?: boolean }>`
320
320
  text-overflow: ellipsis;
321
321
  white-space: nowrap;
322
322
  }
323
+ /* The code size chip kept above an OPEN editor. The read row puts the same chip
324
+ at the top of the value cell with a 2px row-gap under it; matching that here is
325
+ what stops the editor moving as the field opens. */
326
+ .options-readfirst-editing-summary {
327
+ margin-bottom: 2px;
328
+ }
323
329
  .options-readfirst-reason {
324
330
  font-style: italic;
325
331
  font-size: 12px;
@@ -381,6 +387,19 @@ export const StyledRowInset = styled.div`
381
387
  margin-top: 4px;
382
388
  `;
383
389
 
390
+ /**
391
+ * The stack literal text renders in: source, ids, tokens, data strings.
392
+ *
393
+ * Defined HERE rather than taken from reqore's `ReqoreFonts.mono`, which is the
394
+ * same stack. Reqore is a peer dependency at `>=0.71.16` and that export arrived
395
+ * later, so reading it would crash every consumer inside the declared range —
396
+ * the whole form preview lost over a font choice. One local constant costs
397
+ * nothing and keeps the code preview and the schema view's data values
398
+ * identical, which is the only reason to share it in the first place.
399
+ */
400
+ export const MONO_FONT_STACK =
401
+ "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace";
402
+
384
403
  // A collapsed code-block preview shown under the value summary for a
385
404
  // `code-editor` field. Multi-line, monospace, subtle background — matches
386
405
  // the aesthetic of the classic code-view surface but small enough to sit
@@ -393,8 +412,7 @@ export const StyledCodePreview = styled.pre<{ $bg: string; $border: string; $fg:
393
412
  background: ${({ $bg }) => $bg};
394
413
  border: 1px solid ${({ $border }) => $border};
395
414
  color: ${({ $fg }) => $fg};
396
- font-family:
397
- ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
415
+ font-family: ${MONO_FONT_STACK};
398
416
  font-size: 11.5px;
399
417
  line-height: 1.5;
400
418
  white-space: pre;
@@ -672,3 +690,27 @@ export const StyledGroupBody = styled.div<{
672
690
  padding-bottom: 0;
673
691
  }
674
692
  `;
693
+
694
+ // One absorbed sibling, rendered inside its host row's container rather than
695
+ // on a row of its own (see `absorb_fields`). The pair this exists for is a code
696
+ // editor and its language: reading them as two unrelated rows one above the
697
+ // other said they were separate decisions, and they are not.
698
+ //
699
+ // Laid out as label-then-control on one line so it reads as a property of the
700
+ // element it now sits in, and separated from the editor below it by a hairline
701
+ // rather than a gap — the two share one container, so the seam has to look like
702
+ // a division inside it and not like two stacked things.
703
+ export const StyledAbsorbedField = styled.div`
704
+ display: flex;
705
+ align-items: center;
706
+ gap: 10px;
707
+ min-width: 0;
708
+ padding-bottom: 8px;
709
+ margin-bottom: 8px;
710
+ border-bottom: 1px solid ${({ theme }) => `${theme.main}22`};
711
+ `;
712
+
713
+ export const StyledAbsorbedLabel = styled(ReqoreP)`
714
+ flex: 0 0 auto;
715
+ margin: 0;
716
+ `;