@cosmicdrift/kumiko-framework 0.158.2 → 0.160.0

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 (168) hide show
  1. package/package.json +7 -2
  2. package/src/__tests__/consumer-cli.integration.test.ts +110 -0
  3. package/src/api/__tests__/api.test.ts +65 -0
  4. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  5. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +237 -0
  6. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +1 -0
  7. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  8. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  9. package/src/api/__tests__/jwt.test.ts +150 -1
  10. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +41 -0
  11. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  12. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  13. package/src/api/api-constants.ts +5 -0
  14. package/src/api/auth-middleware.ts +48 -59
  15. package/src/api/auth-routes.ts +51 -17
  16. package/src/api/index.ts +3 -3
  17. package/src/api/jwt.ts +148 -7
  18. package/src/api/pii-leak-guard.ts +5 -2
  19. package/src/api/routes.ts +57 -0
  20. package/src/api/server.ts +19 -5
  21. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  22. package/src/bun-db/query.ts +46 -27
  23. package/src/consumer-cli.ts +87 -0
  24. package/src/crypto/__tests__/pii-field-encryption.test.ts +69 -13
  25. package/src/crypto/blind-index.ts +8 -4
  26. package/src/crypto/event-pii.ts +1 -0
  27. package/src/crypto/kms-adapter.ts +2 -118
  28. package/src/crypto/pii-field-encryption.ts +49 -15
  29. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  30. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  31. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +396 -0
  32. package/src/db/__tests__/event-store-executor.integration.test.ts +5 -5
  33. package/src/db/blind-index-cleanup.ts +3 -1
  34. package/src/db/connection.ts +3 -11
  35. package/src/db/cursor.ts +1 -18
  36. package/src/db/dialect.ts +8 -19
  37. package/src/db/encryption.ts +2 -3
  38. package/src/db/entity-table-meta-types.ts +2 -0
  39. package/src/db/entity-table-meta.ts +16 -90
  40. package/src/db/event-store-executor.ts +4 -96
  41. package/src/db/queries/backfill-pii.ts +1 -0
  42. package/src/db/queries/event-consumer.ts +35 -2
  43. package/src/db/table-builder.ts +2 -19
  44. package/src/db/tenant-db.ts +6 -55
  45. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  46. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -233
  47. package/src/engine/__tests__/boot-validator.test.ts +46 -0
  48. package/src/engine/__tests__/codemod-pipeline.test.ts +139 -10
  49. package/src/engine/__tests__/define-roles.test.ts +21 -0
  50. package/src/engine/__tests__/engine.test.ts +28 -0
  51. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  52. package/src/engine/__tests__/registry-facade-sweep.test.ts +80 -0
  53. package/src/engine/__tests__/registry.test.ts +40 -0
  54. package/src/engine/__tests__/store-table.test.ts +12 -0
  55. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  56. package/src/engine/boot-validator/action-wiring.ts +1 -1
  57. package/src/engine/boot-validator/boot-check.ts +21 -0
  58. package/src/engine/boot-validator/entity-handler.ts +10 -1
  59. package/src/engine/boot-validator/entity-list-screens.ts +1 -1
  60. package/src/engine/boot-validator/gdpr-storage.ts +0 -112
  61. package/src/engine/boot-validator/index.ts +3 -9
  62. package/src/engine/boot-validator/screens.ts +1 -1
  63. package/src/engine/define-feature.ts +2 -0
  64. package/src/engine/define-handler.ts +11 -91
  65. package/src/engine/entity-handlers.ts +15 -27
  66. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +11 -1
  67. package/src/engine/feature-ast/__tests__/parse.test.ts +983 -3
  68. package/src/engine/feature-ast/__tests__/patch.test.ts +168 -0
  69. package/src/engine/feature-ast/__tests__/patcher.test.ts +7 -0
  70. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +9 -0
  71. package/src/engine/feature-ast/extractors/handlers.ts +19 -2
  72. package/src/engine/feature-ast/extractors/index.ts +1 -0
  73. package/src/engine/feature-ast/index.ts +2 -0
  74. package/src/engine/feature-ast/parse.ts +3 -0
  75. package/src/engine/feature-ast/patch.ts +2 -0
  76. package/src/engine/feature-ast/patcher.ts +21 -0
  77. package/src/engine/feature-ast/patterns.ts +16 -0
  78. package/src/engine/feature-ast/render.ts +15 -0
  79. package/src/engine/feature-builder-state.ts +6 -0
  80. package/src/engine/feature-config-events-jobs.ts +1 -1
  81. package/src/engine/feature-entity-handlers.ts +36 -2
  82. package/src/engine/feature-ui-extensions.ts +5 -1
  83. package/src/engine/field-helpers.ts +31 -0
  84. package/src/engine/handler-helpers.ts +26 -0
  85. package/src/engine/hook-helpers.ts +14 -0
  86. package/src/engine/index.ts +5 -2
  87. package/src/engine/ownership.ts +22 -76
  88. package/src/engine/pattern-library/__tests__/library.test.ts +9 -0
  89. package/src/engine/pattern-library/library.ts +2 -0
  90. package/src/engine/pattern-library/mixed-schemas.ts +37 -0
  91. package/src/engine/registry-facade.ts +9 -0
  92. package/src/engine/registry-ingest.ts +10 -0
  93. package/src/engine/registry-state.ts +3 -0
  94. package/src/engine/registry-validate.ts +1 -1
  95. package/src/engine/screen-helpers.ts +54 -0
  96. package/src/engine/tier-resolver-extension.ts +3 -2
  97. package/src/engine/types/config.ts +2 -497
  98. package/src/engine/types/define-handler.ts +2 -0
  99. package/src/engine/types/entity-handlers.ts +2 -0
  100. package/src/engine/types/event-type-map.ts +1 -37
  101. package/src/engine/types/feature.ts +2 -976
  102. package/src/engine/types/fields.ts +2 -697
  103. package/src/engine/types/handlers.ts +2 -839
  104. package/src/engine/types/hooks.ts +2 -184
  105. package/src/engine/types/http-route.ts +1 -72
  106. package/src/engine/types/identifiers.ts +1 -47
  107. package/src/engine/types/index.ts +66 -33
  108. package/src/engine/types/nav.ts +2 -67
  109. package/src/engine/types/ownership.ts +2 -0
  110. package/src/engine/types/projection.ts +2 -165
  111. package/src/engine/types/relations.ts +1 -51
  112. package/src/engine/types/screen.ts +2 -793
  113. package/src/engine/types/step.ts +2 -334
  114. package/src/engine/types/target-ref.ts +1 -21
  115. package/src/engine/types/tree-node.ts +1 -129
  116. package/src/engine/types/workspace.ts +2 -42
  117. package/src/entrypoint/index.ts +2 -2
  118. package/src/errors/write-error-info.ts +6 -22
  119. package/src/event-store/__tests__/event-store.integration.test.ts +31 -0
  120. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +43 -0
  121. package/src/event-store/errors.ts +2 -35
  122. package/src/event-store/event-store.ts +28 -51
  123. package/src/event-store/events-schema.ts +1 -10
  124. package/src/event-store/index.ts +3 -2
  125. package/src/event-store/snapshot.ts +11 -35
  126. package/src/event-store/types.ts +2 -0
  127. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  128. package/src/files/file-handle.ts +2 -19
  129. package/src/files/provider-resolver.ts +3 -5
  130. package/src/files/types.ts +5 -54
  131. package/src/i18n/required-surface-keys.ts +1 -1
  132. package/src/jobs/__tests__/jobs.integration.test.ts +102 -1
  133. package/src/logging/types.ts +1 -7
  134. package/src/observability/types/index.ts +1 -29
  135. package/src/observability/types/metric.ts +1 -56
  136. package/src/observability/types/provider.ts +1 -32
  137. package/src/observability/types/span.ts +1 -58
  138. package/src/pipeline/__tests__/dispatcher.test.ts +134 -1
  139. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  140. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +180 -0
  141. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +208 -0
  142. package/src/pipeline/dispatch-shared.ts +51 -3
  143. package/src/pipeline/dispatch-stream.ts +74 -0
  144. package/src/pipeline/dispatcher-utils.ts +1 -1
  145. package/src/pipeline/dispatcher.ts +7 -0
  146. package/src/pipeline/entity-cache.ts +2 -33
  147. package/src/pipeline/event-consumer-state.ts +28 -3
  148. package/src/pipeline/event-dispatcher-admin.ts +4 -0
  149. package/src/pipeline/event-dispatcher-delivery.ts +29 -3
  150. package/src/pipeline/event-dispatcher.ts +27 -1
  151. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  152. package/src/pipeline/system-hooks.ts +7 -0
  153. package/src/rate-limit/resolver.ts +10 -30
  154. package/src/search/types.ts +1 -39
  155. package/src/secrets/__tests__/envelope-cipher.test.ts +2 -30
  156. package/src/secrets/__tests__/envelope.test.ts +1 -1
  157. package/src/secrets/envelope-cipher.ts +17 -45
  158. package/src/secrets/types.ts +2 -177
  159. package/src/stack/__tests__/event-collector.test.ts +42 -0
  160. package/src/testing/__tests__/late-bound.test.ts +25 -0
  161. package/src/testing/__tests__/wait-for.test.ts +53 -0
  162. package/src/testing/boot-validator-fixture.ts +1 -1
  163. package/src/testing/file-provider-contract.ts +84 -0
  164. package/src/testing/handler-context.ts +1 -1
  165. package/src/testing/index.ts +1 -0
  166. package/src/time/geo-tz.ts +1 -32
  167. package/src/time/tz-context.ts +9 -56
  168. package/src/ui-types/index.ts +7 -7
