@cosmicdrift/kumiko-headless 0.328.0 → 0.328.1

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 (108) hide show
  1. package/dist/apex/css.d.ts +3 -0
  2. package/{src/apex/css.ts → dist/apex/css.js} +11 -24
  3. package/dist/apex/index.d.ts +229 -0
  4. package/dist/apex/index.js +360 -0
  5. package/dist/apex/lightbox.d.ts +4 -0
  6. package/{src/apex/lightbox.ts → dist/apex/lightbox.js} +0 -4
  7. package/dist/contracts/asset.d.ts +15 -0
  8. package/dist/contracts/asset.js +17 -0
  9. package/dist/contracts/index.d.ts +3 -0
  10. package/dist/contracts/index.js +1 -0
  11. package/dist/contracts/locale.d.ts +7 -0
  12. package/dist/contracts/locale.js +11 -0
  13. package/dist/contracts/primitives.d.ts +93 -0
  14. package/dist/contracts/primitives.js +21 -0
  15. package/dist/dispatcher/index.d.ts +2 -0
  16. package/dist/dispatcher/index.js +1 -0
  17. package/dist/dispatcher/types.d.ts +86 -0
  18. package/dist/dispatcher/types.js +34 -0
  19. package/dist/form/form-controller.d.ts +2 -0
  20. package/dist/form/form-controller.js +404 -0
  21. package/dist/form/index.d.ts +3 -0
  22. package/dist/form/index.js +2 -0
  23. package/dist/form/types.d.ts +71 -0
  24. package/dist/form/types.js +1 -0
  25. package/dist/form/zod-bridge.d.ts +5 -0
  26. package/{src/form/zod-bridge.ts → dist/form/zod-bridge.js} +52 -58
  27. package/dist/format/escape.d.ts +5 -0
  28. package/{src/format/escape.ts → dist/format/escape.js} +43 -47
  29. package/dist/format/html-template.d.ts +9 -0
  30. package/dist/format/html-template.js +39 -0
  31. package/dist/format/index.d.ts +10 -0
  32. package/dist/format/index.js +192 -0
  33. package/dist/format/money.d.ts +1 -0
  34. package/{src/format/money.ts → dist/format/money.js} +6 -5
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.js +9 -0
  37. package/dist/locale-routing/index.d.ts +39 -0
  38. package/dist/locale-routing/index.js +143 -0
  39. package/dist/nav/index.d.ts +2 -0
  40. package/dist/nav/index.js +1 -0
  41. package/dist/nav/resolve.d.ts +2 -0
  42. package/dist/nav/resolve.js +58 -0
  43. package/dist/nav/types.d.ts +26 -0
  44. package/dist/nav/types.js +1 -0
  45. package/dist/store/create-store.d.ts +2 -0
  46. package/{src/store/create-store.ts → dist/store/create-store.js} +21 -23
  47. package/dist/store/equality.d.ts +1 -0
  48. package/{src/store/equality.ts → dist/store/equality.js} +19 -17
  49. package/dist/store/index.d.ts +3 -0
  50. package/dist/store/index.js +2 -0
  51. package/dist/store/types.d.ts +7 -0
  52. package/dist/store/types.js +10 -0
  53. package/dist/view-model/edit.d.ts +10 -0
  54. package/dist/view-model/edit.js +392 -0
  55. package/dist/view-model/embedded-list.d.ts +27 -0
  56. package/dist/view-model/embedded-list.js +63 -0
  57. package/dist/view-model/index.d.ts +7 -0
  58. package/dist/view-model/index.js +3 -0
  59. package/dist/view-model/list.d.ts +16 -0
  60. package/dist/view-model/list.js +201 -0
  61. package/dist/view-model/types.d.ts +300 -0
  62. package/dist/view-model/types.js +1 -0
  63. package/package.json +31 -12
  64. package/src/apex/__tests__/lightbox.test.ts +0 -20
  65. package/src/apex/__tests__/page-head.test.ts +0 -63
  66. package/src/apex/__tests__/render.test.ts +0 -401
  67. package/src/apex/index.ts +0 -667
  68. package/src/contracts/__tests__/contracts.test.ts +0 -123
  69. package/src/contracts/__tests__/field-issue-compat.test.ts +0 -19
  70. package/src/contracts/asset.ts +0 -48
  71. package/src/contracts/index.ts +0 -23
  72. package/src/contracts/locale.ts +0 -33
  73. package/src/contracts/primitives.ts +0 -158
  74. package/src/dispatcher/__tests__/contract.test.ts +0 -163
  75. package/src/dispatcher/index.ts +0 -16
  76. package/src/dispatcher/types.ts +0 -217
  77. package/src/form/__tests__/conditional-fields.test.ts +0 -177
  78. package/src/form/__tests__/form-controller.test.ts +0 -231
  79. package/src/form/__tests__/submit.test.ts +0 -575
  80. package/src/form/__tests__/validation.test.ts +0 -274
  81. package/src/form/form-controller.ts +0 -458
  82. package/src/form/index.ts +0 -15
  83. package/src/form/types.ts +0 -297
  84. package/src/format/__tests__/escape.test.ts +0 -79
  85. package/src/format/__tests__/format.test.ts +0 -285
  86. package/src/format/__tests__/html-template.test.ts +0 -47
  87. package/src/format/html-template.ts +0 -49
  88. package/src/format/index.ts +0 -223
  89. package/src/index.ts +0 -118
  90. package/src/locale-routing/__tests__/locale-routing.test.ts +0 -276
  91. package/src/locale-routing/index.ts +0 -218
  92. package/src/nav/__tests__/resolve.test.ts +0 -279
  93. package/src/nav/index.ts +0 -8
  94. package/src/nav/resolve.ts +0 -60
  95. package/src/nav/types.ts +0 -70
  96. package/src/store/__tests__/create-store.test.ts +0 -139
  97. package/src/store/__tests__/equality.test.ts +0 -66
  98. package/src/store/index.ts +0 -3
  99. package/src/store/types.ts +0 -27
  100. package/src/view-model/__tests__/edit.test.ts +0 -1072
  101. package/src/view-model/__tests__/embedded-list.test.ts +0 -96
  102. package/src/view-model/__tests__/field-labels.test.ts +0 -62
  103. package/src/view-model/__tests__/list.test.ts +0 -439
  104. package/src/view-model/edit.ts +0 -645
  105. package/src/view-model/embedded-list.ts +0 -89
  106. package/src/view-model/index.ts +0 -43
  107. package/src/view-model/list.ts +0 -280
  108. package/src/view-model/types.ts +0 -400
