@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
@@ -1,400 +0,0 @@
1
- import type {
2
- EditFieldSpec,
3
- EditSectionSpec,
4
- FieldIconKey,
5
- FieldRenderer,
6
- IconKey,
7
- ListColumnSpec,
8
- ListFacetSpec,
9
- ListSortSpec,
10
- PlatformComponent,
11
- RelatedListToolbarAction,
12
- RowAction,
13
- ScreenSlots,
14
- SelectOptionTone,
15
- } from "@cosmicdrift/kumiko-framework/ui-types";
16
-
17
- // Runtime-only renderer — function form allowed here because the renderer
18
- // layer (render-list) injects reference-column lookup closures at mount time.
19
- // Never serialized to JSON.
20
- export type RuntimeRenderer = (value: unknown, row?: Readonly<Record<string, unknown>>) => string;
21
-
22
- // View-Models — plain data structures produced by computeListViewModel and
23
- // computeEditViewModel. They flatten the combined [screen-def + entity-def
24
- // + row-data + user] inputs into a shape the renderer draws directly,
25
- // without re-resolving conditions or re-reading the entity field map.
26
- //
27
- // The renderer's render-list.tsx / render-edit.tsx are essentially
28
- // (viewModel) => JSX // on web
29
- // (viewModel) => <View/Text> // on native
30
- // so everything the renderer needs to render one frame must be in here.
31
- //
32
- // Why pre-compute instead of resolving at render-time: predicates
33
- // (visible/readonly/required) are arbitrary JS closures — running them
34
- // inside React's render is wasteful (happens on every re-render, even
35
- // when values haven't changed) and mixes side-effect-free view code
36
- // with business logic. Computing once on form-state-change gives the
37
- // renderer pure data and keeps render paths trivial.
38
-
39
- // --- list view model ---
40
-
41
- // One column, fully resolved. `label` is the localized string the
42
- // renderer puts in the column header; view-model builder runs it through
43
- // LocaleResolver.translate() from the i18nKey wired onto the field.
44
- // `renderer` passes through ScreenDefinition's FieldRenderer verbatim; the
45
- // renderer layer may also inject a RuntimeRenderer closure (reference lookups).
46
- export type ListColumnViewModel = {
47
- readonly field: string;
48
- readonly label: string;
49
- readonly type: string; // field-type ("text", "number", "money", ...)
50
- readonly renderer?: FieldRenderer | RuntimeRenderer;
51
- readonly sortable: boolean;
52
- /** Only for `type: "select"` or `type: "multiSelect"` — translated option
53
- * labels keyed by raw value. The renderer renders `optionLabels[value]`
54
- * instead of humanizeSlug when present. Convention key:
55
- * `<feature>:entity:<entity>:field:<field>:option:<value>`. */
56
- readonly optionLabels?: Readonly<Record<string, string>>;
57
- /** Only for `type: "select"` — declared tone per raw option value. */
58
- readonly optionTones?: Readonly<Partial<Record<string, SelectOptionTone>>>;
59
- /** Nur bei `type: "reference"` — referenced Entity-Name für Bulk-
60
- * Lookup im Renderer (`<refFeature>:query:<refEntity>:list`). */
61
- readonly refEntity?: string;
62
- /** Nur bei `type: "reference"` — Feature-Name in dem die referenced
63
- * Entity wohnt. Default = current feature. Cross-Feature über
64
- * qualifizierte Form ("feature:entity") am ReferenceFieldDef. */
65
- readonly refFeature?: string;
66
- /** Nur bei `type: "reference"` — Welches Feld der referenced Entity
67
- * als Display-Wert in der Cell erscheint (Default "id"). */
68
- readonly refLabelField?: string;
69
- /** Set by the caller at runtime (not schema-derived) to emphasize one
70
- * column — e.g. the currently selected base-year column in a
71
- * multi-year statement grid. DataTable renders it with a distinct
72
- * header/cell background. */
73
- readonly highlighted?: boolean;
74
- /** Mirrors `ListColumnSpec.hideOnNarrow`: card layout skips the column. */
75
- readonly hideOnNarrow?: boolean;
76
- /** Only for `type: "number"` — mirrors `NumberFieldDef.grouping` (default
77
- * `true`). `false` renders without thousands-separators (e.g. a model
78
- * year). */
79
- readonly grouping?: boolean;
80
- };
81
-
82
- export type ListRowViewModel = {
83
- readonly id: string;
84
- readonly values: Readonly<Record<string, unknown>>;
85
- };
86
-
87
- export type ListViewModel = {
88
- readonly screenId: string;
89
- readonly entityName: string;
90
- readonly columns: readonly ListColumnViewModel[];
91
- readonly rows: readonly ListRowViewModel[];
92
- readonly slots?: ScreenSlots;
93
- // Flags for the renderer to decide what kind of container to draw.
94
- readonly isEmpty: boolean;
95
- };
96
-
97
- // --- edit view model ---
98
-
99
- // Per-cell metadata for one sub-field of an embedded-LIST field
100
- // (`multiple: true`) — the column/cell shape a renderer needs to draw an
101
- // invoice-positions-style table. One entry per key of the source
102
- // EmbeddedFieldDef's `schema`.
103
- export type EmbeddedListCellViewModel = {
104
- readonly field: string;
105
- readonly label: string;
106
- readonly type:
107
- | "text"
108
- | "number"
109
- | "boolean"
110
- | "date"
111
- | "money"
112
- | "decimal"
113
- | "select"
114
- | "reference"
115
- | "timestamp";
116
- readonly required: boolean;
117
- /** Only for `type: "select"`. */
118
- readonly options?: readonly string[];
119
- readonly optionLabels?: Readonly<Record<string, string>>;
120
- /** Only for `type: "reference"`. */
121
- readonly refEntity?: string;
122
- readonly refFeature?: string;
123
- readonly refLabelField?: string;
124
- readonly refOptionsQuery?: string;
125
- /** Only for `type: "decimal"`. */
126
- readonly scale?: number;
127
- };
128
-
129
- // Resolved field — all predicates evaluated, labels translated. The
130
- // renderer reads `{ visible, readonly, required }` directly without
131
- // re-running any predicate.
132
- export type EditFieldViewModel = {
133
- readonly field: string;
134
- readonly label: string;
135
- readonly type: string;
136
- readonly value: unknown;
137
- readonly visible: boolean;
138
- // `readOnly` (camelCase) matches the spec-side name on EditFieldSpec —
139
- // one convention through the stack. The TS property modifier `readonly`
140
- // (lowercase) only collides as a key name in type declarations of the
141
- // spec; here in the view-model we could have used either, but symmetric
142
- // naming beats clever ergonomics. Parallel-agent chose `readOnly` on
143
- // the input; we honour it on the output.
144
- readonly readOnly: boolean;
145
- readonly required: boolean;
146
- readonly span?: number;
147
- readonly renderer?: FieldRenderer;
148
- /** Set for `type: "select"` and `type: "multiSelect"` — the allowed
149
- * values. The renderer uses these as dropdown/combobox options.
150
- * Source is SelectFieldDef.options / MultiSelectFieldDef.options on
151
- * the EntityDefinition. */
152
- readonly options?: readonly string[];
153
- /** Set for `type: "select"` and `type: "multiSelect"` — translated
154
- * labels per option, keyed by raw value. The renderer shows
155
- * `optionLabels[value]` as the dropdown/combobox label instead of the
156
- * raw value. Convention key:
157
- * `<feature>:entity:<entity>:field:<field>:option:<value>`. */
158
- readonly optionLabels?: Readonly<Record<string, string>>;
159
- /** Set for `type: "multiSelect"` when MultiSelectFieldDef.display is
160
- * "checkboxes" — the renderer shows a checkbox grid with a select-all
161
- * toggle instead of the default combobox dropdown. Set for
162
- * `type: "select"` when SelectFieldDef.display is "radio" or "dropdown" —
163
- * the renderer then skips its own radio-vs-dropdown heuristic. */
164
- readonly display?: "checkboxes" | "dropdown" | "radio";
165
- /** Set for `type: "multiSelect"` when MultiSelectFieldDef.columns is
166
- * defined — column count for the checkbox grid at the widest breakpoint.
167
- * Ignored unless `display` is "checkboxes". */
168
- readonly columns?: 1 | 2 | 3 | 4;
169
- /** Set for `type: "multiSelect"` when MultiSelectFieldDef.maxRows is
170
- * defined — grid rows visible before the checkbox grid becomes
171
- * scrollable. Ignored unless `display` is "checkboxes". */
172
- readonly maxRows?: number;
173
- /** For `type: "text"`, set when TextFieldDef.multiline is true — the
174
- * renderer then renders a textarea instead of a single-line input.
175
- * `type: "longText"` always renders a textarea regardless of this
176
- * value; here it only carries an optional `{ rows }` override.
177
- * `true` = default rows, `{ rows }` = explicit height. */
178
- readonly multiline?: boolean | { readonly rows?: number };
179
- /** From TextFieldDef.format — `password` makes the renderer mask the input. */
180
- readonly format?: "email" | "url" | "phone" | "password";
181
- /** Nur bei `type: "timestamp"` gesetzt wenn TimestampFieldDef.locatedBy
182
- * existiert — Wall-Clock-Zeit ohne Offset. Der Renderer emittiert
183
- * dann lokale Zeit ohne `Z` statt eines UTC-Instants. */
184
- readonly wallClock?: boolean;
185
- /** Nur bei `type: "date" | "timestamp"` — Datumsgrenzen (ISO) und
186
- * Format/Locale-Override aus der FieldDefinition. Der Renderer begrenzt
187
- * damit den Picker; die Write-Validierung läuft separat über Zod. */
188
- readonly min?: string;
189
- readonly max?: string;
190
- readonly dateLocale?: string;
191
- /** Only for `type: "number"` — mirrors `NumberFieldDef.grouping` (default
192
- * `true`). `false` renders without thousands-separators (e.g. a model
193
- * year). */
194
- readonly grouping?: boolean;
195
- /** Nur bei `type: "reference"` gesetzt — Tier 2.7e-3.
196
- * Die referenced Entity (kurz, ohne feature-prefix). Der Renderer
197
- * baut die Query-QN als `<refFeature>:query:<refEntity>:list`. */
198
- readonly refEntity?: string;
199
- /** Nur bei `type: "reference"` gesetzt — Feature-Name in dem die
200
- * referenced Entity wohnt. Same-feature default = aktuelles
201
- * Feature. Cross-Feature wird über "feature:entity" am
202
- * ReferenceFieldDef.entity erkannt. */
203
- readonly refFeature?: string;
204
- /** Nur bei `type: "reference"` gesetzt — Welches Feld der referenced
205
- * Entity als Display-Label im Dropdown erscheint. Default: "id". */
206
- readonly refLabelField?: string;
207
- /** Only for `type: "reference"` — query handler QN the picker sources its
208
- * options from (fw#2780), replacing the referenced entity's list handler.
209
- * Rows carry `{ id, label }`; `refLabelField` is then unused for the
210
- * picker but still drives list cells. */
211
- readonly refOptionsQuery?: string;
212
- /** Nur bei `type: "reference"` — Multi-Mode (Tier 2.7e-Multi):
213
- * Wert ist UUID-Array, Renderer mountet Multi-Combobox mit Tags. */
214
- readonly refMultiple?: boolean;
215
- /** Nur bei `type: "file" | "image"` — erlaubte Extensions/MIME (z.B.
216
- * ["jpg","png"]) + Max-Größe ("5mb"). Der Renderer setzt das `accept`-
217
- * Attribut + zeigt die Grenze; die Validierung läuft serverseitig im
218
- * Upload-Endpoint gegen die Field-Def. */
219
- readonly accept?: readonly string[];
220
- readonly maxSize?: string;
221
- /** Nur bei `type: "file" | "image"` — Entity-Name + Field-Name für den
222
- * Upload-POST (`/api/files`), damit der Endpoint maxSize/accept gegen
223
- * die richtige Field-Def prüfen kann. */
224
- readonly entityType?: string;
225
- readonly fieldName?: string;
226
- /** Only for `type: "image"` — the variant name the preview requests
227
- * (`/api/files/:id/variant/:name`) instead of the original. The FIRST
228
- * variant declared on the field def wins; a spec may legitimately carry
229
- * neither `size` nor `maxEdge` (format-/blur-only), so "the smallest one"
230
- * is not a rule that always has an answer. Absent when the field declares
231
- * no variants — the preview then loads the original. */
232
- readonly imageVariant?: string;
233
- /** Only for `type: "image"` — forwarded to the file input's `capture`
234
- * attribute so a phone opens the camera instead of the file picker. */
235
- readonly capture?: "environment" | "user";
236
- /** Closed FieldIconKey from EditFieldSpec.icon (FIELD_ICONS registry). */
237
- readonly icon?: FieldIconKey;
238
- /** Only set for an embedded-LIST field (`multiple: true`) — the per-cell
239
- * metadata the renderer needs to draw each column/cell. Absent for a
240
- * plain (non-list) embedded field. */
241
- readonly embeddedListCells?: readonly EmbeddedListCellViewModel[];
242
- /** Minimum row count for the embedded list. From EmbeddedFieldDef.minItems. */
243
- readonly embeddedListMinItems?: number;
244
- /** Maximum row count for the embedded list. From EmbeddedFieldDef.maxItems. */
245
- readonly embeddedListMaxItems?: number;
246
- /** Sub-field name → how to compute its cell from other sub-fields of the
247
- * same row. From EmbeddedFieldDef.derived. */
248
- readonly embeddedListDerived?: Readonly<
249
- Record<
250
- string,
251
- { readonly op: "multiply" | "sum" | "subtract"; readonly from: readonly string[] }
252
- >
253
- >;
254
- /** Numeric sub-field names to sum in a totals row. From EmbeddedFieldDef.totals. */
255
- readonly embeddedListTotals?: readonly string[];
256
- /** Currency for the whole embedded list's money cells and totals row —
257
- * currency lives on the head aggregate (EntityDefinition.defaultCurrency),
258
- * not per row. Falls back to "EUR" when the entity has no defaultCurrency. */
259
- readonly embeddedListCurrency?: string;
260
- /** Only set for a plain (non-list, non-embedded) `type: "money"` field —
261
- * same source as embeddedListCurrency (EntityDefinition.defaultCurrency,
262
- * falls back to "EUR"). The renderer needs this to build the payload's
263
- * `{amount, currency}` shape (kumiko-framework#1923). */
264
- readonly currency?: string;
265
- /** Only set for `type: "number"` when NumberFieldDef.unit is set — a static
266
- * suffix string, or a sibling-field reference the renderer resolves against
267
- * the live row values. Display-only, never converts the stored number. */
268
- readonly unit?: string | { readonly field: string };
269
- };
270
-
271
- // Discriminated by `kind` — mirrors EditSectionSpec on the engine side.
272
- // The builder always emits `kind` explicitly (no defaulting), so the
273
- // renderer narrows with a strict equality check.
274
- export type EditSectionViewModel =
275
- | EditFieldsSectionViewModel
276
- | EditExtensionSectionViewModel
277
- | EditRelatedListSectionViewModel
278
- | EditWriteFormSectionViewModel;
279
-
280
- export type EditFieldsSectionViewModel = {
281
- readonly kind: "fields";
282
- /** From `EditFieldsSection.id`. The boot-validator makes it mandatory under
283
- * `layout.mode: "tabs"` so the tab strip has a stable key per tab; other
284
- * layouts leave it unset. */
285
- readonly id?: string;
286
- /** False when the section has fields but none of them is visible.
287
- * The renderer then draws nothing, but the section stays in the array
288
- * (key stability). */
289
- readonly visible: boolean;
290
- /** Optional — eine titellose Section rendert nur ihre Felder (flache Form). */
291
- readonly title?: string;
292
- /** Translated help text under the block heading, from
293
- * `EditFieldsSection.description`. Renders even without `title`. */
294
- readonly description?: string;
295
- readonly columns: number;
296
- readonly fields: readonly EditFieldViewModel[];
297
- /** From `EditFieldsSection.groups` — each entry's `fields` are the same
298
- * view-model objects as `fields` above, re-grouped (not recomputed). */
299
- readonly groups?: readonly {
300
- readonly title: string;
301
- readonly columns: number;
302
- readonly fields: readonly EditFieldViewModel[];
303
- }[];
304
- /** From `EditFieldsSection.icon` — closed IconKey vocabulary, renders
305
- * left of the title. No effect without `title`. */
306
- readonly icon?: IconKey;
307
- /** From `EditFieldsSection.actions` — rendered in the Section's
308
- * title row, same slot as the projectionDetail head card's actions. */
309
- readonly actions?: readonly RowAction[];
310
- };
311
-
312
- export type EditExtensionSectionViewModel = {
313
- readonly kind: "extension";
314
- /** From `EditExtensionSection.id` — same tab-strip role as on a "fields"
315
- * section, see EditFieldsSectionViewModel.id. */
316
- readonly id?: string;
317
- readonly title: string;
318
- readonly component: PlatformComponent;
319
- readonly contributesToFormSubmit: boolean;
320
- /** Section-declared override — see `EditExtensionSection.entityName`. */
321
- readonly entityName?: string;
322
- /** From `EditExtensionSection.actions`. */
323
- readonly actions?: readonly RowAction[];
324
- };
325
-
326
- // Mirrors EditRelatedListSection verbatim — no per-row/query resolution
327
- // happens in the view-model builder, the renderer's RelatedListSection
328
- // runs the query itself (fw#2166).
329
- export type EditRelatedListSectionViewModel = {
330
- readonly kind: "relatedList";
331
- readonly title: string;
332
- readonly query: string;
333
- readonly parentParam?: string;
334
- readonly parentFilter?: { readonly field: string };
335
- readonly columns: readonly ListColumnSpec[];
336
- readonly description?: string;
337
- /** Untranslated plural-forms key, resolved with the row count by the list footer. */
338
- readonly itemNoun?: string;
339
- readonly pageSize?: number;
340
- readonly defaultSort?: ListSortSpec;
341
- readonly searchable?: boolean;
342
- readonly facets?: readonly ListFacetSpec[];
343
- readonly rowClick?: { readonly entity: string; readonly idColumn?: string };
344
- readonly rowActions?: readonly RowAction[];
345
- readonly toolbarActions?: readonly RelatedListToolbarAction[];
346
- /** From `EditRelatedListSection.actions` — rendered in the
347
- * Section's title row, distinct from `toolbarActions` (which render
348
- * above the table itself). */
349
- readonly actions?: readonly RowAction[];
350
- /** From `EditRelatedListSection.emptyState` — forwarded to the
351
- * DataTable's `emptyState` prop when the section has zero rows. */
352
- readonly emptyState?: {
353
- readonly title: string;
354
- readonly description?: string;
355
- readonly action?: RowAction;
356
- };
357
- };
358
-
359
- // Mirrors EditWriteFormSection, except `fields` is already resolved through
360
- // the same per-field pipeline as EditFieldsSectionViewModel (computed via a
361
- // recursive computeEditViewModel call against the section's own fieldDefs —
362
- // see edit.ts) — the renderer needs no second field-resolution path.
363
- export type EditWriteFormSectionViewModel = {
364
- readonly kind: "writeForm";
365
- readonly title?: string;
366
- readonly description?: string;
367
- readonly columns: number;
368
- readonly fields: readonly EditFieldViewModel[];
369
- readonly icon?: IconKey;
370
- readonly handler: string;
371
- readonly submitLabel?: string;
372
- /** From `EditWriteFormSection.actions`. */
373
- readonly actions?: readonly RowAction[];
374
- };
375
-
376
- export type EditViewModel = {
377
- readonly screenId: string;
378
- readonly entityName: string;
379
- readonly id: string | null; // null on create (no existing row)
380
- readonly sections: readonly EditSectionViewModel[];
381
- readonly slots?: ScreenSlots;
382
- };
383
-
384
- // --- resolver interfaces (host-injected) ---
385
-
386
- // The view-model builder calls this to translate i18n keys into strings.
387
- // Normally the host passes the renderer's LocaleResolver.translate
388
- // directly — keeping the interface narrow here avoids dragging the full
389
- // LocaleResolver (with subscribe) into a pure compute path.
390
- export type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
391
-
392
- // Optional condition context forwarded to field visibility/readonly/required
393
- // predicates. Same ctx the form-controller uses in conditional-fields — so
394
- // a predicate that gated "admin-only" there keeps working here.
395
- export type FieldConditionCtx = unknown;
396
-
397
- // Re-export the ScreenDef spec halves we don't want ui-core callers to
398
- // import from @cosmicdrift/kumiko-framework separately. Renderer packages expect a
399
- // single import surface.
400
- export type { EditFieldSpec, EditSectionSpec, FieldRenderer, ListColumnSpec, ScreenSlots };