@@ -1,793 +1,2 @@
1
- import type { FieldDefinition } from "./fields";
2
- import type { AccessRule } from "./handlers";
3
-
4
- // Screen definitions describe how a feature surfaces data to the user.
5
- // Pure data — the engine stores these verbatim and ui-core / the renderer
6
- // packages decide what to do with them. The framework must not import
7
- // React / react-native from here; renderer components stay opaque so
8
- // `engine/` imports don't pull the whole UI toolchain into every bundle.
9
- //
10
- // Note on `id`: feature authors write the short form ("product-list"); the
11
- // registry overwrites `id` with the qualified name ("shop:screen:product-list")
12
- // in its stored copies. Callers of `registry.getScreen(qn)` /
13
- // `getAllScreens()` / `getScreensByEntity(...)` always see the qualified id.
14
- // `feature.screens[shortId]` on the unregistered FeatureDefinition keeps
15
- // the short form.
16
-
17
- // A per-platform component pair — used anywhere a feature attaches a
18
- // rendered component (screens, slots, routes, field renderers, future
19
- // r.uiComponent). Both fields are `unknown` so the engine doesn't depend
20
- // on React types; ui-core resolves the correct platform at mount-time.
21
- // Framework code only checks structural presence.
22
- export type PlatformComponent = {
23
- readonly react?: unknown;
24
- readonly native?: unknown;
25
- };
26
-
27
- // Built-in value formatters. Apps extend via module augmentation:
28
- // declare module "@cosmicdrift/kumiko-framework/engine/types" {
29
- // interface FieldFormatRegistry { myFormat: { myOption?: string } }
30
- // }
31
- // renderer-web handles all built-in keys; unknown app-specific keys fall back
32
- // to String(value).
33
- export interface FieldFormatRegistry {
34
- timestamp: {
35
- readonly locale?: string;
36
- readonly dateStyle?: "full" | "long" | "medium" | "short";
37
- readonly timeStyle?: "full" | "long" | "medium" | "short";
38
- };
39
- date: {
40
- readonly locale?: string;
41
- readonly dateStyle?: "full" | "long" | "medium" | "short";
42
- };
43
- boolean: { readonly trueLabel?: string; readonly falseLabel?: string };
44
- currency: { readonly symbol?: string };
45
- priority: { readonly emptyLabel?: string; readonly prefix?: string };
46
- }
47
-
48
- // Discriminated union derived from the registry — one variant per key.
49
- // JSON-safe: no function members, survives buildAppSchema → window.__KUMIKO_SCHEMA__.
50
- export type FormatSpec = {
51
- [K in keyof FieldFormatRegistry]: { readonly format: K } & FieldFormatRegistry[K];
52
- }[keyof FieldFormatRegistry];
53
-
54
- // Level-2 field renderer (ui-architecture.md §Renderer Customization):
55
- // - PlatformComponent → platform-specific component from the same feature
56
- // - string → cross-feature QN reference (resolved at mount-time)
57
- // - FormatSpec → declarative value formatter, JSON-safe ({ format: "timestamp" } etc.)
58
- export type FieldRenderer = PlatformComponent | string | FormatSpec;
59
-
60
- // Declarative field-state condition. Evaluated by the renderer against the
61
- // current row/form values. Three forms:
62
- // boolean — static on/off (e.g. readOnly: true)
63
- // { field, eq } — true when row[field] === eq
64
- // { field, ne } — true when row[field] !== ne
65
- // JSON-safe: survives buildAppSchema → window.__KUMIKO_SCHEMA__ stringify.
66
- export type FieldCondition =
67
- | boolean
68
- | { readonly field: string; readonly eq: unknown }
69
- | { readonly field: string; readonly ne: unknown };
70
-
71
- // --- entityList ---
72
-
73
- // `string` shorthand when the column only needs its field name; the object
74
- // form carries renderer / display overrides. normalizeListColumn() below
75
- // collapses both into the object form for downstream consumers.
76
- export type ListColumnSpec =
77
- | string
78
- | {
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
- };
92
-
93
- // Pagination-Modi für entityList:
94
- // - "pages": klassischer Pager (← 1 2 ... N →) — bookmarkable
95
- // via ?page=N in der URL. Server liefert `total`
96
- // damit der Pager "Page X of Y" rendern kann.
97
- // Default für CRUD-/Admin-Listen.
98
- // - "infinite": IntersectionObserver am Bottom — beim Sichtbar
99
- // werden lädt cursor-basiert die nächste Page und
100
- // appended an `rows`. Kein page-State in URL (Scroll-
101
- // Position ist Browser-eigen).
102
- // - false: Pagination aus — `executor.list()` lädt alles
103
- // was zur Tenant-Sicht passt. Sinnvoll für kleine
104
- // Lookup-/Master-Daten (≤ ~200 Rows).
105
- export type ListPaginationMode = "pages" | "infinite" | false;
106
-
107
- // Sort-State auf der Wire — Field-Name muss zur Entity-Definition
108
- // passen UND als sortable: true markiert sein. Validator lehnt sonst
109
- // beim Boot ab.
110
- export type ListSortDir = "asc" | "desc";
111
- export type ListSortSpec = {
112
- readonly field: string;
113
- readonly dir: ListSortDir;
114
- };
115
-
116
- // Screen-Level Filter — Author deklariert pro List-Screen einen festen
117
- // Filter der bei jedem Query angewendet wird. Use-case: drei Buckets
118
- // derselben Entity ("Upcoming Maintenance" / "Active Maintenance" /
119
- // "Past Maintenance") ohne drei Custom-Pages — Filter pro Screen
120
- // unterscheidet sie.
121
- //
122
- // Operatoren (Drizzle-konsistent):
123
- // eq/ne → field = value | field != value
124
- // lt/gt → field < value | field > value (numerisch / temporal)
125
- // in → field IN (...values), value muss readonly array sein
126
- //
127
- // Field muss in der Entity existieren UND `filterable: true` haben
128
- // (Boot-Validator pinned beides). `lt`/`gt` nur auf vergleichbaren
129
- // Field-Types (number/money/date/timestamp/locatedTimestamp); auf
130
- // text/boolean/select/multiSelect lehnt der Validator das ab.
131
- //
132
- // Security-Modell: Filter ist UX-Bucketing, KEINE Access-Boundary. Der
133
- // Server appliziert den filter aus dem Payload — der Client kann ihn
134
- // weglassen oder durch einen anderen ersetzen. Boundary bleiben
135
- // access-rule + Tenant-Scope; Felder mit Sicherheits-Bias (encrypted,
136
- // restricted) müssen dort geschützt werden, nicht über den Screen-Filter.
137
- export type ScreenFilterOp = "eq" | "ne" | "lt" | "gt" | "in";
138
- export type ScreenFilter = {
139
- readonly field: string;
140
- readonly op: ScreenFilterOp;
141
- readonly value: unknown;
142
- };
143
-
144
- /** Deklarativer Row-Field-Extraktor — JSON-sicher (kein Function-Prop,
145
- * überlebt window.__KUMIKO_SCHEMA__ / buildAppSchema).
146
- *
147
- * `pick`: extrahiert Felder 1:1. `{ pick: ["id", "version"] }` → `{ id: row.id, version: row.version }`
148
- * `map`: benennt um. `{ map: { incidentId: "id" } }` → `{ incidentId: row.id }`
149
- *
150
- * Limitation: computed/template-Werte können nicht ausgedrückt werden
151
- * — solche Logik gehört server-side in den Write-Handler. */
152
- export type RowFieldExtractor =
153
- | { readonly pick: readonly string[] }
154
- | { readonly map: Readonly<Record<string, string>> };
155
-
156
- // RowAction — per-Row Button/Dropdown-Item das einen Write-Handler
157
- // triggert oder zu einem anderen Screen navigiert.
158
- //
159
- // Pattern: row-level Lifecycle-Operations (Maintenance start/cancel/
160
- // complete, Incident resolve, Order ship etc.) — Sachen die in einem
161
- // CRUD-update kein passendes Verb haben aber als WriteHandler existieren.
162
- //
163
- // Discriminated Union mit `kind`:
164
- // - "writeHandler" (default): dispatched einen Write-Handler mit
165
- // Payload pro Row.
166
- // - "navigate" (Tier 2.7e): navigiert zu einem anderen Screen,
167
- // optional mit URL-Search-Params aus `params`. Use-case: "Edit",
168
- // "View Audit-Log", "Open in actionForm" etc.
169
- export type RowAction = RowActionWriteHandler | RowActionNavigate;
170
-
171
- export type RowActionWriteHandler = {
172
- /** Default für RowActions ohne explizit gesetzten `kind` —
173
- * Backwards-kompatible Form. Kann auch explizit gesetzt werden. */
174
- readonly kind?: "writeHandler";
175
- /** Stable id pro Screen — kebab-case, eindeutig im Action-Set. */
176
- readonly id: string;
177
- /** Anzeige-Text (i18n-Key). */
178
- readonly label: string;
179
- /** Qualified-Name des Server-Handlers, z.B.
180
- * "publicstatus:write:maintenance:start". Wird via useDispatcher
181
- * dispatcht. */
182
- readonly handler: string;
183
- /** Deklarativer Payload pro Row. Default = `{ id: row.id }`.
184
- * `pick` extrahiert Felder gleichen Namens; `map` benennt um. */
185
- readonly payload?: RowFieldExtractor;
186
- /** i18n-Key für die Confirm-Dialog-Description. Wenn gesetzt, öffnet
187
- * ein Modal vor der Ausführung — der User muss explizit bestätigen.
188
- * Zusammen mit `style: "danger"` ist das die Standard-Sicherheits-
189
- * Garde für destruktive Aktionen. */
190
- readonly confirm?: string;
191
- /** i18n-Key für den Confirm-Button-Text im Dialog. Default = `label`
192
- * (also "Delete" → "Delete"-Button im Confirm). Setzen wenn die
193
- * Action einen langen Namen hat ("Mark Subscription as Cancelled")
194
- * und der Button kürzer sein soll ("Cancel Subscription"). */
195
- readonly confirmLabel?: string;
196
- /** Conditional Visibility pro Row. */
197
- readonly visible?: FieldCondition;
198
- /** Visual-Style. "danger" rendert rot + erzwingt einen Confirm-
199
- * Dialog (auch ohne expliziten `confirm`-Key). */
200
- readonly style?: "primary" | "secondary" | "danger";
201
- };
202
-
203
- export type RowActionNavigate = {
204
- readonly kind: "navigate";
205
- readonly id: string;
206
- readonly label: string;
207
- /** Screen-id (kurz, unqualified) zu dem navigiert wird. Boot-
208
- * Validator prüft Existenz im selben Feature. */
209
- readonly screen: string;
210
- /** Feldname dessen Wert als entityId in den URL-Pfad eingebettet wird
211
- * (`/<workspace>/<screen>/<entityId>`). entityEdit liest die Id
212
- * AUSSCHLIESSLICH aus dem Pfad. Default: "id" wenn der Ziel-Screen
213
- * ein entityEdit ist. */
214
- readonly entityId?: string;
215
- /** Deklarative URL-Search-Params aus row-Context. Wird in actionForm-
216
- * Targets als initial values gelesen (actionForm pre-fillen).
217
- * `pick` extrahiert Felder gleichen Namens; `map` benennt um. */
218
- readonly params?: RowFieldExtractor;
219
- /** Conditional Visibility pro Row. */
220
- readonly visible?: FieldCondition;
221
- readonly style?: "primary" | "secondary";
222
- /** Wenn true, löst ein Klick auf die ganze Zeile (nicht nur das Aktionsmenü)
223
- * diese navigate-Action aus. Max. eine pro Liste (Boot-Validator prüft). Nur
224
- * auf navigate — ein Row-Klick darf keinen (evtl. destruktiven, unbestätigten)
225
- * Write auslösen, daher nicht auf writeHandler-Actions. */
226
- readonly rowClick?: boolean;
227
- };
228
-
229
- // ToolbarAction — Button im List-Header. Zwei Varianten: navigate auf
230
- // einen anderen Screen (z.B. zu einem actionForm) oder direkt einen
231
- // Handler dispatchen (z.B. "Sync All" ohne Form).
232
- export type ToolbarAction =
233
- | {
234
- readonly kind: "navigate";
235
- readonly id: string;
236
- readonly label: string;
237
- /** Screen-id (kurz, unqualified) zu dem navigiert wird. */
238
- readonly screen: string;
239
- readonly style?: "primary" | "secondary";
240
- }
241
- | {
242
- readonly kind: "writeHandler";
243
- readonly id: string;
244
- readonly label: string;
245
- readonly handler: string;
246
- /** Statischer Payload ohne row-Context. Default = `{}`. */
247
- readonly payload?: Record<string, unknown>;
248
- /** i18n-Key für Confirm-Dialog-Description. Wenn gesetzt UND/ODER
249
- * style="danger": Modal vor der Ausführung. */
250
- readonly confirm?: string;
251
- /** i18n-Key für Confirm-Button-Text im Dialog. Default = `label`. */
252
- readonly confirmLabel?: string;
253
- readonly style?: "primary" | "secondary" | "danger";
254
- };
255
-
256
- export type EntityListScreenDefinition = {
257
- readonly id: string;
258
- readonly type: "entityList";
259
- readonly entity: string;
260
- readonly columns: readonly ListColumnSpec[];
261
- // Row renderer (Desktop) — when omitted, renderer draws the default table
262
- // from `columns`. cardRenderer fills the same role on compact layouts.
263
- readonly rowRenderer?: PlatformComponent;
264
- readonly cardRenderer?: PlatformComponent;
265
- /** Per-Row-Aktionen — rendert eine Actions-Spalte rechts in der Tabelle.
266
- * Bis zu 2 actions als inline-Buttons; >2 als Kebab-Dropdown.
267
- * Reihenfolge im Array = Reihenfolge in der UI. */
268
- readonly rowActions?: readonly RowAction[];
269
- /** Toolbar-Aktionen (List-Header). "Open Incident", "Schedule Maintenance"
270
- * etc. — neben "+ Neu" wenn vorhanden. Reihenfolge im Array = UI-
271
- * Reihenfolge, primary-style links. */
272
- readonly toolbarActions?: readonly ToolbarAction[];
273
- /** Server-side Filter, fest am Screen — drei Buckets derselben
274
- * Entity ohne Custom-Pages (z.B. "Upcoming" / "Active" / "Past"
275
- * Maintenance). User-side q-Search läuft AUF dem gefilterten Set
276
- * oben drauf. Boot-Validator pinst dass field in der Entity existiert. */
277
- readonly filter?: ScreenFilter;
278
- // Pagination-Modus (Default "pages"). Bestimmt UI (Pager vs Scroll-
279
- // Sentinel) und ob der Server `total` mitliefern muss.
280
- readonly pagination?: ListPaginationMode;
281
- // Page-Größe. Default 50 — guter Kompromiss zwischen "viel sichtbar"
282
- // und "DB liefert schnell". Apps mit teurem Read (Joins, Computed-
283
- // Fields) gehen runter; Power-User-Listen (z.B. internal Analytics)
284
- // gehen hoch.
285
- readonly pageSize?: number;
286
- // Default-Sortierung beim Erst-Mount. Wenn URL-Param `?sort=…`
287
- // gesetzt ist, gewinnt der; sonst nutzt RenderList diesen Default.
288
- // `field` muss in der Entity sortable: true sein — Boot-Validator
289
- // pinnt das.
290
- readonly defaultSort?: ListSortSpec;
291
- // Search-Toolbar im UI an/aus. Server-Search geht IMMER über den
292
- // SearchAdapter (Meilisearch) — kein DB-ILIKE-Drift. Default true
293
- // wenn die Entity searchable Felder hat, sonst false.
294
- readonly searchable?: boolean;
295
- readonly slots?: ScreenSlots;
296
- readonly access?: AccessRule;
297
- };
298
-
299
- // --- projectionList ---
300
-
301
- // Like entityList, but bound to an EXPLICIT query instead of an entity. The
302
- // list-query is taken verbatim from `query` (a fully qualified QN like
303
- // "ledger:query:schedule:list") — NOT derived from the screen's own feature —
304
- // so a screen can render any read-projection, including one owned by a
305
- // different feature (the entityList feature-local resolution can't). Columns
306
- // carry their own labels (no entity to derive field-labels from); there is no
307
- // auto create-navigation (a projection isn't an editable entity list). Row
308
- // interaction is explicit via `rowActions`. The query must return the same
309
- // paged envelope as an entity list-query: `{ rows, nextCursor, total? }`.
310
- export type ProjectionListScreenDefinition = {
311
- readonly id: string;
312
- readonly type: "projectionList";
313
- readonly query: string;
314
- readonly columns: readonly ListColumnSpec[];
315
- readonly rowRenderer?: PlatformComponent;
316
- readonly cardRenderer?: PlatformComponent;
317
- readonly rowActions?: readonly RowAction[];
318
- readonly toolbarActions?: readonly ToolbarAction[];
319
- readonly pagination?: ListPaginationMode;
320
- readonly pageSize?: number;
321
- readonly defaultSort?: ListSortSpec;
322
- readonly searchable?: boolean;
323
- readonly slots?: ScreenSlots;
324
- readonly access?: AccessRule;
325
- };
326
-
327
- // --- projectionDetail ---
328
-
329
- // Read-only counterpart to projectionList — a single-row inspector bound to
330
- // an EXPLICIT query instead of an entity (`entityEdit` requires `r.entity`,
331
- // which a direct-write/projection read-model like `jobs`/`sessions` doesn't
332
- // have — see #255). There is no write path: the renderer forces every field
333
- // readOnly structurally (not just by convention — see
334
- // renderer/projection-detail-shim.ts), so no `<entity>:write:...:update`
335
- // command is ever constructed. `idParam` names the query-payload key the
336
- // route's row-id is passed under; defaults to "id", but a query handler
337
- // owned by a different feature may already use a domain-specific param name
338
- // (e.g. jobs' `detailQuery` expects `runId`) — this lets the primitive bind
339
- // to it without forcing a handler rename. Extension sections aren't
340
- // supported (no entity for them to persist against); the boot-validator
341
- // rejects them.
342
- export type ProjectionDetailScreenDefinition = {
343
- readonly id: string;
344
- readonly type: "projectionDetail";
345
- readonly query: string;
346
- /** Query-payload key for the row-id. Default "id". */
347
- readonly idParam?: string;
348
- readonly layout: EditLayout;
349
- /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key), analog zu
350
- * entityEdit.fieldLabels. Die Pseudo-Entity `__projection-detail__` hat
351
- * keinen natürlichen Field-Namespace — fehlt ein Eintrag, gilt die
352
- * Konvention `<feature>:entity:__projection-detail__:field:<name>`. */
353
- readonly fieldLabels?: Readonly<Record<string, string>>;
354
- /** Parent list screen (kurze id) für eine "Zurück"-Navigation. */
355
- readonly listScreenId?: string;
356
- readonly slots?: ScreenSlots;
357
- readonly access?: AccessRule;
358
- };
359
-
360
- // --- dashboard ---
361
-
362
- // Deklaratives Panel-Grid — Kennzahlen, Verläufe und Kurzlisten ohne
363
- // Custom-JSX. Jedes Panel zieht seine Daten aus einer eigenen Query
364
- // (fully-qualified QN, cross-feature erlaubt wie projectionList).
365
- // Formatierung ist Sache des Query-Handlers: Stat-Werte kommen als
366
- // anzeigefertige Strings/Zahlen aus der Read-Projection (ES-Read-Models
367
- // shapen ihre Daten selbst; der Renderer formatiert nicht nach).
368
-
369
- // Query-Result-Contract: flaches Record; `valueField` zeigt auf den
370
- // anzeigefertigen Wert, `subField` optional auf eine Sub-Zeile,
371
- // `toneField` optional auf "default" | "positive" | "warn".
372
- export type DashboardStatPanel = {
373
- readonly kind: "stat";
374
- /** Stable id — kebab-case, eindeutig im Panel-Set. */
375
- readonly id: string;
376
- /** Anzeige-Text (i18n-Key). */
377
- readonly label: string;
378
- readonly query: string;
379
- readonly valueField: string;
380
- readonly subField?: string;
381
- readonly toneField?: string;
382
- /** Optionaler Delta-Chip (z.B. "↓23 %") neben dem Label. Nur wenn BEIDE
383
- * Felder gesetzt sind UND der Query-Handler sie liefert, rendert der Chip
384
- * — sonst bleibt die Kachel wie ohne Delta. `deltaToneField` fällt auf
385
- * `toneField`/"default" zurück, wenn ungesetzt. */
386
- readonly deltaField?: string;
387
- readonly deltaDirectionField?: string;
388
- readonly deltaToneField?: string;
389
- /** Statisches Icon neben dem Label — anders als value/sub/delta variiert
390
- * das Icon nicht pro Query-Result, sondern ist eine Author-Entscheidung
391
- * wie das Panel selbst. Aufgelöst über dieselbe extensionSectionComponents-
392
- * Registry wie custom-Panels; die registrierte Komponente ignoriert
393
- * typischerweise entityName/entityId/filterParams (kein Entity-Kontext
394
- * für ein reines Icon). */
395
- readonly icon?: PlatformComponent;
396
- /** Statischer CSS-Farbwert (z.B. "var(--color-debt)") für den Icon-Chip —
397
- * Passthrough an die Kachel, keine Registry, kein Lookup. Wirkt NUR wenn
398
- * `icon` gesetzt ist (StatCard rendert den Chip nur zusammen mit einem
399
- * Icon) — ohne icon wird der Wert still verworfen. */
400
- readonly accentColor?: string;
401
- };
402
-
403
- // Query-Result-Contract: `{ points: { atMs, value | null }[],
404
- // windowStartMs, windowEndMs }` — value=null zeichnet einen Einbruch.
405
- export type DashboardChartPanel = {
406
- readonly kind: "chart";
407
- readonly id: string;
408
- readonly label: string;
409
- /** v1: geglättete Zeitreihe. Weitere Chart-Formen additiv. */
410
- readonly chart: "timeseries";
411
- readonly query: string;
412
- };
413
-
414
- // Kurzliste im Dashboard — Query-Contract wie projectionList
415
- // (`{ rows, nextCursor, total? }`), gerendert ohne Pager/Toolbar.
416
- export type DashboardListPanel = {
417
- readonly kind: "list";
418
- readonly id: string;
419
- readonly label: string;
420
- readonly query: string;
421
- readonly columns: readonly ListColumnSpec[];
422
- };
423
-
424
- // Betitelte Sektion aus mehreren Stat-Panels (z.B. "Net Worth": Assets/Debts/
425
- // Net). Ein Nesting-Level, kein Group-of-Groups — jedes Kind bleibt ein
426
- // vollwertiges DashboardStatPanel mit eigener Query/id/label, der Renderer
427
- // zieht sie nur gemeinsam unter einen Sektions-Titel.
428
- export type DashboardStatGroupPanel = {
429
- readonly kind: "stat-group";
430
- readonly id: string;
431
- readonly label: string;
432
- readonly stats: readonly DashboardStatPanel[];
433
- };
434
-
435
- // Nicht-tabellarische Kurzliste (z.B. "nächste Termine"). Query-Result-
436
- // Contract: `{ rows: { primary: string; trailing?: string }[] }`.
437
- export type DashboardFeedPanel = {
438
- readonly kind: "feed";
439
- readonly id: string;
440
- readonly label: string;
441
- readonly query: string;
442
- readonly emptyLabel?: string;
443
- };
444
-
445
- // Liste aus Label/Wert/Fortschrittsbalken (z.B. Tilgungsfortschritt pro
446
- // Kredit). Query-Result-Contract: `{ rows: { label: string; value: string;
447
- // fraction: number }[] }` — fraction wird auf 0..1 geclampt.
448
- export type DashboardProgressListPanel = {
449
- readonly kind: "progress-list";
450
- readonly id: string;
451
- readonly label: string;
452
- readonly query: string;
453
- };
454
-
455
- // Eingehängte App-Komponente, die ihre Daten/Titel selbst verwaltet (wie ein
456
- // custom Screen, nur als Panel — bleibt an ihrer Array-Position statt in
457
- // einen separaten Slot zu wandern). Kein `query`, keine `label`: der Renderer
458
- // löst `component` über dieselbe extensionSectionComponents-Registry auf wie
459
- // entityEdit-Extension-Sections und List-Header-Slots.
460
- export type DashboardCustomPanel = {
461
- readonly kind: "custom";
462
- readonly id: string;
463
- readonly component: PlatformComponent;
464
- };
465
-
466
- export type DashboardPanelDefinition =
467
- | DashboardStatPanel
468
- | DashboardStatGroupPanel
469
- | DashboardChartPanel
470
- | DashboardListPanel
471
- | DashboardFeedPanel
472
- | DashboardProgressListPanel
473
- | DashboardCustomPanel;
474
-
475
- // Screen-weiter Picker (Combobox), dessen gewählter Wert unter `id` in JEDE
476
- // Panel-Query dieses Screens gemerged wird (Query-Handler validieren den Wert
477
- // selbst gegen die Tenant-Sicht — dies ist UX-Scoping, keine Access-Boundary).
478
- // Genau eins von `options`/`optionsQuery` ist gesetzt (Boot-Validator prüft).
479
- export type DashboardFilterDefinition = {
480
- readonly id: string;
481
- readonly label: string;
482
- readonly kind: "select";
483
- readonly placeholder?: string;
484
- /** i18n-Key für den "(alle)"-Eintrag. */
485
- readonly allLabel?: string;
486
- readonly options?: readonly { readonly value: string; readonly label: string }[];
487
- /** Query-Result-Contract: `{ rows: { value: string; label: string }[] }`. */
488
- readonly optionsQuery?: string;
489
- };
490
-
491
- export type DashboardScreenDefinition = {
492
- readonly id: string;
493
- readonly type: "dashboard";
494
- readonly panels: readonly DashboardPanelDefinition[];
495
- readonly filter?: DashboardFilterDefinition;
496
- readonly slots?: ScreenSlots;
497
- readonly access?: AccessRule;
498
- };
499
-
500
- // --- entityEdit ---
501
-
502
- // camelCase `readOnly` instead of the spec's lowercase `readonly`: TS's
503
- // `readonly` modifier on the same line would make the declaration read
504
- // `readonly readonly?: FieldCondition`, which is legal but a real lese-knick.
505
- // Mirrors React's `readOnly` prop so the ergonomic cost of the divergence
506
- // from ui-architecture.md is minimal.
507
- export type EditFieldSpec =
508
- | string
509
- | {
510
- readonly field: string;
511
- readonly span?: number;
512
- readonly visible?: FieldCondition;
513
- readonly readOnly?: FieldCondition;
514
- readonly required?: FieldCondition;
515
- readonly renderer?: FieldRenderer;
516
- };
517
-
518
- // A section is either a normal field-grid (default — `kind` omitted keeps
519
- // every existing screen-def working) or an extension slot that mounts a
520
- // feature-provided component. The extension component is resolved client-
521
- // side by name (same `__component` marker as custom screens / column
522
- // renderers) and receives the host entity name + id, so a bundled feature
523
- // (e.g. custom-fields) can load and persist its own data inside the form.
524
- export type EditSectionSpec = EditFieldsSection | EditExtensionSection;
525
-
526
- export type EditFieldsSection = {
527
- readonly kind?: "fields";
528
- /** Optional. Ohne Titel rendert die Section nur ihre Felder (keine h3-
529
- * Überschrift) — für flache Forms (Card-Titel + Felder direkt, ein
530
- * einzelner Abschnitt) wie bei den meisten shadcn-Form-Mustern. */
531
- readonly title?: string;
532
- readonly columns?: number;
533
- readonly fields: readonly EditFieldSpec[];
534
- };
535
-
536
- export type EditExtensionSection = {
537
- readonly kind: "extension";
538
- readonly title: string;
539
- readonly component: PlatformComponent;
540
- };
541
-
542
- export function isExtensionEditSection(section: EditSectionSpec): section is EditExtensionSection {
543
- return section.kind === "extension";
544
- }
545
-
546
- export type EditLayout = {
547
- readonly sections: readonly EditSectionSpec[];
548
- };
549
-
550
- export type EntityEditScreenDefinition = {
551
- readonly id: string;
552
- readonly type: "entityEdit";
553
- readonly entity: string;
554
- readonly layout: EditLayout;
555
- /** Optionaler i18n-Key (oder Roh-String) für den Submit-Button. Default
556
- * `kumiko.actions.save`. Lässt den Auto-Edit-Screen domain-spezifische
557
- * CTAs zeigen ("Save Address", "Create item") statt generisch "Speichern". */
558
- readonly submitLabel?: string;
559
- /** Default true. `false` für Entities deren Create über einen eigenen
560
- * Lifecycle-Write läuft (z.B. incident:open mit Event-Stream + Joins)
561
- * statt über `<entity>:create`: unterdrückt den automatischen
562
- * „+ Neu"-Button auf entityList-Screens dieser Entity und rendert den
563
- * Create-Branch (Aufruf ohne entityId) als Fehler statt eines Forms,
564
- * dessen Submit gegen einen nicht registrierten Handler liefe. */
565
- readonly allowCreate?: boolean;
566
- /** Default true. `false` wenn kein `<entity>:delete`-Handler existiert
567
- * (History-/Audit-Erhalt): unterdrückt den Löschen-Button im
568
- * Update-Form. */
569
- readonly allowDelete?: boolean;
570
- /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key), überschreibt
571
- * die Default-Konvention `<feature>:entity:<entity>:field:<name>`.
572
- * Primär für configEdit: dessen Pseudo-Entity `__config-edit__` hat
573
- * keinen natürlichen Field-Namespace — der Settings-Hub injiziert hier
574
- * das `mask.title`-Label des Config-Keys. Fehlt ein Eintrag, gilt die
575
- * Konvention. */
576
- readonly fieldLabels?: Readonly<Record<string, string>>;
577
- readonly slots?: ScreenSlots;
578
- readonly access?: AccessRule;
579
- };
580
-
581
- // --- actionForm ---
582
-
583
- // Form-Screen für non-CRUD Write-Handler. Wird gerendert wie ein
584
- // EntityEditScreen (sections + fields), aber:
585
- // - kein detail-fetch beim Mount (initial-state = field-defaults)
586
- // - kein CRUD-verb-mapping ("create"/"update") — Author gibt
587
- // explizit die Write-Handler-QN an
588
- // - Form-Object landet 1:1 als payload beim Handler; sein Zod-Schema
589
- // validiert weiter
590
- // - optional `redirect` zu einem anderen Screen nach Submit-Success
591
- //
592
- // Beispiele: "Send invitation" (mit recipient-email + role), "Approve
593
- // invoice" (mit notes), "Bulk-import" (mit CSV-string + mode).
594
- //
595
- // Field-Shape: inline am Screen statt entity-Reference. Author hat
596
- // explizite Kontrolle was die Form rendert ohne eine ganze Entity
597
- // dafür anzulegen. Die FieldDefinitions sind dieselben wie auf
598
- // Entities (text/select/number/...) — alle DefaultInput-Renderer
599
- // greifen unverändert.
600
- export type ActionFormScreenDefinition = {
601
- readonly id: string;
602
- readonly type: "actionForm";
603
- /** Write-Handler-QN der bei Submit gerufen wird. Form-Object landet
604
- * 1:1 als payload — Handler-Schema (Zod) validiert weiter. */
605
- readonly handler: string;
606
- /** Form-Shape: Field-Map pro Name. Nutzt dieselben FieldDefinitions
607
- * wie Entity-Felder. Mindestens ein Feld erforderlich (Boot-
608
- * Validator). */
609
- readonly fields: Readonly<Record<string, FieldDefinition>>;
610
- /** Layout analog zu EntityEditScreen: sections mit fields aus dem
611
- * fields-Map oben. */
612
- readonly layout: EditLayout;
613
- /** i18n-key für den Submit-Button. Default: i18n-Default des
614
- * Renderers (typischerweise "actions.submit"). */
615
- readonly submitLabel?: string;
616
- /** Nach erfolgreichem Submit zu dieser Screen-ID navigieren (kurze
617
- * ID, z.B. "item-list" — gleiche Feature, der nav-Router resolved
618
- * zum vollen Pfad). Cross-Feature-Redirect ist nicht supported.
619
- * Wenn nicht gesetzt, bleibt der User auf dem Form-Screen. Boot-
620
- * Validator prüft dass die ID einen registrierten Screen meint. */
621
- readonly redirect?: string;
622
- /** Ziel des Abbrechen-Buttons. Default: `redirect` (historisches
623
- * Verhalten — Cancel und Submit-Redirect landen dann am selben Ort).
624
- * `false` = kein Abbrechen-Button; richtig für Single-Action-Screens
625
- * ohne verwerfbaren Zustand (z.B. "Test-Mail senden"), wo Abbrechen
626
- * nur ein zweiter Weg zum selben Ziel wäre. Boot-Validator prüft
627
- * String-Targets wie `redirect`. */
628
- readonly cancelTarget?: string | false;
629
- readonly slots?: ScreenSlots;
630
- readonly access?: AccessRule;
631
- };
632
-
633
- // --- custom ---
634
-
635
- // Sub-route declared by a custom screen (Expo Router / URL-routing use).
636
- // `path` is the route-segment appended to the screen's own path; the
637
- // framework owns the outer routing. Components stay opaque.
638
- export type CustomScreenRoute = {
639
- readonly path: string;
640
- readonly component: PlatformComponent;
641
- };
642
-
643
- export type CustomScreenDefinition = {
644
- readonly id: string;
645
- readonly type: "custom";
646
- readonly renderer: PlatformComponent;
647
- readonly routes?: readonly CustomScreenRoute[];
648
- /** Parent list screen for breadcrumb when this detail is not in nav. */
649
- readonly listScreenId?: string;
650
- readonly access?: AccessRule;
651
- };
652
-
653
- // --- configEdit ---
654
-
655
- // Form-Screen der Tenant-/User-/System-Settings aus dem bundled
656
- // config-Feature liest und schreibt. Wird gerendert wie ein
657
- // EntityEditScreen (sections + fields), aber:
658
- // - Detail-Load via `config:query:values` (statt `<entity>:detail`)
659
- // - Pre-Fill nutzt `configKeys[shortName]` → qualifizierter Key, dann
660
- // `values[qualifiedKey].value`
661
- // - Submit feuert pro geändertem Feld einen `config:write:set` mit
662
- // {key, value, scope}; das config-feature behandelt jeden Key als
663
- // eigenes Aggregate (configValue.<keyHash>) und alle N Writes laufen
664
- // parallel (Promise.all). Per-Key idempotent → Retry safe.
665
- // - kein Singleton-Hack nötig: pro (key+tenantId) gibt's by-design
666
- // genau eine Row, der Bridge-Pattern aus dem Branding-MVP fällt weg
667
- //
668
- // Partial-Failure-Semantik: wenn von N parallelen Writes einer scheitert,
669
- // bleiben die anderen committed (pro-Aggregate, kein Multi-Stream-Rollback).
670
- // Das Form bleibt dirty bis der User retried — die schon erfolgreichen
671
- // Writes feuern dann nochmal mit demselben Wert. Für `text` / `number` /
672
- // `boolean` Keys ist das idempotent. Wer einen ConfigKey mit nicht-
673
- // idempotentem Setter baut (Counter, append-only-list o.ä.) muss die
674
- // Idempotenz im Setter sicherstellen.
675
- //
676
- // Field-Shape: inline am Screen wie bei `actionForm`. Author hat damit
677
- // explizite Kontrolle über Input-Type (text/number/select/...) ohne
678
- // Resolve-Ceremony — die FieldDefinitions sind dieselben wie auf
679
- // Entities, alle DefaultInput-Renderer greifen unverändert. Field-
680
- // Labels gehen über bestehende i18n-Konventionen (`<feature>:entity:
681
- // <namespace>:field:<name>` o.ä. — der Author wählt den Namespace).
682
- //
683
- // scope MUSS mit der `createTenantConfig`/`createSystemConfig`/
684
- // `createUserConfig`-Deklaration der referenzierten Keys
685
- // übereinstimmen — der Boot-Validator pinnt das.
686
- export type ConfigEditScreenDefinition = {
687
- readonly id: string;
688
- readonly type: "configEdit";
689
- /** scope für config:write:set Calls. Muss zur Scope-Deklaration der
690
- * in `configKeys` referenzierten Keys passen — Boot-Validator
691
- * prüft das gegen die Registry. */
692
- readonly scope: "tenant" | "system" | "user";
693
- /** Map: form-field-name (kurz, wie im Layout referenziert) → voll-
694
- * qualifizierter Config-Key (`<feature>:config:<short>`). Boot-
695
- * Validator prüft dass jeder qualifizierte Key in der Registry
696
- * bekannt ist. */
697
- readonly configKeys: Readonly<Record<string, string>>;
698
- /** Form-Shape pro Field-Name. Selbe FieldDefinitions wie auf
699
- * Entities/ActionForm. Field-Names matchen die Keys in `configKeys`
700
- * — Boot-Validator pinnt das. */
701
- readonly fields: Readonly<Record<string, FieldDefinition>>;
702
- /** Layout: Sections mit Field-Refs. Identisch zu entityEdit/
703
- * actionForm. */
704
- readonly layout: EditLayout;
705
- /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key). Der
706
- * Settings-Hub setzt hier `mask.title` des jeweiligen Config-Keys,
707
- * damit das am Key deklarierte Label am generierten Feld erscheint —
708
- * ohne es unter der `__config-edit__`-Konvention zu duplizieren.
709
- * Fehlt ein Eintrag, gilt die Konvention. */
710
- readonly fieldLabels?: Readonly<Record<string, string>>;
711
- /** i18n-key für den Submit-Button. Default: "kumiko.actions.save". */
712
- readonly submitLabel?: string;
713
- readonly slots?: ScreenSlots;
714
- readonly access?: AccessRule;
715
- };
716
-
717
- // --- shared slots (Level 4 from ui-architecture.md) ---
718
-
719
- export type ScreenSlots = {
720
- readonly header?: PlatformComponent;
721
- readonly beforeForm?: PlatformComponent;
722
- readonly afterForm?: PlatformComponent;
723
- readonly sidebar?: PlatformComponent;
724
- readonly footer?: PlatformComponent;
725
- readonly toolbar?: PlatformComponent;
726
- };
727
-
728
- // --- discriminated union ---
729
-
730
- // Inline nav-entry sugar for `r.screen({ ..., nav: {...} })` — covers the
731
- // common case of "one nav entry pointing at this screen". The nav entry's
732
- // `id`/`screen` are synthesized from the screen's own id; for anything
733
- // beyond label/icon/parent/order (access-gating, workspaces, actions),
734
- // declare a standalone `r.nav()` entry instead.
735
- export type ScreenNavSugar = {
736
- readonly label: string;
737
- readonly icon?: string;
738
- readonly parent?: string;
739
- readonly order?: number;
740
- };
741
-
742
- export type ScreenDefinition = (
743
- | EntityListScreenDefinition
744
- | ProjectionListScreenDefinition
745
- | ProjectionDetailScreenDefinition
746
- | DashboardScreenDefinition
747
- | EntityEditScreenDefinition
748
- | ActionFormScreenDefinition
749
- | ConfigEditScreenDefinition
750
- | CustomScreenDefinition
751
- ) & { readonly nav?: ScreenNavSugar };
752
-
753
- // Type guard — narrows FieldRenderer to FormatSpec. Useful for renderer
754
- // authors who branch on the three FieldRenderer variants without manual
755
- // "format" in renderer checks.
756
- export function isFormatSpec(r: unknown): r is FormatSpec {
757
- return typeof r === "object" && r !== null && "format" in r && typeof r.format === "string";
758
- }
759
-
760
- // Collapse the string-shorthand into the object form. Both the boot-validator
761
- // and (later) ui-core's view-model builder iterate over fields/columns — the
762
- // helper keeps that loop from growing two branches everywhere.
763
- export function normalizeListColumn(c: ListColumnSpec): Exclude<ListColumnSpec, string> {
764
- const col = typeof c === "string" ? { field: c } : c;
765
- if (
766
- typeof process !== "undefined" &&
767
- process.env.NODE_ENV !== "production" &&
768
- col.renderer !== undefined &&
769
- typeof col.renderer === "function"
770
- ) {
771
- // biome-ignore lint/suspicious/noConsole: dev-only warning
772
- console.warn(
773
- `[kumiko] normalizeListColumn: Feld "${col.field}" hat einen Funktions-Renderer — dieser wird von JSON.stringify verworfen. Bitte auf FormatSpec ({ format: "..." }) migrieren.`,
774
- );
775
- }
776
- return col;
777
- }
778
-
779
- /** Evaluates a declarative FieldCondition against the current row/form
780
- * values. THE single implementation — renderer (row-action visibility),
781
- * headless view-model (visible/readOnly/required) and render-edit
782
- * (form-condition closures) reuse it; three hand-rolled copies had
783
- * already drifted in shape. */
784
- export function evalFieldCondition(cond: FieldCondition, values: Record<string, unknown>): boolean {
785
- if (typeof cond === "boolean") return cond;
786
- const val = values[cond.field];
787
- if ("eq" in cond) return val === cond.eq;
788
- return val !== cond.ne;
789
- }
790
-
791
- export function normalizeEditField(f: EditFieldSpec): Exclude<EditFieldSpec, string> {
792
- return typeof f === "string" ? { field: f } : f;
793
- }
1
+ // Legacy path re-exported for callers still importing this module directly.
2
+ export * from "@cosmicdrift/kumiko-types/screen";