@cosmicdrift/kumiko-types 0.327.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 (174) hide show
  1. package/dist/action-icon.d.ts +3 -0
  2. package/dist/action-icon.js +51 -0
  3. package/{src/agent-exposure.ts → dist/agent-exposure.d.ts} +8 -15
  4. package/dist/agent-exposure.js +16 -0
  5. package/dist/concurrency-mode.d.ts +1 -0
  6. package/dist/config-scope.d.ts +1 -0
  7. package/dist/config-scope.js +1 -0
  8. package/dist/config.d.ts +234 -0
  9. package/dist/config.js +1 -0
  10. package/dist/cursor-types.d.ts +16 -0
  11. package/dist/cursor-types.js +1 -0
  12. package/dist/db-connection.d.ts +14 -0
  13. package/dist/db-connection.js +1 -0
  14. package/dist/define-handler.d.ts +58 -0
  15. package/dist/define-handler.js +1 -0
  16. package/dist/derivatives-types.d.ts +53 -0
  17. package/dist/derivatives-types.js +3 -0
  18. package/dist/entity-cache.d.ts +16 -0
  19. package/dist/entity-cache.js +1 -0
  20. package/dist/entity-handlers.d.ts +54 -0
  21. package/dist/entity-handlers.js +1 -0
  22. package/dist/entity-table-meta-types.d.ts +44 -0
  23. package/dist/entity-table-meta-types.js +6 -0
  24. package/dist/envelope-cipher-types.d.ts +5 -0
  25. package/dist/envelope-cipher-types.js +1 -0
  26. package/dist/event-store-executor-types.d.ts +86 -0
  27. package/dist/event-store-executor-types.js +1 -0
  28. package/dist/event-store-types.d.ts +37 -0
  29. package/dist/event-store-types.js +7 -0
  30. package/dist/event-type-map.d.ts +8 -0
  31. package/{src/event-type-map.ts → dist/event-type-map.js} +1 -16
  32. package/dist/executor-brand.d.ts +8 -0
  33. package/dist/executor-brand.js +1 -0
  34. package/dist/extension-options-map.d.ts +9 -0
  35. package/dist/extension-options-map.js +4 -0
  36. package/dist/feature.d.ts +518 -0
  37. package/dist/feature.js +1 -0
  38. package/dist/field-icon.d.ts +1 -0
  39. package/dist/field-icon.js +1 -0
  40. package/dist/fields.d.ts +729 -0
  41. package/dist/fields.js +50 -0
  42. package/dist/file-handle-types.d.ts +12 -0
  43. package/dist/file-handle-types.js +1 -0
  44. package/dist/file-provider-resolver-types.d.ts +3 -0
  45. package/dist/file-provider-resolver-types.js +1 -0
  46. package/dist/file-storage-provider-types.d.ts +17 -0
  47. package/dist/file-storage-provider-types.js +1 -0
  48. package/dist/geo-tz.d.ts +17 -0
  49. package/{src/geo-tz.ts → dist/geo-tz.js} +1 -20
  50. package/dist/handlers.d.ts +493 -0
  51. package/dist/handlers.js +49 -0
  52. package/dist/hook-phase.d.ts +1 -0
  53. package/dist/hook-phase.js +1 -0
  54. package/dist/hooks.d.ts +75 -0
  55. package/dist/hooks.js +1 -0
  56. package/dist/http-route.d.ts +52 -0
  57. package/dist/http-route.js +13 -0
  58. package/dist/identifiers.d.ts +8 -0
  59. package/{src/identifiers.ts → dist/identifiers.js} +7 -24
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.js +1 -0
  62. package/dist/kms-adapter-types.d.ts +68 -0
  63. package/dist/kms-adapter-types.js +49 -0
  64. package/dist/logger.d.ts +7 -0
  65. package/dist/logger.js +1 -0
  66. package/dist/multi-stream-apply-context-types.d.ts +18 -0
  67. package/dist/multi-stream-apply-context-types.js +1 -0
  68. package/dist/nav-icon.d.ts +3 -0
  69. package/dist/nav-icon.js +100 -0
  70. package/dist/nav.d.ts +39 -0
  71. package/dist/nav.js +1 -0
  72. package/dist/observability/index.d.ts +3 -0
  73. package/dist/observability/index.js +2 -0
  74. package/dist/observability/metric.d.ts +34 -0
  75. package/dist/observability/metric.js +3 -0
  76. package/dist/observability/provider.d.ts +24 -0
  77. package/dist/observability/provider.js +4 -0
  78. package/dist/observability/span.d.ts +32 -0
  79. package/dist/observability/span.js +4 -0
  80. package/dist/ownership.d.ts +33 -0
  81. package/dist/ownership.js +5 -0
  82. package/dist/projection.d.ts +38 -0
  83. package/dist/projection.js +1 -0
  84. package/dist/rate-limit-types.d.ts +19 -0
  85. package/dist/rate-limit-types.js +1 -0
  86. package/dist/relations.d.ts +25 -0
  87. package/dist/relations.js +1 -0
  88. package/dist/schema-table-types.d.ts +15 -0
  89. package/{src/schema-table-types.ts → dist/schema-table-types.js} +0 -20
  90. package/dist/screen.d.ts +1265 -0
  91. package/dist/screen.js +3 -0
  92. package/dist/search-adapter.d.ts +32 -0
  93. package/dist/search-adapter.js +1 -0
  94. package/dist/secrets-types.d.ts +52 -0
  95. package/dist/secrets-types.js +30 -0
  96. package/dist/snapshot-types.d.ts +13 -0
  97. package/dist/snapshot-types.js +1 -0
  98. package/dist/step.d.ts +304 -0
  99. package/dist/step.js +11 -0
  100. package/dist/target-ref.d.ts +5 -0
  101. package/{src/target-ref.ts → dist/target-ref.js} +1 -6
  102. package/dist/tenancy-brand.d.ts +6 -0
  103. package/dist/tenancy-brand.js +1 -0
  104. package/dist/tenant-db-types.d.ts +67 -0
  105. package/dist/tenant-db-types.js +5 -0
  106. package/dist/tree-node.d.ts +26 -0
  107. package/dist/tree-node.js +22 -0
  108. package/dist/tz-context.d.ts +48 -0
  109. package/dist/tz-context.js +6 -0
  110. package/dist/websocket-route.d.ts +46 -0
  111. package/dist/websocket-route.js +10 -0
  112. package/dist/where-clause-types.d.ts +20 -0
  113. package/dist/where-clause-types.js +1 -0
  114. package/dist/workspace.d.ts +10 -0
  115. package/dist/workspace.js +1 -0
  116. package/dist/write-error-info-types.d.ts +12 -0
  117. package/dist/write-error-info-types.js +1 -0
  118. package/package.json +372 -124
  119. package/src/changes.json +18 -0
  120. package/src/__tests__/handlers.test.ts +0 -40
  121. package/src/__tests__/plural-forms.test.ts +0 -18
  122. package/src/action-icon.ts +0 -57
  123. package/src/concurrency-mode.ts +0 -5
  124. package/src/config-scope.ts +0 -5
  125. package/src/config.ts +0 -563
  126. package/src/cursor-types.ts +0 -18
  127. package/src/db-connection.ts +0 -25
  128. package/src/define-handler.ts +0 -138
  129. package/src/derivatives-types.ts +0 -106
  130. package/src/entity-cache.ts +0 -35
  131. package/src/entity-handlers.ts +0 -68
  132. package/src/entity-table-meta-types.ts +0 -101
  133. package/src/envelope-cipher-types.ts +0 -6
  134. package/src/event-store-executor-types.ts +0 -163
  135. package/src/event-store-types.ts +0 -85
  136. package/src/executor-brand.ts +0 -19
  137. package/src/extension-options-map.ts +0 -21
  138. package/src/feature.ts +0 -1141
  139. package/src/field-icon.ts +0 -23
  140. package/src/fields.ts +0 -1114
  141. package/src/file-handle-types.ts +0 -20
  142. package/src/file-provider-resolver-types.ts +0 -7
  143. package/src/file-storage-provider-types.ts +0 -54
  144. package/src/handlers.ts +0 -1246
  145. package/src/hook-phase.ts +0 -2
  146. package/src/hooks.ts +0 -170
  147. package/src/http-route.ts +0 -78
  148. package/src/kms-adapter-types.ts +0 -128
  149. package/src/logger.ts +0 -7
  150. package/src/multi-stream-apply-context-types.ts +0 -52
  151. package/src/nav-icon.ts +0 -105
  152. package/src/nav.ts +0 -139
  153. package/src/observability/index.ts +0 -28
  154. package/src/observability/metric.ts +0 -56
  155. package/src/observability/provider.ts +0 -32
  156. package/src/observability/span.ts +0 -58
  157. package/src/ownership.ts +0 -83
  158. package/src/projection.ts +0 -170
  159. package/src/rate-limit-types.ts +0 -32
  160. package/src/relations.ts +0 -52
  161. package/src/screen.ts +0 -1593
  162. package/src/search-adapter.ts +0 -45
  163. package/src/secrets-types.ts +0 -182
  164. package/src/snapshot-types.ts +0 -36
  165. package/src/step.ts +0 -390
  166. package/src/tenancy-brand.ts +0 -9
  167. package/src/tenant-db-types.ts +0 -116
  168. package/src/tree-node.ts +0 -134
  169. package/src/tz-context.ts +0 -63
  170. package/src/websocket-route.ts +0 -68
  171. package/src/where-clause-types.ts +0 -31
  172. package/src/workspace.ts +0 -42
  173. package/src/write-error-info-types.ts +0 -20
  174. /package/{src/index.ts → dist/concurrency-mode.js} +0 -0