@@ -0,0 +1,392 @@
1
+ import { evalFieldCondition, isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, parseRefTarget, sectionFieldSpecs, WRITE_FORM_SECTION_ENTITY, } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { buildOptionLabels, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, } from "./list.js";
3
+ // Resolves a writeForm section's own fieldDefs/fields through the same
4
+ // per-field pipeline as a plain fields section, by reusing the pipeline
5
+ // itself rather than duplicating it: wrap fieldDefs/fields as a throwaway
6
+ // entityEdit screen (same "synthesize a pseudo-entity" idiom as the shims in
7
+ // packages/renderer/src/app/*-shim.ts) and recurse into computeEditViewModel
8
+ // once. Split out of computeEditViewModel's section-map to keep that
9
+ // function's own complexity from absorbing this branch's decision points.
10
+ function computeWriteFormSectionViewModel(sectionSpec, screenId, values, translate, featureName) {
11
+ const innerEntity = { fields: sectionSpec.fieldDefs };
12
+ const innerScreen = {
13
+ id: `${screenId}:${sectionSpec.id ?? "write-form"}`,
14
+ type: "entityEdit",
15
+ entity: WRITE_FORM_SECTION_ENTITY,
16
+ layout: {
17
+ sections: [{ kind: "fields", fields: sectionSpec.fields, columns: sectionSpec.columns }],
18
+ },
19
+ };
20
+ const inner = computeEditViewModel({
21
+ screen: innerScreen,
22
+ entity: innerEntity,
23
+ values,
24
+ translate,
25
+ featureName,
26
+ });
27
+ const innerSection = inner.sections[0];
28
+ const fields = innerSection?.kind === "fields" ? innerSection.fields : [];
29
+ return {
30
+ kind: "writeForm",
31
+ ...(sectionSpec.title !== undefined && { title: translate(sectionSpec.title) }),
32
+ ...(sectionSpec.description !== undefined && {
33
+ description: translate(sectionSpec.description),
34
+ }),
35
+ columns: sectionSpec.columns ?? 1,
36
+ fields,
37
+ ...(sectionSpec.icon !== undefined && { icon: sectionSpec.icon }),
38
+ handler: sectionSpec.handler,
39
+ ...(sectionSpec.submitLabel !== undefined && {
40
+ submitLabel: translate(sectionSpec.submitLabel),
41
+ }),
42
+ ...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
43
+ };
44
+ }
45
+ // relatedList runs its own query — nothing here to resolve against
46
+ // entity/values, the spec passes through verbatim (fw#2166).
47
+ function computeRelatedListSectionViewModel(sectionSpec, translate) {
48
+ return {
49
+ kind: "relatedList",
50
+ title: translate(sectionSpec.title),
51
+ query: sectionSpec.query,
52
+ ...(sectionSpec.parentParam !== undefined && { parentParam: sectionSpec.parentParam }),
53
+ ...(sectionSpec.parentFilter !== undefined && { parentFilter: sectionSpec.parentFilter }),
54
+ columns: sectionSpec.columns,
55
+ ...(sectionSpec.description !== undefined && {
56
+ description: translate(sectionSpec.description),
57
+ }),
58
+ ...(sectionSpec.itemNoun !== undefined && { itemNoun: sectionSpec.itemNoun }),
59
+ ...(sectionSpec.pageSize !== undefined && { pageSize: sectionSpec.pageSize }),
60
+ ...(sectionSpec.defaultSort !== undefined && { defaultSort: sectionSpec.defaultSort }),
61
+ ...(sectionSpec.searchable !== undefined && { searchable: sectionSpec.searchable }),
62
+ ...(sectionSpec.facets !== undefined && { facets: sectionSpec.facets }),
63
+ ...(sectionSpec.rowClick !== undefined && { rowClick: sectionSpec.rowClick }),
64
+ ...(sectionSpec.rowActions !== undefined && { rowActions: sectionSpec.rowActions }),
65
+ ...(sectionSpec.toolbarActions !== undefined && { toolbarActions: sectionSpec.toolbarActions }),
66
+ ...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
67
+ ...(sectionSpec.emptyState !== undefined && {
68
+ emptyState: {
69
+ title: translate(sectionSpec.emptyState.title),
70
+ ...(sectionSpec.emptyState.description !== undefined && {
71
+ description: translate(sectionSpec.emptyState.description),
72
+ }),
73
+ // Action stays untranslated, same convention as rowActions/
74
+ // toolbarActions above — the renderer's row-actions builder
75
+ // translates `action.label` at button-build time.
76
+ ...(sectionSpec.emptyState.action !== undefined && {
77
+ action: sectionSpec.emptyState.action,
78
+ }),
79
+ },
80
+ }),
81
+ };
82
+ }
83
+ function deriveSelectFieldHints(fieldDef, translate, featureName, entityName, fieldName) {
84
+ const options = fieldDef.type === "select" || fieldDef.type === "multiSelect"
85
+ ? (fieldDef.options ?? [])
86
+ : undefined;
87
+ const optionLabels = options !== undefined
88
+ ? buildOptionLabels(translate, (value) => fieldOptionLabelKey(featureName, entityName, fieldName, value), options)
89
+ : undefined;
90
+ const display = fieldDef.type === "multiSelect" || fieldDef.type === "select" ? fieldDef.display : undefined;
91
+ const columns = fieldDef.type === "multiSelect" ? fieldDef.columns : undefined;
92
+ const maxRows = fieldDef.type === "multiSelect" ? fieldDef.maxRows : undefined;
93
+ return {
94
+ ...(options !== undefined && { options }),
95
+ ...(optionLabels !== undefined && { optionLabels }),
96
+ ...(display !== undefined && { display }),
97
+ ...(columns !== undefined && { columns }),
98
+ ...(maxRows !== undefined && { maxRows }),
99
+ };
100
+ }
101
+ function deriveTextFieldHints(fieldDef) {
102
+ const multiline = fieldDef.type === "text" || fieldDef.type === "longText"
103
+ ? fieldDef.multiline
104
+ : undefined;
105
+ const format = fieldDef.type === "text"
106
+ ? fieldDef.format
107
+ : undefined;
108
+ return {
109
+ ...(multiline !== undefined && { multiline }),
110
+ ...(format !== undefined && { format }),
111
+ };
112
+ }
113
+ function deriveDateFieldHints(fieldDef) {
114
+ const wallClock = fieldDef.type === "timestamp" &&
115
+ fieldDef.locatedBy !== undefined
116
+ ? true
117
+ : undefined;
118
+ const dateBounds = fieldDef.type === "date" ||
119
+ fieldDef.type === "timestamp" ||
120
+ fieldDef.type === "locatedTimestamp"
121
+ ? fieldDef
122
+ : undefined;
123
+ return {
124
+ ...(wallClock !== undefined && { wallClock }),
125
+ ...(dateBounds?.min !== undefined && { min: dateBounds.min }),
126
+ ...(dateBounds?.max !== undefined && { max: dateBounds.max }),
127
+ ...(dateBounds?.locale !== undefined && { dateLocale: dateBounds.locale }),
128
+ };
129
+ }
130
+ function deriveNumericFieldHints(fieldDef, resolvedCurrency) {
131
+ const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
132
+ return {
133
+ ...(grouping !== undefined && { grouping }),
134
+ ...(fieldDef.type === "money" && { currency: resolvedCurrency }),
135
+ ...(fieldDef.type === "number" && fieldDef.unit !== undefined && { unit: fieldDef.unit }),
136
+ };
137
+ }
138
+ // Declared reference metadata (EditFieldSpec.refEntity) takes priority over
139
+ // fieldDef.type — see computeEditViewModel's effectiveType, same reasoning.
140
+ function deriveReferenceFieldHints(fieldDef, normalized, declaredRefTarget, featureName) {
141
+ const refRaw = declaredRefTarget === undefined && fieldDef.type === "reference"
142
+ ? fieldDef.entity
143
+ : undefined;
144
+ const refTarget = declaredRefTarget ?? (refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined);
145
+ const refLabelField = declaredRefTarget !== undefined
146
+ ? (normalized.refLabelField ?? "id")
147
+ : fieldDef.type === "reference"
148
+ ? (fieldDef.labelField ?? "id")
149
+ : undefined;
150
+ return {
151
+ ...(refTarget?.entityName !== undefined && { refEntity: refTarget.entityName }),
152
+ ...(refTarget?.featureName !== undefined && { refFeature: refTarget.featureName }),
153
+ ...(refLabelField !== undefined && { refLabelField }),
154
+ ...deriveReferenceMultiHints(fieldDef, declaredRefTarget),
155
+ };
156
+ }
157
+ // Declared reference metadata has no `multiple` or `optionsQuery` concept
158
+ // (it targets row-meta/derived fields, always single-valued and resolved
159
+ // through the target entity) — only a real ReferenceFieldDef carries them.
160
+ function deriveReferenceMultiHints(fieldDef, declaredRefTarget) {
161
+ const ownReferenceDef = declaredRefTarget === undefined && fieldDef.type === "reference"
162
+ ? fieldDef
163
+ : undefined;
164
+ const refMultiple = ownReferenceDef === undefined ? undefined : (ownReferenceDef.multiple ?? false);
165
+ return {
166
+ ...(ownReferenceDef?.optionsQuery !== undefined && {
167
+ refOptionsQuery: ownReferenceDef.optionsQuery,
168
+ }),
169
+ ...(refMultiple !== undefined && { refMultiple }),
170
+ };
171
+ }
172
+ // file/image: entityType/fieldName travel with the upload POST so the
173
+ // endpoint validates against the right field definition.
174
+ function deriveFileFieldHints(fieldDef, entityName, fieldName) {
175
+ const isFileType = fieldDef.type === "file" || fieldDef.type === "image" || fieldDef.type === "images";
176
+ const fileDef = isFileType
177
+ ? fieldDef
178
+ : undefined;
179
+ const imageVariant = fieldDef.type === "image" || fieldDef.type === "images"
180
+ ? Object.keys(fileDef?.variants ?? {})[0]
181
+ : undefined;
182
+ const capture = fieldDef.type === "image" ? fileDef?.capture : undefined;
183
+ return {
184
+ ...(fileDef?.accept !== undefined && { accept: fileDef.accept }),
185
+ ...(fileDef?.maxSize !== undefined && { maxSize: fileDef.maxSize }),
186
+ ...(isFileType && { entityType: entityName, fieldName }),
187
+ ...(imageVariant !== undefined && { imageVariant }),
188
+ ...(capture !== undefined && { capture }),
189
+ };
190
+ }
191
+ // Embedded-LIST field (`multiple: true`) — per-cell metadata for a renderer
192
+ // to draw one row per array item (invoice-positions-style table). A plain
193
+ // (non-list) embedded field emits none of this; the renderer tells the two
194
+ // apart by whether embeddedListCells is set, not by `type` (which stays
195
+ // "embedded" either way).
196
+ function deriveEmbeddedListHints(fieldDef, translate, featureName, entityName, fieldName, resolvedCurrency) {
197
+ const isEmbeddedList = fieldDef.type === "embedded" &&
198
+ fieldDef.multiple === true;
199
+ const embeddedListDef = isEmbeddedList
200
+ ? fieldDef
201
+ : undefined;
202
+ const embeddedListCells = embeddedListDef !== undefined
203
+ ? Object.entries(embeddedListDef.schema).map(([subFieldName, subField]) => {
204
+ const cellLabel = translate(embeddedCellLabelKey(featureName, entityName, fieldName, subFieldName));
205
+ const cellOptions = subField.type === "select" ? (subField.options ?? []) : undefined;
206
+ const cellOptionLabels = cellOptions !== undefined
207
+ ? buildOptionLabels(translate, (value) => embeddedCellOptionLabelKey(featureName, entityName, fieldName, subFieldName, value), cellOptions)
208
+ : undefined;
209
+ const cellRef = subField.type === "reference" ? subField : undefined;
210
+ const cellRefTarget = cellRef?.entity !== undefined ? parseRefTarget(cellRef.entity, featureName) : undefined;
211
+ const cell = {
212
+ field: subFieldName,
213
+ label: cellLabel,
214
+ type: subField.type,
215
+ required: subField.required === true,
216
+ ...(cellOptions !== undefined && { options: cellOptions }),
217
+ ...(cellOptionLabels !== undefined && { optionLabels: cellOptionLabels }),
218
+ ...(cellRefTarget !== undefined && { refEntity: cellRefTarget.entityName }),
219
+ ...(cellRefTarget !== undefined && { refFeature: cellRefTarget.featureName }),
220
+ ...(cellRef?.labelField !== undefined && { refLabelField: cellRef.labelField }),
221
+ ...(cellRef?.optionsQuery !== undefined && {
222
+ refOptionsQuery: cellRef.optionsQuery,
223
+ }),
224
+ ...(subField.type === "decimal" &&
225
+ subField.scale !== undefined && { scale: subField.scale }),
226
+ };
227
+ return cell;
228
+ })
229
+ : undefined;
230
+ return {
231
+ ...(embeddedListCells !== undefined && { embeddedListCells }),
232
+ ...(embeddedListDef?.minItems !== undefined && {
233
+ embeddedListMinItems: embeddedListDef.minItems,
234
+ }),
235
+ ...(embeddedListDef?.maxItems !== undefined && {
236
+ embeddedListMaxItems: embeddedListDef.maxItems,
237
+ }),
238
+ ...(embeddedListDef?.derived !== undefined && {
239
+ embeddedListDerived: embeddedListDef.derived,
240
+ }),
241
+ ...(embeddedListDef?.totals !== undefined && {
242
+ embeddedListTotals: embeddedListDef.totals,
243
+ }),
244
+ // Currency lives on the head aggregate (entity.defaultCurrency), not per
245
+ // row — one value for the whole embedded list.
246
+ ...(embeddedListDef !== undefined && { embeddedListCurrency: resolvedCurrency }),
247
+ };
248
+ }
249
+ function buildExtensionSectionViewModel(sectionSpec, translate) {
250
+ return {
251
+ kind: "extension",
252
+ ...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
253
+ title: translate(sectionSpec.title),
254
+ component: sectionSpec.component,
255
+ contributesToFormSubmit: sectionSpec.contributesToFormSubmit === true,
256
+ ...(sectionSpec.entityName !== undefined && { entityName: sectionSpec.entityName }),
257
+ ...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
258
+ };
259
+ }
260
+ // Pure transform from screen-def + entity-def + row-values to the flat
261
+ // section/field tree the renderer draws. FieldConditions are evaluated here
262
+ // so the renderer never re-runs them during React render.
263
+ export function computeEditViewModel(input) {
264
+ const { screen, entity, values, translate, featureName } = input;
265
+ const sections = screen.layout.sections.map((sectionSpec) => {
266
+ if (isExtensionEditSection(sectionSpec)) {
267
+ return buildExtensionSectionViewModel(sectionSpec, translate);
268
+ }
269
+ if (isWriteFormEditSection(sectionSpec)) {
270
+ return computeWriteFormSectionViewModel(sectionSpec, screen.id, values, translate, featureName);
271
+ }
272
+ if (!isFieldsEditSection(sectionSpec)) {
273
+ return computeRelatedListSectionViewModel(sectionSpec, translate);
274
+ }
275
+ // `groups` flattens into the same per-field pipeline as plain `fields`;
276
+ // the group structure below just re-groups the computed views by name.
277
+ const fields = sectionFieldSpecs(sectionSpec).map((fieldSpec) => {
278
+ const normalized = normalizeEditField(fieldSpec);
279
+ const fieldDef = entity.fields[normalized.field];
280
+ if (!fieldDef) {
281
+ throw new Error(`computeEditViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`);
282
+ }
283
+ // Declared reference metadata (EditFieldSpec.refEntity) — for
284
+ // projectionDetail fields, which have no EntityDefinition field to
285
+ // carry a real "reference" type. Takes priority over fieldDef.type
286
+ // below so it also fires against the pseudo-entity that hardcodes
287
+ // every field as "text" (projection-detail-shim) — real entityEdit
288
+ // screens never set this, so their fieldDef.type === "reference"
289
+ // branch is unaffected.
290
+ const declaredRefTarget = normalized.refEntity !== undefined
291
+ ? parseRefTarget(normalized.refEntity, featureName)
292
+ : undefined;
293
+ const effectiveType = declaredRefTarget !== undefined ? "reference" : fieldDef.type;
294
+ const label = translate(screen.fieldLabels?.[normalized.field] ??
295
+ fieldLabelKey(featureName, screen.entity, normalized.field));
296
+ const visible = evalCondition(normalized.visible, true, values);
297
+ // `readOnly` (camelCase) is the name on both sides: EditFieldSpec
298
+ // in the engine, and the view-model emitted here. One convention
299
+ // through the stack beats translating at the boundary.
300
+ const readOnly = evalCondition(normalized.readOnly, false, values);
301
+ // `required` on the field-spec overrides the entity-default. A
302
+ // field that's required at the entity-level but marked required:
303
+ // false on the screen (e.g. a soft-onboarding wizard that
304
+ // collects less up-front) respects the screen override.
305
+ const entityRequired = fieldDef.required === true;
306
+ const required = evalCondition(normalized.required, entityRequired, values);
307
+ // ponytail: "EUR" mirrors DEFAULT_CURRENCIES[0] from
308
+ // framework/src/engine/field-helpers.ts — headless has no dependency
309
+ // on that module, so the literal is duplicated here instead of
310
+ // importing it just for one fallback string.
311
+ const resolvedCurrency = entity.defaultCurrency ?? "EUR";
312
+ const selectHints = deriveSelectFieldHints(fieldDef, translate, featureName, screen.entity, normalized.field);
313
+ const textHints = deriveTextFieldHints(fieldDef);
314
+ const dateHints = deriveDateFieldHints(fieldDef);
315
+ const numericHints = deriveNumericFieldHints(fieldDef, resolvedCurrency);
316
+ const referenceHints = deriveReferenceFieldHints(fieldDef, normalized, declaredRefTarget, featureName);
317
+ const fileHints = deriveFileFieldHints(fieldDef, screen.entity, normalized.field);
318
+ const embeddedListHints = deriveEmbeddedListHints(fieldDef, translate, featureName, screen.entity, normalized.field, resolvedCurrency);
319
+ const view = {
320
+ field: normalized.field,
321
+ label,
322
+ type: effectiveType,
323
+ value: values[normalized.field],
324
+ visible,
325
+ readOnly,
326
+ required,
327
+ ...(normalized.span !== undefined && { span: normalized.span }),
328
+ ...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
329
+ ...selectHints,
330
+ ...textHints,
331
+ ...dateHints,
332
+ ...numericHints,
333
+ ...referenceHints,
334
+ ...fileHints,
335
+ ...(normalized.icon !== undefined && { icon: normalized.icon }),
336
+ ...embeddedListHints,
337
+ };
338
+ return view;
339
+ });
340
+ // Boot-validator rejects fields.length === 0 with no groups (screens.ts),
341
+ // so an empty section never reaches this code.
342
+ const visible = fields.some((field) => field.visible);
343
+ // Tabs mode renders this section as its own panel, where two columns reads
344
+ // better than the single-column default a stacked form keeps. Applies to
345
+ // entityEdit tabs as well since fw#3134, not just projectionDetail.
346
+ const defaultColumns = screen.layout.mode === "tabs" ? 2 : 1;
347
+ const groups = sectionSpec.groups?.map((group) => ({
348
+ title: translate(group.title),
349
+ columns: group.columns ?? 2,
350
+ fields: group.fields.map((fieldSpec) => {
351
+ const fieldName = normalizeEditField(fieldSpec).field;
352
+ const fieldView = fields.find((f) => f.field === fieldName);
353
+ if (fieldView === undefined) {
354
+ throw new Error(`computeEditViewModel: screen "${screen.id}" group "${group.title}" references field ` +
355
+ `"${fieldName}" that failed to resolve.`);
356
+ }
357
+ return fieldView;
358
+ }),
359
+ }));
360
+ return {
361
+ kind: "fields",
362
+ ...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
363
+ visible,
364
+ // Titellose Section (flache Form) → kein h3; nur übersetzen wenn gesetzt.
365
+ ...(sectionSpec.title !== undefined && { title: translate(sectionSpec.title) }),
366
+ ...(sectionSpec.description !== undefined && {
367
+ description: translate(sectionSpec.description),
368
+ }),
369
+ columns: sectionSpec.columns ?? defaultColumns,
370
+ fields,
371
+ ...(groups !== undefined && { groups }),
372
+ ...(sectionSpec.icon !== undefined && { icon: sectionSpec.icon }),
373
+ ...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
374
+ };
375
+ });
376
+ const id = values["id"] ?? null;
377
+ return {
378
+ screenId: screen.id,
379
+ entityName: screen.entity,
380
+ id,
381
+ sections,
382
+ ...(screen.slots && { slots: screen.slots }),
383
+ };
384
+ }
385
+ // Resolves a FieldCondition against the current row values.
386
+ // `undefined` means "not declared" — caller substitutes the default.
387
+ function evalCondition(condition, fallback, values) {
388
+ if (condition === undefined)
389
+ return fallback;
390
+ // @cast-boundary view-model: TValues ist strukturell ein Record.
391
+ return evalFieldCondition(condition, values);
392
+ }
@@ -0,0 +1,27 @@
1
+ import type { EmbeddedDerivedCellDef } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { FieldIssue } from "../dispatcher/index.js";
3
+ export type EmbeddedDerivedOp = EmbeddedDerivedCellDef["op"];
4
+ export type { DerivedCellRoundingTarget } from "@cosmicdrift/kumiko-framework/ui-types";
5
+ export { computeDerivedCellValue, roundDerivedCellValue, } from "@cosmicdrift/kumiko-framework/ui-types";
6
+ /** Sums a numeric/money/decimal column across all rows. Non-numeric or
7
+ * missing values count as 0. Money columns are minor-unit integers —
8
+ * the sum stays in minor units, never rounds through a major-unit float. */
9
+ export declare function sumEmbeddedListColumn(rows: readonly Readonly<Record<string, unknown>>[], field: string): number;
10
+ export type EmbeddedListIssueGroups = {
11
+ /** Issues at exactly `listField` — e.g. min/max row-count violations. */
12
+ readonly listIssues: readonly FieldIssue[];
13
+ /** Issues at exactly `${listField}.${rowIndex}` — a row-level check
14
+ * (e.g. "row incomplete") that isn't attributable to one cell. */
15
+ readonly rowIssues: Readonly<Record<number, readonly FieldIssue[]>>;
16
+ /** Issues at exactly `${listField}.${rowIndex}.${cellField}`, keyed
17
+ * `${rowIndex}.${cellField}` (not the full path — the caller already
18
+ * knows listField). */
19
+ readonly cellIssues: Readonly<Record<string, readonly FieldIssue[]>>;
20
+ };
21
+ /** Buckets a flat issues-by-path map (FormSnapshot.errors shape — see
22
+ * packages/headless/src/form/zod-bridge.ts groupIssuesByPath) into the
23
+ * three levels an embedded-list widget renders. Only keys that are
24
+ * exactly `listField`, `listField.N`, or `listField.N.subfield` are
25
+ * claimed; unrelated keys (other top-level fields, or deeper nesting
26
+ * than this field ever produces) are ignored. */
27
+ export declare function groupEmbeddedListIssues(allIssues: Readonly<Record<string, readonly FieldIssue[]>>, listField: string): EmbeddedListIssueGroups;
@@ -0,0 +1,63 @@
1
+ // Canonical implementation moved to packages/framework/src/engine/embedded-derived.ts
2
+ // (the write-schema preprocess needs it server-side too).
3
+ export { computeDerivedCellValue, roundDerivedCellValue, } from "@cosmicdrift/kumiko-framework/ui-types";
4
+ /** Sums a numeric/money/decimal column across all rows. Non-numeric or
5
+ * missing values count as 0. Money columns are minor-unit integers —
6
+ * the sum stays in minor units, never rounds through a major-unit float. */
7
+ export function sumEmbeddedListColumn(rows, field) {
8
+ let sum = 0;
9
+ for (const row of rows) {
10
+ const value = row[field];
11
+ if (typeof value === "number" && Number.isFinite(value))
12
+ sum += value;
13
+ }
14
+ return sum;
15
+ }
16
+ /** Buckets a flat issues-by-path map (FormSnapshot.errors shape — see
17
+ * packages/headless/src/form/zod-bridge.ts groupIssuesByPath) into the
18
+ * three levels an embedded-list widget renders. Only keys that are
19
+ * exactly `listField`, `listField.N`, or `listField.N.subfield` are
20
+ * claimed; unrelated keys (other top-level fields, or deeper nesting
21
+ * than this field ever produces) are ignored. */
22
+ export function groupEmbeddedListIssues(allIssues, listField) {
23
+ const listIssues = [];
24
+ const rowIssues = {};
25
+ const cellIssues = {};
26
+ const prefix = `${listField}.`;
27
+ for (const [path, issues] of Object.entries(allIssues)) {
28
+ if (path === listField) {
29
+ listIssues.push(...issues);
30
+ continue;
31
+ }
32
+ if (!path.startsWith(prefix))
33
+ continue;
34
+ const rest = path.slice(prefix.length);
35
+ const segments = rest.split(".");
36
+ if (segments.length === 1) {
37
+ const rowIndex = parsePureRowIndex(segments[0]);
38
+ if (rowIndex === undefined)
39
+ continue;
40
+ rowIssues[rowIndex] = issues;
41
+ continue;
42
+ }
43
+ if (segments.length === 2) {
44
+ const rowIndex = parsePureRowIndex(segments[0]);
45
+ if (rowIndex === undefined)
46
+ continue;
47
+ const cellField = segments[1];
48
+ cellIssues[`${rowIndex}.${cellField}`] = issues;
49
+ }
50
+ // Deeper than `listField.N.subfield` — not a shape this field ever
51
+ // produces; ignore rather than misattribute to a cell.
52
+ }
53
+ return { listIssues, rowIssues, cellIssues };
54
+ }
55
+ function parsePureRowIndex(segment) {
56
+ if (segment === undefined || segment.length === 0)
57
+ return undefined;
58
+ for (const char of segment) {
59
+ if (char < "0" || char > "9")
60
+ return undefined;
61
+ }
62
+ return Number(segment);
63
+ }
@@ -0,0 +1,7 @@
1
+ export type { ComputeEditViewModelInput } from "./edit.js";
2
+ export { computeEditViewModel } from "./edit.js";
3
+ export type { DerivedCellRoundingTarget, EmbeddedDerivedOp, EmbeddedListIssueGroups, } from "./embedded-list.js";
4
+ export { computeDerivedCellValue, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./embedded-list.js";
5
+ export type { ComputeListViewModelInput } from "./list.js";
6
+ export { computeListViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, } from "./list.js";
7
+ export type { EditExtensionSectionViewModel, EditFieldSpec, EditFieldsSectionViewModel, EditFieldViewModel, EditRelatedListSectionViewModel, EditSectionSpec, EditSectionViewModel, EditViewModel, EditWriteFormSectionViewModel, EmbeddedListCellViewModel, FieldConditionCtx, FieldRenderer, ListColumnSpec, ListColumnViewModel, ListRowViewModel, ListViewModel, RuntimeRenderer, ScreenSlots, Translate, } from "./types.js";
@@ -0,0 +1,3 @@
1
+ export { computeEditViewModel } from "./edit.js";
2
+ export { computeDerivedCellValue, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./embedded-list.js";
3
+ export { computeListViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, } from "./list.js";
@@ -0,0 +1,16 @@
1
+ import type { EntityDefinition, EntityListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { ListViewModel, Translate } from "./types.js";
3
+ export type ComputeListViewModelInput = {
4
+ readonly screen: EntityListScreenDefinition;
5
+ readonly entity: EntityDefinition;
6
+ readonly rows: readonly Readonly<Record<string, unknown>>[];
7
+ readonly translate: Translate;
8
+ readonly featureName: string;
9
+ };
10
+ export declare function computeListViewModel(input: ComputeListViewModelInput): ListViewModel;
11
+ export declare function fieldLabelKey(featureName: string, entityName: string, fieldName: string): string;
12
+ export declare function fieldOptionLabelKeyPrefix(featureName: string, entityName: string, fieldName: string): string;
13
+ export declare function fieldOptionLabelKey(featureName: string, entityName: string, fieldName: string, value: string): string;
14
+ export declare function embeddedCellLabelKey(featureName: string, entityName: string, fieldName: string, subFieldName: string): string;
15
+ export declare function embeddedCellOptionLabelKey(featureName: string, entityName: string, fieldName: string, subFieldName: string, value: string): string;
16
+ export declare function buildOptionLabels(translate: (key: string, params?: Readonly<Record<string, unknown>>) => string, keyFor: (value: string) => string, options: readonly string[]): Readonly<Record<string, string>>;