@@ -0,0 +1,1265 @@
1
+ import type { FieldIconKey } from "./field-icon.js";
2
+ import type { FieldDefinition, FormFieldDefinition, SelectOptionTone } from "./fields.js";
3
+ import type { AccessRule, AgentHandlerHints } from "./handlers.js";
4
+ import type { IconKey, NavIconKey } from "./nav-icon.js";
5
+ export type { FieldIconKey } from "./field-icon.js";
6
+ export type PlatformComponent = {
7
+ readonly react?: unknown;
8
+ readonly native?: unknown;
9
+ };
10
+ export type UnitKey = "m2" | "km" | "m" | "kg" | "percent" | "mi";
11
+ export interface FieldFormatRegistry {
12
+ timestamp: {
13
+ readonly locale?: string;
14
+ readonly dateStyle?: "full" | "long" | "medium" | "short";
15
+ readonly timeStyle?: "full" | "long" | "medium" | "short";
16
+ };
17
+ date: {
18
+ readonly locale?: string;
19
+ readonly dateStyle?: "full" | "long" | "medium" | "short";
20
+ };
21
+ boolean: {
22
+ readonly trueLabel?: string;
23
+ readonly falseLabel?: string;
24
+ };
25
+ currency: {
26
+ readonly symbol?: string;
27
+ };
28
+ priority: {
29
+ readonly emptyLabel?: string;
30
+ readonly prefix?: string;
31
+ };
32
+ /** `grouping: false` renders without thousands-separators (e.g. a model
33
+ * year: "2021", not "2.021"). Default `true`. */
34
+ number: {
35
+ readonly locale?: string;
36
+ readonly grouping?: boolean;
37
+ };
38
+ decimal: {
39
+ readonly locale?: string;
40
+ readonly grouping?: boolean;
41
+ };
42
+ bigInt: {
43
+ readonly locale?: string;
44
+ readonly grouping?: boolean;
45
+ };
46
+ unit: {
47
+ readonly unit: UnitKey;
48
+ readonly locale?: string;
49
+ readonly unitDisplay?: "long" | "short" | "narrow";
50
+ };
51
+ enumOption: {
52
+ readonly keyPrefix: string;
53
+ };
54
+ /** JSON-safe structured value rendered as indented text. */
55
+ json: {
56
+ readonly indent?: number;
57
+ };
58
+ }
59
+ export type FormatSpec = {
60
+ [K in keyof FieldFormatRegistry]: {
61
+ readonly format: K;
62
+ } & FieldFormatRegistry[K];
63
+ }[keyof FieldFormatRegistry];
64
+ export type FieldRenderer = PlatformComponent | string | FormatSpec;
65
+ export type FieldCondition = boolean | {
66
+ readonly field: string;
67
+ readonly eq: unknown;
68
+ } | {
69
+ readonly field: string;
70
+ readonly ne: unknown;
71
+ } | {
72
+ readonly field: string;
73
+ readonly in: readonly unknown[];
74
+ } | {
75
+ readonly field: string;
76
+ readonly notIn: readonly unknown[];
77
+ };
78
+ export type ListColumnSpec = string | {
79
+ readonly field: string;
80
+ readonly renderer?: FieldRenderer;
81
+ /** Optional header, overriding the default
82
+ * `<feature>:entity:<entity>:field:<field>` i18n convention. Resolved
83
+ * through `translate` like any header — an i18n key, or a plain literal
84
+ * shown verbatim if it isn't a key. Also the way to declare a *virtual*
85
+ * column whose `field` is NOT an entity field — a presentational column
86
+ * drawn entirely by a `columnRenderer` component that reads the `row`
87
+ * (e.g. a tag-chips cell), not a stored value. Such a column needs a
88
+ * label; without one it is rejected at boot as an unknown field. `field`
89
+ * is then just a stable column key (pick any unique slug). */
90
+ readonly label?: string;
91
+ /** Marks this column as a reference lookup instead of a plain value —
92
+ * for projectionList/relatedList columns, which have no
93
+ * `EntityDefinition` to carry a real `reference` field type. Same
94
+ * target convention as `parseRefTarget`: an entity name (same
95
+ * feature) or `feature:entity` (cross-feature). Ignored on
96
+ * `entityList` columns, where the field's own declared type always
97
+ * wins. */
98
+ readonly refEntity?: string;
99
+ /** Row field on the referenced entity shown as the label (default
100
+ * "id"). Only meaningful together with `refEntity`. */
101
+ readonly refLabelField?: string;
102
+ /** Marks this column as sortable via a clickable header — for
103
+ * relatedList columns, which have no `EntityDefinition` field to carry
104
+ * `sortable` on. Ignored on `entityList` columns, where the entity
105
+ * field's own `sortable` flag wins. */
106
+ readonly sortable?: boolean;
107
+ /** Value type of a relatedList column, which has no `EntityDefinition`
108
+ * field to derive it from. Numeric types right-align the column and its
109
+ * header with tabular figures, like an entityList column of that field
110
+ * type. Ignored on entityList columns, where the field's own type wins. */
111
+ readonly valueType?: "number" | "decimal" | "bigInt" | "money";
112
+ /** Leaves the column out of the compact card layout used below the
113
+ * `md` breakpoint (tables keep it). For columns that only make sense
114
+ * next to the others, like a sort key. */
115
+ readonly hideOnNarrow?: boolean;
116
+ };
117
+ export type ListPaginationMode = "pages" | "infinite" | false;
118
+ export type ListSortDir = "asc" | "desc";
119
+ export type ListSortSpec = {
120
+ readonly field: string;
121
+ readonly dir: ListSortDir;
122
+ };
123
+ export type ScreenFilterOp = "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
124
+ export type ScreenFilter = {
125
+ readonly field: string;
126
+ readonly op: ScreenFilterOp;
127
+ readonly value: unknown;
128
+ };
129
+ /** Declarative row-field extractor — JSON-safe (no function prop, survives
130
+ * GET /api/schema / buildAppSchema).
131
+ *
132
+ * `pick`: extracts fields 1:1. `{ pick: ["id", "version"] }` → `{ id: row.id, version: row.version }`
133
+ * `map`: renames fields. `{ map: { incidentId: "id" } }` → `{ incidentId: row.id }`
134
+ *
135
+ * Limitation: computed/templated values can't be expressed this way —
136
+ * that logic belongs server-side in the write handler. */
137
+ export type RowFieldExtractor = {
138
+ readonly pick: readonly string[];
139
+ } | {
140
+ readonly map: Readonly<Record<string, string>>;
141
+ };
142
+ export type RowAction = RowActionWriteHandler | RowActionNavigate | RowActionDrawer;
143
+ export type RowActionWriteHandler = {
144
+ /** Default für RowActions ohne explizit gesetzten `kind` —
145
+ * Backwards-kompatible Form. Kann auch explizit gesetzt werden. */
146
+ readonly kind?: "writeHandler";
147
+ /** Stable id pro Screen — kebab-case, eindeutig im Action-Set. */
148
+ readonly id: string;
149
+ /** Anzeige-Text (i18n-Key). */
150
+ readonly label: string;
151
+ /** Qualified-Name des Server-Handlers, z.B.
152
+ * "publicstatus:write:maintenance:start". Wird via useDispatcher
153
+ * dispatcht. */
154
+ readonly handler: string;
155
+ /** Deklarativer Payload pro Row. Default = `{ id: row.id }`.
156
+ * `pick` extrahiert Felder gleichen Namens; `map` benennt um. */
157
+ readonly payload?: RowFieldExtractor;
158
+ /** i18n-Key für die Confirm-Dialog-Description. Wenn gesetzt, öffnet
159
+ * ein Modal vor der Ausführung — der User muss explizit bestätigen.
160
+ * Zusammen mit `style: "danger"` ist das die Standard-Sicherheits-
161
+ * Garde für destruktive Aktionen. */
162
+ readonly confirm?: string;
163
+ /** i18n-Key für den Confirm-Button-Text im Dialog. Default = `label`
164
+ * (also "Delete" → "Delete"-Button im Confirm). Setzen wenn die
165
+ * Action einen langen Namen hat ("Mark Subscription as Cancelled")
166
+ * und der Button kürzer sein soll ("Cancel Subscription"). */
167
+ readonly confirmLabel?: string;
168
+ /** Conditional Visibility pro Row. */
169
+ readonly visible?: FieldCondition;
170
+ /** Visual-Style. "danger" rendert rot + erzwingt einen Confirm-
171
+ * Dialog (auch ohne expliziten `confirm`-Key). */
172
+ readonly style?: "primary" | "secondary" | "danger";
173
+ /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
174
+ * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
175
+ * (renderer-web), analogous to EditFieldSpec.icon. */
176
+ readonly icon?: IconKey;
177
+ };
178
+ export type RowActionNavigateBase = {
179
+ readonly kind: "navigate";
180
+ readonly id: string;
181
+ readonly label: string;
182
+ /** Feldname dessen Wert als entityId in den URL-Pfad eingebettet wird
183
+ * (`/<workspace>/<screen>/<entityId>`). entityEdit liest die Id
184
+ * AUSSCHLIESSLICH aus dem Pfad. Default: "id" wenn der Ziel-Screen ein
185
+ * entityEdit ist ODER wenn `entity` (statt `screen`) gesetzt ist. */
186
+ readonly entityId?: string;
187
+ /** Declarative URL search params extracted from the clicked row.
188
+ * Both actionForm and entityEdit-create targets read `params` as
189
+ * initial values (pre-filling the form). entityEdit-update does
190
+ * not — the existing record's values take precedence.
191
+ * `pick` extracts fields of the same name; `map` renames them. */
192
+ readonly params?: RowFieldExtractor;
193
+ /** Section id of the tab to activate on the target projectionDetail
194
+ * (layout.mode "tabs"); merged into the navigate search params next to
195
+ * returnTo. Boot validator checks the id exists on the target. */
196
+ readonly tab?: string;
197
+ /** Conditional Visibility pro Row. */
198
+ readonly visible?: FieldCondition;
199
+ /** Visual style only — unlike the writeHandler variants, "danger" does not
200
+ * force a confirm dialog here: the target form is the confirmation. */
201
+ readonly style?: "primary" | "secondary" | "danger";
202
+ /** Wenn true, löst ein Klick auf die ganze Zeile (nicht nur das Aktionsmenü)
203
+ * diese navigate-Action aus. Max. eine pro Liste (Boot-Validator prüft). Nur
204
+ * auf navigate — ein Row-Klick darf keinen (evtl. destruktiven, unbestätigten)
205
+ * Write auslösen, daher nicht auf writeHandler-Actions. */
206
+ readonly rowClick?: boolean;
207
+ /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
208
+ * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
209
+ * (renderer-web), analogous to EditFieldSpec.icon. */
210
+ readonly icon?: IconKey;
211
+ };
212
+ /** Exactly one of `screen` / `entity` — mutual exclusivity is type-enforced
213
+ * (boot validator still rejects both/neither for untyped schemas). */
214
+ export type RowActionNavigate = RowActionNavigateBase & ({
215
+ /** Screen-id (kurz, unqualified). Boot-Validator prüft Existenz im
216
+ * selben Feature. */
217
+ readonly screen: string;
218
+ readonly entity?: never;
219
+ } | {
220
+ /** Entity-Name statt Screen-Id (fw#2228) — löst zur Boot-Zeit auf den
221
+ * Screen auf, der `detailFor: "<entity>"` deklariert (siehe ObjectTarget/
222
+ * resolveTarget in @cosmicdrift/kumiko-renderer's nav.tsx). */
223
+ readonly entity: string;
224
+ readonly screen?: never;
225
+ });
226
+ export type RowActionDrawer = {
227
+ readonly kind: "drawer";
228
+ readonly id: string;
229
+ readonly label: string;
230
+ /** Short, unqualified id of an `actionForm` screen in the same feature.
231
+ * Boot validator rejects a missing screen and a screen whose `type`
232
+ * isn't `actionForm` — same check as ToolbarAction's drawer variant
233
+ * (see docs/plans/bundled-features-screen-standardisierung.md §2.6c). */
234
+ readonly screen: string;
235
+ /** Declarative prefill extracted from the clicked row. `pick` extracts
236
+ * fields of the same name; `map` renames them. */
237
+ readonly params?: RowFieldExtractor;
238
+ /** Per-row conditional visibility. */
239
+ readonly visible?: FieldCondition;
240
+ /** Visual style only — unlike the writeHandler variants, "danger" does not
241
+ * force a confirm dialog here: the target form is the confirmation. */
242
+ readonly style?: "primary" | "secondary" | "danger";
243
+ /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
244
+ * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
245
+ * (renderer-web), analogous to EditFieldSpec.icon. */
246
+ readonly icon?: IconKey;
247
+ };
248
+ export type ToolbarAction = {
249
+ readonly kind: "navigate";
250
+ readonly id: string;
251
+ readonly label: string;
252
+ /** Screen-id (kurz, unqualified) zu dem navigiert wird. */
253
+ readonly screen: string;
254
+ /** Section id of the tab to activate on the target projectionDetail
255
+ * (layout.mode "tabs"); merged into the navigate search params next to
256
+ * returnTo. Boot validator checks the id exists on the target. */
257
+ readonly tab?: string;
258
+ /** Visual style only — unlike the writeHandler variant, "danger" does
259
+ * not force a confirm dialog here: the target form is the
260
+ * confirmation. */
261
+ readonly style?: "primary" | "secondary" | "danger";
262
+ } | {
263
+ readonly kind: "writeHandler";
264
+ readonly id: string;
265
+ readonly label: string;
266
+ readonly handler: string;
267
+ /** Statischer Payload ohne row-Context. Default = `{}`. */
268
+ readonly payload?: Record<string, unknown>;
269
+ /** i18n-Key für Confirm-Dialog-Description. Wenn gesetzt UND/ODER
270
+ * style="danger": Modal vor der Ausführung. */
271
+ readonly confirm?: string;
272
+ /** i18n-Key für Confirm-Button-Text im Dialog. Default = `label`. */
273
+ readonly confirmLabel?: string;
274
+ readonly style?: "primary" | "secondary" | "danger";
275
+ } | {
276
+ readonly kind: "drawer";
277
+ readonly id: string;
278
+ readonly label: string;
279
+ /** Short, unqualified id of an `actionForm` screen in the same
280
+ * feature. Boot validator rejects a missing screen and a screen
281
+ * whose `type` isn't `actionForm` — the caller decides full-page
282
+ * vs. drawer, not the form (see docs/plans/bundled-features-screen-
283
+ * standardisierung.md §2.6c). */
284
+ readonly screen: string;
285
+ /** Visual style only — unlike the writeHandler variant, "danger" does
286
+ * not force a confirm dialog here: the target form is the
287
+ * confirmation. */
288
+ readonly style?: "primary" | "secondary" | "danger";
289
+ };
290
+ export type RelatedListToolbarAction = (Extract<ToolbarAction, {
291
+ readonly kind: "navigate";
292
+ }> & {
293
+ /** Conditional visibility, evaluated against the relatedList's parent
294
+ * record (the "Akte"). */
295
+ readonly visible?: FieldCondition;
296
+ /** Declarative URL search params extracted from the parent record,
297
+ * prefilling the target screen. Replaces the implicit
298
+ * `{ [parentParam]: parentId }` default when set. */
299
+ readonly params?: RowFieldExtractor;
300
+ }) | (Extract<ToolbarAction, {
301
+ readonly kind: "writeHandler";
302
+ }> & {
303
+ readonly visible?: FieldCondition;
304
+ }) | (Extract<ToolbarAction, {
305
+ readonly kind: "drawer";
306
+ }> & {
307
+ readonly visible?: FieldCondition;
308
+ });
309
+ export type EntityListScreenDefinition = {
310
+ readonly id: string;
311
+ readonly type: "entityList";
312
+ /** Default true: the screen fills the shell content height, its body scrolls
313
+ * inside and the pager / action bar stays pinned. `false` restores page
314
+ * scroll (table grows with its rows, pager below the last row, form footer
315
+ * in the flow). */
316
+ readonly fillHeight?: boolean;
317
+ readonly nav?: ScreenNavSugar;
318
+ readonly detailFor?: string;
319
+ readonly description?: string;
320
+ readonly agent?: AgentHandlerHints;
321
+ /** Screen has no nav entry by design (opened via link or navved by the
322
+ * app); exempts it from the nav-area boot check. */
323
+ readonly dormant?: boolean;
324
+ readonly entity: string;
325
+ readonly columns: readonly ListColumnSpec[];
326
+ readonly rowRenderer?: PlatformComponent;
327
+ readonly cardRenderer?: PlatformComponent;
328
+ /** Per-Row-Aktionen — rendert eine Actions-Spalte rechts in der Tabelle.
329
+ * Bis zu 2 actions als inline-Buttons; >2 als Kebab-Dropdown.
330
+ * Reihenfolge im Array = Reihenfolge in der UI. */
331
+ readonly rowActions?: readonly RowAction[];
332
+ /** Toolbar-Aktionen (List-Header). "Open Incident", "Schedule Maintenance"
333
+ * etc. — neben "+ Neu" wenn vorhanden. Reihenfolge im Array = UI-
334
+ * Reihenfolge, primary-style links. */
335
+ readonly toolbarActions?: readonly ToolbarAction[];
336
+ /** Server-side Filter, fest am Screen — drei Buckets derselben
337
+ * Entity ohne Custom-Pages (z.B. "Upcoming" / "Active" / "Past"
338
+ * Maintenance). User-side q-Search läuft AUF dem gefilterten Set
339
+ * oben drauf. Boot-Validator pinst dass field in der Entity existiert. */
340
+ readonly filter?: ScreenFilter;
341
+ readonly pagination?: ListPaginationMode;
342
+ readonly pageSize?: number;
343
+ readonly defaultSort?: ListSortSpec;
344
+ /** i18n key for the primary create button label (default `kumiko.actions.create`). */
345
+ readonly createLabel?: string;
346
+ /** i18n key for the search field placeholder (default `kumiko.list.search-placeholder`). */
347
+ readonly searchPlaceholder?: string;
348
+ readonly searchable?: boolean;
349
+ readonly slots?: ScreenSlots;
350
+ readonly access?: AccessRule;
351
+ };
352
+ export type ListFacetSpec = {
353
+ readonly field: string;
354
+ readonly type: "select";
355
+ readonly label: string;
356
+ readonly options: readonly {
357
+ readonly value: string;
358
+ readonly label: string;
359
+ }[];
360
+ } | {
361
+ readonly field: string;
362
+ readonly type: "boolean";
363
+ readonly label: string;
364
+ readonly trueLabel: string;
365
+ readonly falseLabel: string;
366
+ } | {
367
+ readonly field: string;
368
+ readonly type: "reference";
369
+ readonly label: string;
370
+ /** Entity name (same feature) or `feature:entity` (cross-feature), same
371
+ * convention as `ListColumnSpec.refEntity`. Options load at render time. */
372
+ readonly entity: string;
373
+ /** Row field on the referenced entity shown as the option label
374
+ * (default "id"). */
375
+ readonly labelField?: string;
376
+ } | {
377
+ /** Declarative only — the date field the range narrows. It is NOT sent
378
+ * to the server (the bounds go under `params`); it namespaces the
379
+ * facet's URL keys and tells a reading agent which column this list
380
+ * is time-rangeable on. */
381
+ readonly field: string;
382
+ readonly type: "dateRange";
383
+ readonly label: string;
384
+ /** Query-payload keys the chosen bounds are sent under as ISO instants.
385
+ * Explicit rather than a `from`/`to` convention — a query is free to
386
+ * name its time bounds anything, and the boot-validator checks these
387
+ * keys against the handler's Zod schema. */
388
+ readonly params: {
389
+ readonly from: string;
390
+ readonly to: string;
391
+ };
392
+ };
393
+ export type ProjectionListScreenDefinition = {
394
+ readonly id: string;
395
+ readonly type: "projectionList";
396
+ /** Default true: the screen fills the shell content height, its body scrolls
397
+ * inside and the pager / action bar stays pinned. `false` restores page
398
+ * scroll (table grows with its rows, pager below the last row, form footer
399
+ * in the flow). */
400
+ readonly fillHeight?: boolean;
401
+ readonly nav?: ScreenNavSugar;
402
+ readonly detailFor?: string;
403
+ readonly description?: string;
404
+ readonly agent?: AgentHandlerHints;
405
+ /** Screen has no nav entry by design (opened via link or navved by the
406
+ * app); exempts it from the nav-area boot check. */
407
+ readonly dormant?: boolean;
408
+ readonly query: string;
409
+ readonly columns: readonly ListColumnSpec[];
410
+ readonly rowRenderer?: PlatformComponent;
411
+ readonly cardRenderer?: PlatformComponent;
412
+ readonly rowActions?: readonly RowAction[];
413
+ readonly toolbarActions?: readonly ToolbarAction[];
414
+ readonly pagination?: ListPaginationMode;
415
+ readonly pageSize?: number;
416
+ readonly defaultSort?: ListSortSpec;
417
+ /** i18n key for the primary create button label (default `kumiko.actions.create`). */
418
+ readonly createLabel?: string;
419
+ /** i18n key for the search field placeholder (default `kumiko.list.search-placeholder`). */
420
+ readonly searchPlaceholder?: string;
421
+ readonly searchable?: boolean;
422
+ /** Derived by buildAppSchema from the query handler's Zod schema (`sort`
423
+ * param present). Same type serves author and wire schema (no separate
424
+ * client-facing screen type exists), so this stays structurally
425
+ * writable — validateProjectionListScreens rejects any hand-authored
426
+ * value at boot (fw#2165). */
427
+ readonly sortable?: boolean;
428
+ /** Derived by buildAppSchema from the query handler's Zod schema (`cursor`
429
+ * or `offset` param present). See `sortable` doc for why this is
430
+ * boot-enforced rather than type-enforced. */
431
+ readonly paginated?: boolean;
432
+ /** Server-side filter, fixed on the screen — see `ScreenFilter` doc above
433
+ * (entityList). Field existence can't be checked without an entity; the
434
+ * boot-validator only checks structure (`op:"in"` ⇒ array value). */
435
+ readonly filter?: ScreenFilter;
436
+ /** User-toggleable facet dropdowns — see `ListFacetSpec` doc. `field`
437
+ * must be a declared column; the bound query handler must accept
438
+ * `filters` in its Zod schema — the boot-validator checks both. A
439
+ * "dateRange" facet instead needs its two `params` keys in that schema;
440
+ * it sends no `filters` entry. */
441
+ readonly facets?: readonly ListFacetSpec[];
442
+ readonly slots?: ScreenSlots;
443
+ readonly access?: AccessRule;
444
+ };
445
+ export type RecordHeaderSpec = {
446
+ readonly title: string;
447
+ readonly subtitle?: string;
448
+ readonly status?: string;
449
+ /** Record field holding an absolute http(s) URL. When present and the
450
+ * field's value is such a URL, the subtitle renders as an external link
451
+ * (`target="_blank"`) instead of plain text. */
452
+ readonly subtitleHref?: string;
453
+ /** Badge tone per `status` field value; unlisted values fall back to the value heuristic. */
454
+ readonly statusTones?: {
455
+ readonly [statusValue: string]: SelectOptionTone;
456
+ };
457
+ };
458
+ export type MetricNavigate = {
459
+ readonly screen?: string;
460
+ readonly entity?: string;
461
+ readonly entityId?: string;
462
+ readonly params?: RowFieldExtractor;
463
+ /** Section id of the tab to activate at the destination. Alone (no
464
+ * `screen`/`entity`), it activates that tab on the current record
465
+ * instead of navigating away. */
466
+ readonly tab?: string;
467
+ };
468
+ export type MetricSpec = string | {
469
+ readonly field: string;
470
+ readonly label?: string;
471
+ readonly navigate?: MetricNavigate;
472
+ };
473
+ export declare function metricField(metric: MetricSpec): string;
474
+ export type ProjectionDetailScreenDefinition = {
475
+ readonly id: string;
476
+ readonly type: "projectionDetail";
477
+ /** Default true: the screen fills the shell content height, its body scrolls
478
+ * inside and the pager / action bar stays pinned. `false` restores page
479
+ * scroll (table grows with its rows, pager below the last row, form footer
480
+ * in the flow). */
481
+ readonly fillHeight?: boolean;
482
+ readonly nav?: ScreenNavSugar;
483
+ readonly detailFor?: string;
484
+ readonly description?: string;
485
+ readonly agent?: AgentHandlerHints;
486
+ /** Screen has no nav entry by design (opened via link or navved by the
487
+ * app); exempts it from the nav-area boot check. */
488
+ readonly dormant?: boolean;
489
+ readonly query: string;
490
+ /** Query-payload key for the row-id. Default "id". */
491
+ readonly idParam?: string;
492
+ /** The server determines the shown row from the caller's session/context
493
+ * instead of a row id in the path — a self-service screen ("my profile",
494
+ * "my data") that has no row to link to (unlike `EntityEditScreenDefinition.
495
+ * singleton`, which still resolves via `<entity>:list` limit 1; here the
496
+ * query itself owns the row selection, e.g. `user:query:user:me`). The
497
+ * query is called WITHOUT the `idParam` key — there is no id to send, so
498
+ * the boot-validator rejects declaring `idParam` alongside this flag
499
+ * instead of letting one silently win. Stricter than an id-addressed
500
+ * screen, not looser: the server picks the row, so no client-supplied id
501
+ * can reach the query — a stray id on the path is ignored, never
502
+ * forwarded. */
503
+ readonly singleton?: boolean;
504
+ readonly layout: EditLayout;
505
+ /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key), analog zu
506
+ * entityEdit.fieldLabels. Die Pseudo-Entity `__projection-detail__` hat
507
+ * keinen natürlichen Field-Namespace — fehlt ein Eintrag, gilt die
508
+ * Konvention `<feature>:entity:__projection-detail__:field:<name>`. */
509
+ readonly fieldLabels?: Readonly<Record<string, string>>;
510
+ /** Record header above the layout — title/subtitle/status name columns
511
+ * in the query row (not literals). Rendered only when set. */
512
+ readonly header?: RecordHeaderSpec;
513
+ /** Metric band above the layout — column names in the query row, labeled
514
+ * via `fieldLabels` (string shorthand) or the object form's own `label`.
515
+ * Rendered only when set. */
516
+ readonly metrics?: readonly MetricSpec[];
517
+ /** Parent list screen (kurze id) für eine "Zurück"-Navigation. */
518
+ readonly listScreenId?: string;
519
+ readonly slots?: ScreenSlots;
520
+ readonly access?: AccessRule;
521
+ /** Header action buttons. Reuses `RowAction` — the screen's single
522
+ * displayed record stands in for the "row", so `pick`/`map`, `visible`,
523
+ * `confirm`/`confirmLabel` and `style` keep their meaning unchanged.
524
+ * `rowClick` has no target here (there is no row to click) and is
525
+ * rejected by the boot-validator. */
526
+ readonly actions?: readonly RowAction[];
527
+ /** How head fields render. Default "text" — plain text instead of a
528
+ * disabled Input, since every field on this screen type is forced
529
+ * readOnly anyway (there is no write path, see the shim doc above).
530
+ * Set "form" to opt back into the disabled-Input look (fw#2245). */
531
+ readonly valueDisplay?: "form" | "text";
532
+ };
533
+ export type DashboardStatPanel = {
534
+ readonly kind: "stat";
535
+ /** Stable id — kebab-case, eindeutig im Panel-Set. */
536
+ readonly id: string;
537
+ /** Anzeige-Text (i18n-Key). */
538
+ readonly label: string;
539
+ readonly query: string;
540
+ /** Static, author-set query parameters merged on top of the screen's
541
+ * dynamic filterParams — lets a panel pin a value (e.g. a status facet)
542
+ * the screen-wide filter doesn't cover, without needing its own query. */
543
+ readonly params?: Readonly<Record<string, unknown>>;
544
+ readonly valueField: string;
545
+ readonly subField?: string;
546
+ readonly toneField?: string;
547
+ /** Optionaler Delta-Chip (z.B. "↓23 %") neben dem Label. Nur wenn BEIDE
548
+ * Felder gesetzt sind UND der Query-Handler sie liefert, rendert der Chip
549
+ * — sonst bleibt die Kachel wie ohne Delta. `deltaToneField` fällt auf
550
+ * `toneField`/"default" zurück, wenn ungesetzt. */
551
+ readonly deltaField?: string;
552
+ readonly deltaDirectionField?: string;
553
+ readonly deltaToneField?: string;
554
+ /** Statisches Icon neben dem Label — anders als value/sub/delta variiert
555
+ * das Icon nicht pro Query-Result, sondern ist eine Author-Entscheidung
556
+ * wie das Panel selbst. Aufgelöst über dieselbe extensionSectionComponents-
557
+ * Registry wie custom-Panels; die registrierte Komponente ignoriert
558
+ * typischerweise entityName/entityId/filterParams (kein Entity-Kontext
559
+ * für ein reines Icon). */
560
+ readonly icon?: PlatformComponent;
561
+ /** Statischer CSS-Farbwert (z.B. "var(--color-debt)") für den Icon-Chip —
562
+ * Passthrough an die Kachel, keine Registry, kein Lookup. Wirkt NUR wenn
563
+ * `icon` gesetzt ist (StatCard rendert den Chip nur zusammen mit einem
564
+ * Icon) — ohne icon wird der Wert still verworfen. */
565
+ readonly accentColor?: string;
566
+ };
567
+ export type DashboardChartPanel = {
568
+ readonly kind: "chart";
569
+ readonly id: string;
570
+ readonly label: string;
571
+ /** v1: geglättete Zeitreihe. Weitere Chart-Formen additiv. */
572
+ readonly chart: "timeseries";
573
+ readonly query: string;
574
+ };
575
+ export type DashboardListPanel = {
576
+ readonly kind: "list";
577
+ readonly id: string;
578
+ readonly label: string;
579
+ readonly query: string;
580
+ readonly columns: readonly ListColumnSpec[];
581
+ };
582
+ export type DashboardStatGroupPanel = {
583
+ readonly kind: "stat-group";
584
+ readonly id: string;
585
+ readonly label: string;
586
+ readonly stats: readonly DashboardStatPanel[];
587
+ };
588
+ export type DashboardFeedPanel = {
589
+ readonly kind: "feed";
590
+ readonly id: string;
591
+ readonly label: string;
592
+ readonly query: string;
593
+ readonly emptyLabel?: string;
594
+ };
595
+ export type DashboardProgressListPanel = {
596
+ readonly kind: "progress-list";
597
+ readonly id: string;
598
+ readonly label: string;
599
+ readonly query: string;
600
+ };
601
+ export type DashboardCustomPanel = {
602
+ readonly kind: "custom";
603
+ readonly id: string;
604
+ readonly component: PlatformComponent;
605
+ };
606
+ export type DashboardPanelVisibility = {
607
+ readonly query: string;
608
+ readonly field: string;
609
+ readonly eq: string | number | boolean | null;
610
+ };
611
+ export type DashboardScreenPanel = {
612
+ readonly kind: "screen";
613
+ readonly id: string;
614
+ /** Same-feature short id or cross-feature QN `<feature>:screen:<id>`. */
615
+ readonly screen: string;
616
+ readonly label?: string;
617
+ readonly visibleWhen?: DashboardPanelVisibility;
618
+ };
619
+ export type DashboardPanelDefinition = DashboardStatPanel | DashboardStatGroupPanel | DashboardChartPanel | DashboardListPanel | DashboardFeedPanel | DashboardProgressListPanel | DashboardCustomPanel | DashboardScreenPanel;
620
+ export type DashboardFilterDefinition = {
621
+ readonly id: string;
622
+ readonly label: string;
623
+ readonly kind: "select";
624
+ readonly placeholder?: string;
625
+ /** i18n-Key für den "(alle)"-Eintrag. */
626
+ readonly allLabel?: string;
627
+ readonly options?: readonly {
628
+ readonly value: string;
629
+ readonly label: string;
630
+ }[];
631
+ /** Query-Result-Contract: `{ rows: { value: string; label: string }[] }`. */
632
+ readonly optionsQuery?: string;
633
+ };
634
+ export type DashboardScreenDefinition = {
635
+ readonly id: string;
636
+ readonly type: "dashboard";
637
+ readonly nav?: ScreenNavSugar;
638
+ readonly detailFor?: string;
639
+ readonly description?: string;
640
+ readonly agent?: AgentHandlerHints;
641
+ /** Screen has no nav entry by design (opened via link or navved by the
642
+ * app); exempts it from the nav-area boot check. */
643
+ readonly dormant?: boolean;
644
+ readonly panels: readonly DashboardPanelDefinition[];
645
+ readonly filter?: DashboardFilterDefinition;
646
+ readonly slots?: ScreenSlots;
647
+ readonly access?: AccessRule;
648
+ };
649
+ export type EditFieldSpec = string | {
650
+ readonly field: string;
651
+ readonly span?: number;
652
+ readonly visible?: FieldCondition;
653
+ readonly readOnly?: FieldCondition;
654
+ readonly required?: FieldCondition;
655
+ readonly renderer?: FieldRenderer;
656
+ /** Prefix icon on the input — closed FieldIconKey vocabulary into
657
+ * the FIELD_ICONS registry (renderer-web), analogous to
658
+ * `ScreenNavSugar.icon` / NavIconKey. Only takes effect for
659
+ * single-line `type: "text"` and `"number"` — a `text` field with
660
+ * `multiline` routes to a textarea (no icon slot), and other input
661
+ * kinds (select, combobox, date, …) silently ignore the key. */
662
+ readonly icon?: FieldIconKey;
663
+ /** Marks this field as a reference lookup instead of a plain value —
664
+ * for projectionDetail fields, which have no `EntityDefinition` to
665
+ * carry a real `reference` field type. Same target convention as
666
+ * `parseRefTarget` (and `ListColumnSpec.refEntity`): an entity name
667
+ * (same feature) or `feature:entity` (cross-feature). Ignored on
668
+ * `entityEdit` fields, where the field's own declared type always
669
+ * wins. */
670
+ readonly refEntity?: string;
671
+ /** Row field on the referenced entity shown as the label (default
672
+ * "id"). Only meaningful together with `refEntity`. */
673
+ readonly refLabelField?: string;
674
+ };
675
+ export type EditSectionSpec = EditFieldsSection | EditExtensionSection | EditRelatedListSection | EditWriteFormSection;
676
+ export type EditFieldsSection = {
677
+ /** Kebab-case tab id, used as the `?tab=` value. Required by the
678
+ * boot-validator when the enclosing `EditLayout.mode` is "tabs". */
679
+ readonly id?: string;
680
+ readonly kind?: "fields";
681
+ /** Optional. Ohne Titel rendert die Section nur ihre Felder (keine h3-
682
+ * Überschrift) — für flache Forms (Card-Titel + Felder direkt, ein
683
+ * einzelner Abschnitt) wie bei den meisten shadcn-Form-Mustern. */
684
+ readonly title?: string;
685
+ /** Optional help text under the block heading (i18n key or raw string,
686
+ * like `title`) — makes a form usable without training instead of
687
+ * falling back to custom JSX for help text. Renders through the same
688
+ * `subtitle` slot as `FormProps.subtitle`. Works without `title` too
689
+ * (subtitle-only section). */
690
+ readonly description?: string;
691
+ readonly columns?: number;
692
+ /** Mutually exclusive with `groups` — pass `[]` when using `groups`; the
693
+ * boot-validator rejects both non-empty or both empty. */
694
+ readonly fields: readonly EditFieldSpec[];
695
+ /** Splits the section into multiple titled cards instead of one flat grid.
696
+ * Mutually exclusive with `fields`; fields named here still need to exist. */
697
+ readonly groups?: readonly {
698
+ readonly title: string;
699
+ readonly fields: readonly EditFieldSpec[];
700
+ /** Default 2. */
701
+ readonly columns?: number;
702
+ }[];
703
+ /** Rendered left of the section title, `text-muted-foreground` — closed
704
+ * IconKey vocabulary into the ICONS registry (renderer-web), analogous
705
+ * to EditFieldSpec.icon. No title → no icon, and no heuristic derives
706
+ * one from the title (titles are free i18n strings). */
707
+ readonly icon?: IconKey;
708
+ /** Record field rendered as a count badge in the tab label when the
709
+ * enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
710
+ * Ignored outside tabs mode or when the field's value is not a finite
711
+ * number. */
712
+ readonly countField?: string;
713
+ /** Rendered top-right in the section's Card title row — same
714
+ * navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`,
715
+ * evaluated against the same record. Every action must resolve an icon
716
+ * (declared or id-derived); the boot-validator rejects one that doesn't. */
717
+ readonly actions?: readonly RowAction[];
718
+ };
719
+ export type EditExtensionSection = {
720
+ /** Kebab-case tab id, used as the `?tab=` value. Required by the
721
+ * boot-validator when the enclosing `EditLayout.mode` is "tabs". */
722
+ readonly id?: string;
723
+ readonly kind: "extension";
724
+ readonly title: string;
725
+ readonly component: PlatformComponent;
726
+ /** When true, the extension registers via `useExtensionFormSubmit` and the
727
+ * surrounding form's Save button should activate for its dirty state.
728
+ * Omit/false for extensions that persist through their own dispatcher
729
+ * writes (e.g. notes-history NotesSection) — otherwise a read-only form
730
+ * shows a Save button that does nothing (fw#2359). */
731
+ readonly contributesToFormSubmit?: boolean;
732
+ /** Overrides the host-derived entity name passed to the mounted component.
733
+ * Required on `projectionDetail` (which has no real entity — the host
734
+ * value there is an internal placeholder) so a self-persisting extension
735
+ * like NotesSection filters/writes against the right domain entity. Takes
736
+ * precedence over the host value on every screen type. */
737
+ readonly entityName?: string;
738
+ /** Record field rendered as a count badge in the tab label when the
739
+ * enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
740
+ * Ignored outside tabs mode or when the field's value is not a finite
741
+ * number. */
742
+ readonly countField?: string;
743
+ /** Rendered top-right in the section's Card title row — same
744
+ * navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`,
745
+ * evaluated against the same record. Every action must resolve an icon
746
+ * (declared or id-derived); the boot-validator rejects one that doesn't. */
747
+ readonly actions?: readonly RowAction[];
748
+ };
749
+ export type EditRelatedListSection = {
750
+ /** Kebab-case tab id, used as the `?tab=` value. Required by the
751
+ * boot-validator when the enclosing `EditLayout.mode` is "tabs". */
752
+ readonly id?: string;
753
+ readonly kind: "relatedList";
754
+ readonly title: string;
755
+ /** Fully qualified query QN. Same paged envelope as projectionList.query:
756
+ * `{ rows, nextCursor, total? }`. */
757
+ readonly query: string;
758
+ /** Query-payload key the parent record's id is passed under. Default "id". */
759
+ readonly parentParam?: string;
760
+ /** Server-side WHERE clause pinning this section to the parent record —
761
+ * sent as `payload.filter: { field, op: "eq", value: parentId }`, kept
762
+ * out of the user-facet `filters` array so it can't be cleared by facet
763
+ * interaction. Lets a tab reuse the generic `<entity>:list` query instead
764
+ * of a bespoke child-rows handler. Mutually exclusive with `parentParam`
765
+ * (the boot-validator rejects both). `field` must be a real field on the
766
+ * entity behind `query`, and that query's Zod schema must accept
767
+ * `filter` (same requirement `filter`/`facets` already have). */
768
+ readonly parentFilter?: {
769
+ readonly field: string;
770
+ };
771
+ readonly columns: readonly ListColumnSpec[];
772
+ /** i18n key for the one-line hint at the left of the tab toolbar. */
773
+ readonly description?: string;
774
+ /** i18n key holding plural forms for the footer count ("1 Position",
775
+ * "3 Positionen"). Omitted: generic "entries" wording. */
776
+ readonly itemNoun?: string;
777
+ readonly pageSize?: number;
778
+ /** Initial sort on mount, applied client-side over the already-loaded rows
779
+ * (this section has no pager, so there is no "loaded subset" to mislead).
780
+ * `field` must name a column with `sortable: true` — boot-validator pins
781
+ * that, same as `entityList.defaultSort`. */
782
+ readonly defaultSort?: ListSortSpec;
783
+ /** Renders a search box whose debounced term rides along as
784
+ * `payload.search` on this section's own query. Opt-in, unlike
785
+ * `projectionList.searchable` (derived from the bound query's schema): a
786
+ * relatedList query is an ordinary child-rows handler, so a `search`
787
+ * parameter in its schema is not by itself a statement that this section
788
+ * should offer search. The boot-validator rejects `true` when the query's
789
+ * Zod schema has no `search` parameter. */
790
+ readonly searchable?: boolean;
791
+ /** User-toggleable facet dropdowns — same explicit-label `ListFacetSpec` as
792
+ * `projectionList.facets` (a relatedList has no entity to derive filterable
793
+ * fields from). `field` must be a declared column and the bound query
794
+ * handler must accept `filters` in its Zod schema — the boot-validator
795
+ * checks both. Selected values ride along as `payload.filters`. */
796
+ readonly facets?: readonly ListFacetSpec[];
797
+ /** Row click opens the target entity's detail screen via ObjectTarget —
798
+ * the `detailFor` lookup owns the entity→screen mapping, so no screenId
799
+ * is named here. `idColumn` names the row key holding that id (default
800
+ * "id"). Omit `rowClick` for a non-interactive list. */
801
+ readonly rowClick?: {
802
+ readonly entity: string;
803
+ readonly idColumn?: string;
804
+ };
805
+ /** Per-row actions — same type and dispatch semantics as `entityList`/
806
+ * `projectionList`'s `rowActions` (writeHandler dispatch with a
807
+ * declarative `payload` extractor, or navigate). A successful
808
+ * writeHandler action re-runs this section's own query, same as a
809
+ * projectionList row action re-running its list query. */
810
+ readonly rowActions?: readonly RowAction[];
811
+ /** Toolbar actions above the table — same type and dispatch semantics as
812
+ * `entityList`/`projectionList`'s `toolbarActions` ("+ Anlegen" etc.),
813
+ * plus `visible`/`params` evaluated against the parent record (see
814
+ * RelatedListToolbarAction). */
815
+ readonly toolbarActions?: readonly RelatedListToolbarAction[];
816
+ /** Record field rendered as a count badge in the tab label when the
817
+ * enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
818
+ * Ignored outside tabs mode or when the field's value is not a finite
819
+ * number. */
820
+ readonly countField?: string;
821
+ /** Rendered top-right in the section's Card title row — same
822
+ * navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`,
823
+ * evaluated against the parent record. Every action must resolve an icon
824
+ * (declared or id-derived); the boot-validator rejects one that doesn't.
825
+ * Distinct from `toolbarActions` (rendered inside the DataTable toolbar) —
826
+ * this is the Card-level title-row action. */
827
+ readonly actions?: readonly RowAction[];
828
+ /** Rendered inside the section's Card in place of the table when this
829
+ * section's query returns zero rows. Omitted: the DataTable default
830
+ * ("kumiko.list.no-entries") applies. `action`'s icon must resolve, same
831
+ * as `actions` above. */
832
+ readonly emptyState?: {
833
+ readonly title: string;
834
+ readonly description?: string;
835
+ readonly action?: RowAction;
836
+ };
837
+ };
838
+ export type EditWriteFormSection = {
839
+ /** Kebab-case tab id, used as the `?tab=` value. Required by the
840
+ * boot-validator when the enclosing `EditLayout.mode` is "tabs". */
841
+ readonly id?: string;
842
+ readonly kind: "writeForm";
843
+ readonly title?: string;
844
+ readonly description?: string;
845
+ readonly columns?: number;
846
+ readonly icon?: IconKey;
847
+ /** Field-type map for this section's own form — same shape as
848
+ * `ActionFormScreenDefinition.fields`. */
849
+ readonly fieldDefs: Readonly<Record<string, FieldDefinition>>;
850
+ /** Rendered fields, in order — same shape as `EditFieldsSection.fields`.
851
+ * Every referenced field must have an entry in `fieldDefs`. */
852
+ readonly fields: readonly EditFieldSpec[];
853
+ /** Write-handler QN dispatched on submit. The submitted values (not a
854
+ * diff) land 1:1 as payload, same as `ActionFormScreenDefinition.handler`.
855
+ * The payload carries only this section's own fields — no parent/record
856
+ * id is injected automatically. To thread the host record's id through,
857
+ * declare it in `fieldDefs` with `visible: false` on its `fields` entry:
858
+ * it gets prefilled from the loaded record like any other field and rides
859
+ * along in the submit payload without being shown in the form. */
860
+ readonly handler: string;
861
+ /** i18n-key for the submit button. Default: "kumiko.actions.save". */
862
+ readonly submitLabel?: string;
863
+ /** Rendered top-right in the section's Card title row — same
864
+ * navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`,
865
+ * evaluated against the same record. Every action must resolve an icon
866
+ * (declared or id-derived); the boot-validator rejects one that doesn't. */
867
+ readonly actions?: readonly RowAction[];
868
+ };
869
+ export type FormWidth = "3xl" | "4xl" | "full";
870
+ export type EditLayout = {
871
+ readonly sections: readonly EditSectionSpec[];
872
+ /** Default "4xl" (a centered column, narrower than list chrome), unless
873
+ * the app overrides it via `createKumikoApp({ screenWidth })`. Override
874
+ * per-screen to "3xl" for a narrower column or "full" to match list
875
+ * chrome. */
876
+ readonly width?: FormWidth;
877
+ /** Default "single". "wizard" renders one section per step (with
878
+ * progress + per-step validation) instead of all sections at once —
879
+ * boot-validator requires >= 2 sections, each with a title, when set.
880
+ * "tabs" renders one section per tab (projectionDetail only — the
881
+ * boot-validator rejects it on entityEdit, see the check for the reason);
882
+ * requires >= 2 sections, each with a title and an `id`. */
883
+ readonly mode?: "single" | "wizard" | "tabs";
884
+ /** Persists in-progress wizard state as a resumable draft instead of
885
+ * discarding it on navigation away. */
886
+ readonly draft?: boolean;
887
+ };
888
+ export type EntityEditScreenDefinition = {
889
+ readonly id: string;
890
+ readonly type: "entityEdit";
891
+ /** Default true: the screen fills the shell content height, its body scrolls
892
+ * inside and the pager / action bar stays pinned. `false` restores page
893
+ * scroll (table grows with its rows, pager below the last row, form footer
894
+ * in the flow). */
895
+ readonly fillHeight?: boolean;
896
+ readonly nav?: ScreenNavSugar;
897
+ readonly detailFor?: string;
898
+ readonly description?: string;
899
+ readonly agent?: AgentHandlerHints;
900
+ /** Screen has no nav entry by design (opened via link or navved by the
901
+ * app); exempts it from the nav-area boot check. */
902
+ readonly dormant?: boolean;
903
+ /** Derived by buildAppSchema from the navigate `params` targeting this
904
+ * screen — the only URL query keys the create form prefills. An authored
905
+ * value is overwritten. */
906
+ readonly urlPrefillFields?: readonly string[];
907
+ readonly entity: string;
908
+ readonly layout: EditLayout;
909
+ /** Optionaler i18n-Key (oder Roh-String) für den Submit-Button. Default
910
+ * `kumiko.actions.save`. Lässt den Auto-Edit-Screen domain-spezifische
911
+ * CTAs zeigen ("Save Address", "Create item") statt generisch "Speichern". */
912
+ readonly submitLabel?: string;
913
+ /** Default true. `false` für Entities deren Create über einen eigenen
914
+ * Lifecycle-Write läuft (z.B. incident:open mit Event-Stream + Joins)
915
+ * statt über `<entity>:create`: unterdrückt den automatischen
916
+ * „+ Neu"-Button auf entityList-Screens dieser Entity und rendert den
917
+ * Create-Branch (Aufruf ohne entityId) als Fehler statt eines Forms,
918
+ * dessen Submit gegen einen nicht registrierten Handler liefe. */
919
+ readonly allowCreate?: boolean;
920
+ /** Default true. `false` wenn kein `<entity>:delete`-Handler existiert
921
+ * (History-/Audit-Erhalt): unterdrückt den Löschen-Button im
922
+ * Update-Form. */
923
+ readonly allowDelete?: boolean;
924
+ /** Default false. `true` for singleton entities (exactly one record per
925
+ * tenant: organization, settings, tenant profile). A call without an
926
+ * `entityId` first resolves the existing record via `<entity>:list`
927
+ * (limit 1) and renders the update branch with prefill on a hit,
928
+ * instead of blindly creating another one. The create branch only
929
+ * fires when the table is empty (and `allowCreate: false` still
930
+ * applies there). */
931
+ readonly singleton?: boolean;
932
+ /** Navigate to this screen ID after a successful save (create or update).
933
+ * Either a same-feature short screen-ID, or a fully-qualified
934
+ * cross-feature screen QN (`<feature>:screen:<id>`) — the nav-router
935
+ * resolves both against the schema; boot-validator checks the target
936
+ * resolves to a registered screen. Overrides the default post-save
937
+ * target (the entity's list screen); delete is unaffected and still
938
+ * always navigates to the list. A cross-feature QN hard-couples this
939
+ * feature to the target at boot time — an app that doesn't mount the
940
+ * target feature fails the boot-validator; only use this form in a
941
+ * reusable feature when the target feature is guaranteed to be
942
+ * mounted alongside it.
943
+ *
944
+ * The object form additionally names the success-payload field the
945
+ * navigation id comes from (`ActionFormRedirect.idFrom`) — needed when
946
+ * the edited record is a child and the target screen is the parent's
947
+ * detail screen. Update falls back to the loaded record when the
948
+ * success payload doesn't carry that field flatly; create falls back
949
+ * to the submitted form values. */
950
+ readonly redirect?: string | ActionFormRedirect;
951
+ /** Parent list screen (short id) for breadcrumb + nav highlighting when
952
+ * this screen has no nav entry of its own. Same field/semantics as on
953
+ * `custom`/`projectionDetail`; an explicit value here wins over the
954
+ * entity-list heuristic (rowAction target / same-entity entityList). */
955
+ readonly listScreenId?: string;
956
+ /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key), überschreibt
957
+ * die Default-Konvention `<feature>:entity:<entity>:field:<name>`.
958
+ * Primär für configEdit: dessen Pseudo-Entity `__config-edit__` hat
959
+ * keinen natürlichen Field-Namespace — der Settings-Hub injiziert hier
960
+ * das `mask.title`-Label des Config-Keys. Fehlt ein Eintrag, gilt die
961
+ * Konvention. */
962
+ readonly fieldLabels?: Readonly<Record<string, string>>;
963
+ /** Header action buttons. Reuses `RowAction` — analogous to
964
+ * `ProjectionDetailScreenDefinition.actions` (the loaded record stands
965
+ * in for the "row"). `rowClick` has no target here and is rejected by
966
+ * the boot-validator, same as on projectionDetail. Not rendered in
967
+ * create mode — every action targets an existing record, which the
968
+ * create branch doesn't have yet. */
969
+ readonly actions?: readonly RowAction[];
970
+ readonly slots?: ScreenSlots;
971
+ readonly access?: AccessRule;
972
+ };
973
+ export type ActionFormScreenDefinition = {
974
+ readonly id: string;
975
+ readonly type: "actionForm";
976
+ /** Default true: the screen fills the shell content height, its body scrolls
977
+ * inside and the pager / action bar stays pinned. `false` restores page
978
+ * scroll (table grows with its rows, pager below the last row, form footer
979
+ * in the flow). */
980
+ readonly fillHeight?: boolean;
981
+ readonly nav?: ScreenNavSugar;
982
+ readonly detailFor?: string;
983
+ readonly description?: string;
984
+ readonly agent?: AgentHandlerHints;
985
+ /** Screen has no nav entry by design (opened via link or navved by the
986
+ * app); exempts it from the nav-area boot check. */
987
+ readonly dormant?: boolean;
988
+ /** Derived by buildAppSchema — see EntityEditScreenDefinition.urlPrefillFields. */
989
+ readonly urlPrefillFields?: readonly string[];
990
+ /** Write-Handler-QN der bei Submit gerufen wird. Form-Object landet
991
+ * 1:1 als payload — Handler-Schema (Zod) validiert weiter. */
992
+ readonly handler: string;
993
+ /** Form-Shape: Field-Map pro Name. Nutzt dieselben FieldDefinitions
994
+ * wie Entity-Felder. Mindestens ein Feld erforderlich (Boot-
995
+ * Validator).
996
+ *
997
+ * A `money` field here must name its own currency source: this screen
998
+ * has no entity whose `defaultCurrency` it could inherit (fw#2839). */
999
+ readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1000
+ /** Layout analog zu EntityEditScreen: sections mit fields aus dem
1001
+ * fields-Map oben. */
1002
+ readonly layout: EditLayout;
1003
+ /** Per-field label i18n key override, same type/semantics as
1004
+ * `EntityEditScreenDefinition.fieldLabels`. Falls back to the convention when absent. */
1005
+ readonly fieldLabels?: Readonly<Record<string, string>>;
1006
+ /** i18n-key für den Submit-Button. Default: i18n-Default des
1007
+ * Renderers (typischerweise "actions.submit"). */
1008
+ readonly submitLabel?: string;
1009
+ /** Context box above the fields (i18n keys). `{name}` placeholders resolve from the
1010
+ * drawer prefill (`params` of the opening row action), formatted for display. */
1011
+ readonly summary?: {
1012
+ readonly title: string;
1013
+ readonly subtitle?: string;
1014
+ };
1015
+ /** Visual style of the submit button. "danger" renders it red — for a form
1016
+ * whose handler is destructive (terminate, revoke, delete), so the warning
1017
+ * isn't lost on the way from the calling screen to this one. Default "primary". */
1018
+ readonly submitStyle?: "primary" | "danger";
1019
+ /** Navigate to this screen ID after a successful submit: either a short
1020
+ * ID (e.g. "item-list" — same feature, the nav-router resolves to the
1021
+ * full path) or a fully-qualified cross-feature QN
1022
+ * (`<feature>:screen:<id>`). If not set, the user stays on the form
1023
+ * screen. Boot-validator checks that the target resolves to a
1024
+ * registered screen. A cross-feature QN hard-couples this feature to
1025
+ * the target at boot time — an app that doesn't mount the target
1026
+ * feature fails the boot-validator; only use this form in a reusable
1027
+ * feature when the target feature is guaranteed to be mounted
1028
+ * alongside it.
1029
+ *
1030
+ * The object form additionally names the success-payload field the
1031
+ * navigation id comes from (`ActionFormRedirect.idFrom`) — needed when
1032
+ * the handler creates a child record but the target screen is the
1033
+ * parent's detail screen (fw#2670). */
1034
+ readonly redirect?: string | ActionFormRedirect;
1035
+ /** Target of the Cancel button. Default: `redirect` (historical
1036
+ * behavior — Cancel and the submit-redirect then land in the same
1037
+ * place). `false` = no Cancel button; correct for single-action
1038
+ * screens with no discardable state (e.g. "Send test mail"), where
1039
+ * Cancel would just be a second path to the same target. Accepts the
1040
+ * same string targets as `redirect` (short ID or cross-feature QN);
1041
+ * boot-validator checks it. A cross-feature QN hard-couples this
1042
+ * feature to the target at boot time — an app that doesn't mount the
1043
+ * target feature fails the boot-validator; only use this form in a
1044
+ * reusable feature when the target feature is guaranteed to be
1045
+ * mounted alongside it. */
1046
+ readonly cancelTarget?: string | false;
1047
+ /** Parent list screen (short id) for breadcrumb + nav highlighting when
1048
+ * this screen has no nav entry of its own. Same field/semantics as on
1049
+ * `custom`/`projectionDetail`/`entityEdit`; an explicit value here wins
1050
+ * over the rowAction heuristic. */
1051
+ readonly listScreenId?: string;
1052
+ readonly slots?: ScreenSlots;
1053
+ readonly access?: AccessRule;
1054
+ };
1055
+ /** Redirect target plus the success-payload field carrying the navigation
1056
+ * id. The write-handler reports the id of what it wrote (`data.id`); when
1057
+ * that record is a child and the target screen shows its parent, the
1058
+ * parent id has to be read from a different field instead of forcing the
1059
+ * handler to misreport its own result (fw#2670). */
1060
+ export type ActionFormRedirect = {
1061
+ /** Same target forms as the string `redirect`: short screen ID
1062
+ * (same-feature) or a fully-qualified cross-feature screen QN. */
1063
+ readonly screen: string;
1064
+ /** Flat field name in the handler's success payload, e.g. "leaseId".
1065
+ * Only used when the target screen type carries an id (`entityEdit`,
1066
+ * `projectionDetail`); a non-string or missing value navigates without
1067
+ * an id, same as a payload without `id` does today. */
1068
+ readonly idFrom: string;
1069
+ };
1070
+ /** Author-declared field shown once after a mint-form submit — API token,
1071
+ * recovery codes, similar one-time secrets. */
1072
+ export type SecretRevealField = {
1073
+ /** Flat field name in the handler's success payload. */
1074
+ readonly field: string;
1075
+ /** i18n key for this value's label. */
1076
+ readonly label: string;
1077
+ /** "code" (default) renders one monospaced value; "list" renders a
1078
+ * string-array payload one value per line (recovery codes); "qr" renders
1079
+ * the value as a scannable QR code (otpauth://-style enrollment URI) —
1080
+ * platforms without a QR-capable SecretReveal primitive fall back to the
1081
+ * monospaced text display. */
1082
+ readonly display?: "code" | "list" | "qr";
1083
+ /** Default true — offer a copy-to-clipboard control next to the value. */
1084
+ readonly copyable?: boolean;
1085
+ };
1086
+ /** Post-reveal proof-of-receipt step. A mint whose effect is only armed once
1087
+ * the user proves they received the secret (TOTP enrollment: scan the code,
1088
+ * then enter one) declares it here — the reveal card renders this form in
1089
+ * place of the bare acknowledge button. */
1090
+ export type SecretMintConfirmStep = {
1091
+ /** Write-handler QN dispatched when the confirm form is submitted. */
1092
+ readonly handler: string;
1093
+ readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1094
+ readonly layout: EditLayout;
1095
+ /** Mint success-payload fields merged into the confirm payload (e.g. a
1096
+ * short-lived setup token). Held in component state only — never rendered,
1097
+ * never in the URL, a query cache, nav or a persisted draft. */
1098
+ readonly carry?: readonly string[];
1099
+ readonly submitLabel?: string;
1100
+ /** i18n key for the banner shown after a successful confirm when the screen
1101
+ * declares no `redirect`. Default "kumiko.secretMint.done". */
1102
+ readonly doneMessage?: string;
1103
+ };
1104
+ export type SecretReveal = {
1105
+ readonly fields: readonly SecretRevealField[];
1106
+ /** i18n key. Default "kumiko.secretMint.title". */
1107
+ readonly title?: string;
1108
+ /** i18n key for the "this is the only time you see it" warning.
1109
+ * Default "kumiko.secretMint.warning". */
1110
+ readonly warning?: string;
1111
+ /** i18n key for the acknowledge button. Default "kumiko.secretMint.confirm". */
1112
+ readonly confirmLabel?: string;
1113
+ };
1114
+ /** Mint form → one-time reveal → explicit confirm. The secret (API token,
1115
+ * recovery codes) lives only in the write-handler's own success payload —
1116
+ * no stored value backs it, so no query can redisplay it later. That gap
1117
+ * is why this needs its own screen type instead of a flag on `actionForm`,
1118
+ * which discards the success payload after extracting the navigation id. */
1119
+ export type SecretMintScreenDefinition = {
1120
+ readonly id: string;
1121
+ readonly type: "secretMint";
1122
+ readonly nav?: ScreenNavSugar;
1123
+ readonly detailFor?: string;
1124
+ readonly description?: string;
1125
+ readonly agent?: AgentHandlerHints;
1126
+ /** Screen has no nav entry by design (opened via link or navved by the
1127
+ * app); exempts it from the nav-area boot check. */
1128
+ readonly dormant?: boolean;
1129
+ /** Derived by buildAppSchema — see EntityEditScreenDefinition.urlPrefillFields. */
1130
+ readonly urlPrefillFields?: readonly string[];
1131
+ /** Write-handler QN dispatched on submit. */
1132
+ readonly handler: string;
1133
+ readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1134
+ readonly layout: EditLayout;
1135
+ /** Navigate here after the reveal is confirmed. Short id (same-feature) or
1136
+ * a fully-qualified cross-feature screen QN — no result-id navigation
1137
+ * form exists here (unlike `ActionFormScreenDefinition.redirect`), since
1138
+ * the confirmed screen carries no entity id to thread through. */
1139
+ readonly redirect?: string;
1140
+ readonly cancelTarget?: string | false;
1141
+ readonly listScreenId?: string;
1142
+ readonly access?: AccessRule;
1143
+ readonly reveal: SecretReveal;
1144
+ /** i18n-key for the mint button. Default: renderer's own default. */
1145
+ readonly submitLabel?: string;
1146
+ /** Proof-of-receipt step rendered on the reveal card in place of the bare
1147
+ * acknowledge button — see `SecretMintConfirmStep` doc. */
1148
+ readonly confirm?: SecretMintConfirmStep;
1149
+ };
1150
+ export type CustomScreenRoute = {
1151
+ readonly path: string;
1152
+ readonly component: PlatformComponent;
1153
+ };
1154
+ export type CustomScreenDefinition = {
1155
+ readonly id: string;
1156
+ readonly type: "custom";
1157
+ readonly nav?: ScreenNavSugar;
1158
+ readonly detailFor?: string;
1159
+ readonly description?: string;
1160
+ /** Only `expose` is read for screens; `risk` ranks handler tool calls. */
1161
+ readonly agent?: AgentHandlerHints;
1162
+ readonly renderer: PlatformComponent;
1163
+ readonly routes?: readonly CustomScreenRoute[];
1164
+ /** Parent list screen for breadcrumb when this detail is not in nav. */
1165
+ readonly listScreenId?: string;
1166
+ readonly access?: AccessRule;
1167
+ /** This screen is registered without a self-owned `r.nav()` entry by
1168
+ * design — an app opts in by navving it explicitly (e.g. a settings-area
1169
+ * self-service screen). `createKumikoApp`'s boot diagnostic
1170
+ * (kumiko-framework#2025) skips dormant screens instead of flagging
1171
+ * every consumer that doesn't mount the client plugin as a false
1172
+ * positive (kumiko-framework#2034). Only set this on screens the
1173
+ * feature itself never navs — a screen the feature DOES nav still needs
1174
+ * its client plugin mounted by every consumer, and should keep
1175
+ * triggering the diagnostic if it's missing. Also gates the
1176
+ * boot-validator's nav-area check, same as `dormant` on other screens. */
1177
+ readonly dormant?: boolean;
1178
+ };
1179
+ export type ConfigEditScreenDefinition = {
1180
+ readonly id: string;
1181
+ readonly type: "configEdit";
1182
+ readonly nav?: ScreenNavSugar;
1183
+ readonly detailFor?: string;
1184
+ readonly description?: string;
1185
+ readonly agent?: AgentHandlerHints;
1186
+ /** Screen has no nav entry by design (opened via link or navved by the
1187
+ * app); exempts it from the nav-area boot check. */
1188
+ readonly dormant?: boolean;
1189
+ /** scope für config:write:set Calls. Muss zur Scope-Deklaration der
1190
+ * in `configKeys` referenzierten Keys passen — Boot-Validator
1191
+ * prüft das gegen die Registry. */
1192
+ readonly scope: "tenant" | "system" | "user";
1193
+ /** Map: form-field-name (kurz, wie im Layout referenziert) → voll-
1194
+ * qualifizierter Config-Key (`<feature>:config:<short>`). Boot-
1195
+ * Validator prüft dass jeder qualifizierte Key in der Registry
1196
+ * bekannt ist. */
1197
+ readonly configKeys: Readonly<Record<string, string>>;
1198
+ /** Form-Shape pro Field-Name. Selbe FieldDefinitions wie auf
1199
+ * Entities/ActionForm. Field-Names matchen die Keys in `configKeys`
1200
+ * — Boot-Validator pinnt das. */
1201
+ readonly fields: Readonly<Record<string, FieldDefinition>>;
1202
+ /** Layout: Sections mit Field-Refs. Identisch zu entityEdit/
1203
+ * actionForm. */
1204
+ readonly layout: EditLayout;
1205
+ /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key). Der
1206
+ * Settings-Hub setzt hier `mask.title` des jeweiligen Config-Keys,
1207
+ * damit das am Key deklarierte Label am generierten Feld erscheint —
1208
+ * ohne es unter der `__config-edit__`-Konvention zu duplizieren.
1209
+ * Fehlt ein Eintrag, gilt die Konvention. */
1210
+ readonly fieldLabels?: Readonly<Record<string, string>>;
1211
+ /** i18n-key für den Submit-Button. Default: "kumiko.actions.save". */
1212
+ readonly submitLabel?: string;
1213
+ readonly slots?: ScreenSlots;
1214
+ readonly access?: AccessRule;
1215
+ };
1216
+ export type SecretsEditSection = {
1217
+ readonly title?: string;
1218
+ readonly fields: readonly string[];
1219
+ };
1220
+ export type SecretsEditScreenDefinition = {
1221
+ readonly id: string;
1222
+ readonly type: "secretsEdit";
1223
+ readonly nav?: ScreenNavSugar;
1224
+ readonly detailFor?: string;
1225
+ readonly description?: string;
1226
+ readonly agent?: AgentHandlerHints;
1227
+ /** Screen has no nav entry by design (opened via link or navved by the
1228
+ * app); exempts it from the nav-area boot check. */
1229
+ readonly dormant?: boolean;
1230
+ /** field id -> qualified secret name (`<feature>:secret:<kebab>`). */
1231
+ readonly secretKeys: Readonly<Record<string, string>>;
1232
+ /** field id -> i18n key for the label. */
1233
+ readonly fieldLabels: Readonly<Record<string, string>>;
1234
+ /** field id -> i18n key for the hint, only where the declaration carries one. */
1235
+ readonly fieldHints?: Readonly<Record<string, string>>;
1236
+ /** field ids whose declaration set `required: true`. */
1237
+ readonly requiredFields?: readonly string[];
1238
+ readonly sections: readonly SecretsEditSection[];
1239
+ readonly access?: AccessRule;
1240
+ };
1241
+ export type ScreenSlots = {
1242
+ readonly header?: PlatformComponent;
1243
+ /** entityEdit / actionForm: rendered on the right of the form title, same row. */
1244
+ readonly titleAction?: PlatformComponent;
1245
+ readonly beforeForm?: PlatformComponent;
1246
+ readonly afterForm?: PlatformComponent;
1247
+ readonly sidebar?: PlatformComponent;
1248
+ readonly footer?: PlatformComponent;
1249
+ /** entityEdit wizard: marks `footer` as the sticky-footer primary action
1250
+ * (fw#1918's pinned `max-sm:fixed` group on mobile) instead of the
1251
+ * non-sticky secondary bucket it falls into by default alongside
1252
+ * Back/Cancel. Set this when `footer` renders the screen's actual
1253
+ * submit-type action (e.g. a custom "Publish" button standing in for, or
1254
+ * beside, the built-in Finish/Save) — the renderer can't infer that from
1255
+ * the slot's arbitrary rendered output, only from this declaration. */
1256
+ readonly footerPrimary?: boolean;
1257
+ readonly toolbar?: PlatformComponent;
1258
+ };
1259
+ export type ScreenNavSugar = {
1260
+ readonly label: string;
1261
+ readonly icon?: NavIconKey;
1262
+ readonly parent?: string;
1263
+ readonly order?: number;
1264
+ };
1265
+ export type ScreenDefinition = EntityListScreenDefinition | ProjectionListScreenDefinition | ProjectionDetailScreenDefinition | DashboardScreenDefinition | EntityEditScreenDefinition | ActionFormScreenDefinition | ConfigEditScreenDefinition | SecretsEditScreenDefinition | SecretMintScreenDefinition | CustomScreenDefinition;