@cosmicdrift/kumiko-types 0.383.1 → 0.385.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.
package/dist/screen.d.ts CHANGED
@@ -1,13 +1,25 @@
1
- import type { FieldIconKey } from "./field-icon.ts";
2
- import type { FieldCondition, FieldDefinition, FormFieldDefinition, SelectOptionTone } from "./fields.ts";
3
- import type { AccessRule, AgentScreenHints } from "./handlers.ts";
4
- import type { IconKey, NavIconKey } from "./nav-icon.ts";
1
+ import type { NavIconKey } from "./nav-icon.ts";
2
+ import type { ActionFormScreenDefinition, SecretMintScreenDefinition } from "./screen-action-form.ts";
3
+ import type { ConfigEditScreenDefinition, CustomScreenDefinition, SecretsEditScreenDefinition } from "./screen-custom-config.ts";
4
+ import type { DashboardScreenDefinition } from "./screen-dashboard.ts";
5
+ import type { EntityEditScreenDefinition } from "./screen-entity-edit.ts";
6
+ import type { EntityListScreenDefinition } from "./screen-entity-list.ts";
7
+ import type { ProjectionDetailScreenDefinition, ProjectionListScreenDefinition } from "./screen-projection.ts";
5
8
  export type { FieldIconKey } from "./field-icon.ts";
6
9
  export type PlatformComponent = {
7
10
  readonly react?: unknown;
8
11
  readonly native?: unknown;
9
12
  };
10
13
  export type UnitKey = "m2" | "km" | "m" | "kg" | "percent" | "mi";
14
+ type NumberFormatOptions = {
15
+ readonly locale?: string;
16
+ readonly grouping?: boolean;
17
+ /** Intl `minimumFractionDigits` / `maximumFractionDigits` (integers 0..20,
18
+ * min <= max). Without them Intl cuts at 3 digits, so 0.0004 renders as
19
+ * "0". Invalid values are ignored. */
20
+ readonly minimumFractionDigits?: number;
21
+ readonly maximumFractionDigits?: number;
22
+ };
11
23
  export interface FieldFormatRegistry {
12
24
  timestamp: {
13
25
  readonly locale?: string;
@@ -33,14 +45,9 @@ export interface FieldFormatRegistry {
33
45
  };
34
46
  /** `grouping: false` renders without thousands-separators (e.g. a model
35
47
  * year: "2021", not "2.021"). Default `true`. */
36
- number: {
37
- readonly locale?: string;
38
- readonly grouping?: boolean;
39
- };
40
- decimal: {
41
- readonly locale?: string;
42
- readonly grouping?: boolean;
43
- };
48
+ number: NumberFormatOptions;
49
+ /** Same options as `number`. */
50
+ decimal: NumberFormatOptions;
44
51
  bigInt: {
45
52
  readonly locale?: string;
46
53
  readonly grouping?: boolean;
@@ -75,1879 +82,6 @@ export type FormatSpec = {
75
82
  }[keyof FieldFormatRegistry];
76
83
  export type FieldRenderer = PlatformComponent | string | FormatSpec;
77
84
  export type { FieldCondition } from "./fields.ts";
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 a column on a query-backed list (projectionList, relatedList,
92
- * dashboard list, expandableRow) as drawn by a renderer rather than read
93
- * from a row field. Without it, a query that declares an `outputSchema`
94
- * must return `field`; a `label` alone does not exempt the column, since
95
- * nearly every column carries one. */
96
- readonly virtual?: boolean;
97
- /** Marks this column as a reference lookup instead of a plain value —
98
- * for projectionList/relatedList columns, which have no
99
- * `EntityDefinition` to carry a real `reference` field type. Same
100
- * target convention as `parseRefTarget`: an entity name (same
101
- * feature) or `feature:entity` (cross-feature). Ignored on
102
- * `entityList` columns, where the field's own declared type always
103
- * wins. */
104
- readonly refEntity?: string;
105
- /** Row field on the referenced entity shown as the label (default
106
- * "id"). Only meaningful together with `refEntity`. */
107
- readonly refLabelField?: string;
108
- /** Marks this column as sortable via a clickable header — for
109
- * relatedList columns, which have no `EntityDefinition` field to carry
110
- * `sortable` on. Ignored on `entityList` columns, where the entity
111
- * field's own `sortable` flag wins. */
112
- readonly sortable?: boolean;
113
- /** Value type of a relatedList column, which has no `EntityDefinition`
114
- * field to derive it from. Numeric types right-align the column and its
115
- * header with tabular figures, like an entityList column of that field
116
- * type. Ignored on entityList columns, where the field's own type wins. */
117
- readonly valueType?: "number" | "decimal" | "bigInt" | "money";
118
- /** Leaves the column out of the compact card layout used below the
119
- * `md` breakpoint (tables keep it). For columns that only make sense
120
- * next to the others, like a sort key. */
121
- readonly hideOnNarrow?: boolean;
122
- /** Dashboard list panels only. "bar": field holds a 0..1 fraction, drawn
123
- * as bar + percent. "badge": field holds a label, drawn as a pill whose
124
- * tone comes from `badgeToneField`. "datetime": field holds an ISO string or
125
- * epoch ms, shown in the user's locale and time zone. Other list renderers
126
- * ignore it. */
127
- readonly display?: ListColumnDisplay;
128
- /** Row field naming the badge tone; only with `display: "badge"`. */
129
- readonly badgeToneField?: string;
130
- };
131
- export type ListColumnDisplay = "bar" | "badge" | "datetime";
132
- export type ListBadgeTone = "positive" | "warn" | "negative" | "neutral";
133
- export type ListPaginationMode = "pages" | "infinite" | false;
134
- export type ListSortDir = "asc" | "desc";
135
- export type ListSortSpec = {
136
- readonly field: string;
137
- readonly dir: ListSortDir;
138
- };
139
- export type ScreenFilterOp = "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
140
- export type ScreenFilter = {
141
- readonly field: string;
142
- readonly op: ScreenFilterOp;
143
- readonly value: unknown;
144
- };
145
- /** Declarative row-field extractor — JSON-safe (no function prop, survives
146
- * GET /api/schema / buildAppSchema).
147
- *
148
- * `pick`: extracts fields 1:1. `{ pick: ["id", "version"] }` → `{ id: row.id, version: row.version }`
149
- * `map`: renames fields. `{ map: { incidentId: "id" } }` → `{ incidentId: row.id }`
150
- *
151
- * Limitation: computed/templated values can't be expressed this way —
152
- * that logic belongs server-side in the write handler. */
153
- export type RowFieldExtractor = {
154
- readonly pick: readonly string[];
155
- } | {
156
- readonly map: Readonly<Record<string, string>>;
157
- };
158
- /** How a row action renders inline in the row-action column. `"button"` is a
159
- * bordered button (its colour follows the action's `style`), `"link"` a text
160
- * link, `"icon"` an icon-only button (needs a resolved icon, falls back to
161
- * `"link"` without one). `"responsive"` is a `"button"` with icon and label
162
- * from tablet width up and an icon-only button below it, the label staying the
163
- * accessible name (needs a resolved icon, falls back to `"button"` without
164
- * one). Unset: the renderer decides (primary action as link, the rest in the
165
- * kebab menu). */
166
- export type RowActionDisplay = "button" | "link" | "icon" | "responsive";
167
- /** Row-action column layout. `"adaptive"` (default): primary action as link,
168
- * the rest in a kebab menu — except actions with an explicit `display`, which
169
- * stay inline. `"inline"`: every action inline, no kebab. */
170
- export type RowActionMode = "adaptive" | "inline";
171
- export type RowAction = RowActionWriteHandler | RowActionNavigate | RowActionDrawer | RowActionClientAction;
172
- export type RowActionWriteHandler = {
173
- /** Default für RowActions ohne explizit gesetzten `kind` —
174
- * Backwards-kompatible Form. Kann auch explizit gesetzt werden. */
175
- readonly kind?: "writeHandler";
176
- /** Stable id pro Screen — kebab-case, eindeutig im Action-Set. */
177
- readonly id: string;
178
- /** Anzeige-Text (i18n-Key). */
179
- readonly label: string;
180
- /** Qualified-Name des Server-Handlers, z.B.
181
- * "publicstatus:write:maintenance:start". Wird via useDispatcher
182
- * dispatcht. */
183
- readonly handler: string;
184
- /** Deklarativer Payload pro Row. Default = `{ id: row.id }`.
185
- * `pick` extrahiert Felder gleichen Namens; `map` benennt um. */
186
- readonly payload?: RowFieldExtractor;
187
- /** i18n-Key für die Confirm-Dialog-Description. Wenn gesetzt, öffnet
188
- * ein Modal vor der Ausführung — der User muss explizit bestätigen.
189
- * Zusammen mit `style: "danger"` ist das die Standard-Sicherheits-
190
- * Garde für destruktive Aktionen. */
191
- readonly confirm?: string;
192
- /** i18n-Key für den Confirm-Button-Text im Dialog. Default = `label`
193
- * (also "Delete" → "Delete"-Button im Confirm). Setzen wenn die
194
- * Action einen langen Namen hat ("Mark Subscription as Cancelled")
195
- * und der Button kürzer sein soll ("Cancel Subscription"). */
196
- readonly confirmLabel?: string;
197
- /** Conditional Visibility pro Row. */
198
- readonly visible?: FieldCondition;
199
- /** Visual-Style. "danger" rendert rot + erzwingt einen Confirm-
200
- * Dialog (auch ohne expliziten `confirm`-Key). */
201
- readonly style?: "primary" | "secondary" | "danger";
202
- /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
203
- * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
204
- * (renderer/web), analogous to EditFieldSpec.icon. */
205
- readonly icon?: IconKey;
206
- /** Inline render form in the row-action column — see `RowActionDisplay`. */
207
- readonly display?: RowActionDisplay;
208
- /** Where to go after the write succeeded instead of staying on the shown
209
- * record. Same forms as entityEdit `redirect`. Honored on record actions
210
- * (projectionDetail header/section actions, entityEdit header/section
211
- * actions); a valid `returnTo` wins under the same rule as
212
- * entityEdit.redirect. Not used by list row actions. Without it, an action
213
- * deleting the shown record (`<feature>:write:<entity>:delete` with that
214
- * record's id) leaves to `returnTo`/the list; any other action refetches. */
215
- readonly redirect?: string | ActionFormRedirect;
216
- };
217
- export type RowActionNavigateBase = {
218
- readonly kind: "navigate";
219
- readonly id: string;
220
- readonly label: string;
221
- /** Feldname dessen Wert als entityId in den URL-Pfad eingebettet wird
222
- * (`/<workspace>/<screen>/<entityId>`). entityEdit liest die Id
223
- * AUSSCHLIESSLICH aus dem Pfad. Default: "id" wenn der Ziel-Screen ein
224
- * entityEdit ist ODER wenn `entity` (statt `screen`) gesetzt ist. */
225
- readonly entityId?: string;
226
- /** Declarative URL search params extracted from the clicked row.
227
- * Both actionForm and entityEdit-create targets read `params` as
228
- * initial values (pre-filling the form). entityEdit-update does
229
- * not — the existing record's values take precedence.
230
- * `pick` extracts fields of the same name; `map` renames them. */
231
- readonly params?: RowFieldExtractor;
232
- /** Section id of the tab to activate on the target projectionDetail
233
- * (layout.mode "tabs"); merged into the navigate search params next to
234
- * returnTo. Boot validator checks the id exists on the target. */
235
- readonly tab?: string;
236
- /** Conditional Visibility pro Row. */
237
- readonly visible?: FieldCondition;
238
- /** Visual style only — unlike the writeHandler variants, "danger" does not
239
- * force a confirm dialog here: the target form is the confirmation. */
240
- readonly style?: "primary" | "secondary" | "danger";
241
- /** Wenn true, löst ein Klick auf die ganze Zeile (nicht nur das Aktionsmenü)
242
- * diese navigate-Action aus. Max. eine pro Liste (Boot-Validator prüft). Nur
243
- * auf navigate — ein Row-Klick darf keinen (evtl. destruktiven, unbestätigten)
244
- * Write auslösen, daher nicht auf writeHandler-Actions. */
245
- readonly rowClick?: boolean;
246
- /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
247
- * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
248
- * (renderer/web), analogous to EditFieldSpec.icon. */
249
- readonly icon?: IconKey;
250
- /** Inline render form in the row-action column — see `RowActionDisplay`. */
251
- readonly display?: RowActionDisplay;
252
- };
253
- /** Exactly one of `screen` / `entity` — mutual exclusivity is type-enforced
254
- * (boot validator still rejects both/neither for untyped schemas). */
255
- export type RowActionNavigate = RowActionNavigateBase & ({
256
- /** Screen-id (kurz, unqualified). Boot-Validator prüft Existenz im
257
- * selben Feature. */
258
- readonly screen: string;
259
- readonly entity?: never;
260
- } | {
261
- /** Entity-Name statt Screen-Id (fw#2228) — löst zur Boot-Zeit auf den
262
- * Screen auf, der `detailFor: "<entity>"` deklariert (siehe ObjectTarget/
263
- * resolveTarget in @cosmicdrift/kumiko-renderer's nav.tsx). */
264
- readonly entity: string;
265
- readonly screen?: never;
266
- });
267
- export type RowActionDrawer = {
268
- readonly kind: "drawer";
269
- readonly id: string;
270
- readonly label: string;
271
- /** Short, unqualified id of an `actionForm` screen in the same feature.
272
- * Boot validator rejects a missing screen and a screen whose `type`
273
- * isn't `actionForm` — same check as ToolbarAction's drawer variant
274
- * (see docs/plans/bundled-features-screen-standardisierung.md §2.6c). */
275
- readonly screen: string;
276
- /** Declarative prefill extracted from the clicked row. `pick` extracts
277
- * fields of the same name; `map` renames them. */
278
- readonly params?: RowFieldExtractor;
279
- /** Per-row conditional visibility. */
280
- readonly visible?: FieldCondition;
281
- /** Visual style only — unlike the writeHandler variants, "danger" does not
282
- * force a confirm dialog here: the target form is the confirmation. */
283
- readonly style?: "primary" | "secondary" | "danger";
284
- /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
285
- * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
286
- * (renderer/web), analogous to EditFieldSpec.icon. */
287
- readonly icon?: IconKey;
288
- /** Inline render form in the row-action column — see `RowActionDisplay`. */
289
- readonly display?: RowActionDisplay;
290
- /** Drawer heading (i18n key) instead of the action `label`. `{name}`
291
- * placeholders resolve from the drawer prefill (`params`), formatted for
292
- * display like `ActionFormScreenDefinition.summary`. */
293
- readonly title?: string;
294
- /** Line under the drawer heading (i18n key or a bare `{name}` template),
295
- * resolved like `title`. */
296
- readonly subtitle?: string;
297
- /** Clicking the row opens this drawer with that row's `params`. At most one
298
- * `rowClick` action (navigate or drawer) per list; rejected on
299
- * projectionDetail header actions, like navigate's. */
300
- readonly rowClick?: boolean;
301
- };
302
- export type RowActionClientAction = {
303
- readonly kind: "clientAction";
304
- readonly id: string;
305
- readonly label: string;
306
- /** Registered client handler name, `<feature>:<kebab>`, e.g.
307
- * "auth-email-password:request-password-reset". Resolved when the action is
308
- * triggered; a name no client feature registered shows an error toast. */
309
- readonly action: string;
310
- /** Payload handed to the handler, picked from the row (or the shown record).
311
- * Default `{ id: row.id }`, like a writeHandler. */
312
- readonly payload?: RowFieldExtractor;
313
- /** i18n key for the confirm dialog description. */
314
- readonly confirm?: string;
315
- /** i18n key for the confirm button, default `label`. */
316
- readonly confirmLabel?: string;
317
- /** Conditional visibility per row. */
318
- readonly visible?: FieldCondition;
319
- /** Visual style. "danger" forces a confirm dialog, like a writeHandler. */
320
- readonly style?: "primary" | "secondary" | "danger";
321
- /** Overrides the id-based default icon, see `RowActionWriteHandler.icon`. */
322
- readonly icon?: IconKey;
323
- /** Inline render form in the row-action column — see `RowActionDisplay`. */
324
- readonly display?: RowActionDisplay;
325
- /** i18n key of the toast shown after the handler reported `{ ok: true }`
326
- * (default `kumiko.clientAction.success`). */
327
- readonly successMessage?: string;
328
- };
329
- export type ToolbarAction = {
330
- readonly kind: "navigate";
331
- readonly id: string;
332
- readonly label: string;
333
- /** Screen-id (kurz, unqualified) zu dem navigiert wird. */
334
- readonly screen: string;
335
- /** Section id of the tab to activate on the target projectionDetail
336
- * (layout.mode "tabs"); merged into the navigate search params next to
337
- * returnTo. Boot validator checks the id exists on the target. */
338
- readonly tab?: string;
339
- /** Visual style only — unlike the writeHandler variant, "danger" does
340
- * not force a confirm dialog here: the target form is the
341
- * confirmation. */
342
- readonly style?: "primary" | "secondary" | "danger";
343
- } | {
344
- readonly kind: "writeHandler";
345
- readonly id: string;
346
- readonly label: string;
347
- readonly handler: string;
348
- /** Statischer Payload ohne row-Context. Default = `{}`. */
349
- readonly payload?: Record<string, unknown>;
350
- /** i18n-Key für Confirm-Dialog-Description. Wenn gesetzt UND/ODER
351
- * style="danger": Modal vor der Ausführung. */
352
- readonly confirm?: string;
353
- /** i18n-Key für Confirm-Button-Text im Dialog. Default = `label`. */
354
- readonly confirmLabel?: string;
355
- readonly style?: "primary" | "secondary" | "danger";
356
- } | {
357
- readonly kind: "drawer";
358
- readonly id: string;
359
- readonly label: string;
360
- /** Short, unqualified id of an `actionForm` screen in the same
361
- * feature. Boot validator rejects a missing screen and a screen
362
- * whose `type` isn't `actionForm` — the caller decides full-page
363
- * vs. drawer, not the form (see docs/plans/bundled-features-screen-
364
- * standardisierung.md §2.6c). */
365
- readonly screen: string;
366
- /** Visual style only — unlike the writeHandler variant, "danger" does
367
- * not force a confirm dialog here: the target form is the
368
- * confirmation. */
369
- readonly style?: "primary" | "secondary" | "danger";
370
- /** Drawer heading (i18n key) instead of `label`. */
371
- readonly title?: string;
372
- /** Line under the drawer heading (i18n key). */
373
- readonly subtitle?: string;
374
- };
375
- export type RelatedListToolbarAction = (Extract<ToolbarAction, {
376
- readonly kind: "navigate";
377
- }> & {
378
- /** Conditional visibility, evaluated against the relatedList's parent
379
- * record (the "Akte"). */
380
- readonly visible?: FieldCondition;
381
- /** Declarative URL search params extracted from the parent record,
382
- * prefilling the target screen. Replaces the implicit
383
- * `{ [parentParam]: parentId }` default when set. */
384
- readonly params?: RowFieldExtractor;
385
- }) | (Extract<ToolbarAction, {
386
- readonly kind: "writeHandler";
387
- }> & {
388
- readonly visible?: FieldCondition;
389
- }) | (Extract<ToolbarAction, {
390
- readonly kind: "drawer";
391
- }> & {
392
- readonly visible?: FieldCondition;
393
- });
394
- /** Expandable area of an `entityList` row: a relatedList of the row's record.
395
- * `id` (tab id) and `countField` (tab badge) have no meaning outside a tabs
396
- * layout and are therefore omitted. `kind` stays the discriminator so further
397
- * expansion kinds can be added without a breaking change. */
398
- export type EntityListExpandableRow = Omit<EditRelatedListSection, "id" | "countField">;
399
- /** An extra choice on a facet next to the field's own options: a named
400
- * bundle of values ("Running" = several statuses) or "no filter" (`values:
401
- * []`, e.g. "All"). Chips only. */
402
- export type EntityListFacetExtraOption = {
403
- readonly id: string;
404
- /** i18n key. */
405
- readonly label: string;
406
- /** Option values this choice selects; `[]` selects nothing, i.e. no filter. */
407
- readonly values: readonly string[];
408
- /** Where the chip sits relative to the field's own options. Default "start". */
409
- readonly position?: "start" | "end";
410
- };
411
- export type EntityListFacetConfig = {
412
- /** `"select"` (default): dropdown with checkboxes. `"chips"`: one toggle
413
- * chip per option, single choice. */
414
- readonly display?: "select" | "chips";
415
- /** Chips show how many rows each option matches (other facets and the
416
- * search apply). Counts come from the list query itself, one `limit: 1`
417
- * count request per chip. */
418
- readonly showCounts?: boolean;
419
- /** Chips with zero matching rows are not shown. Needs counts; implies
420
- * `showCounts`'s requests even when the number itself is not shown. */
421
- readonly hideEmpty?: boolean;
422
- readonly extraOptions?: readonly EntityListFacetExtraOption[];
423
- };
424
- export type EntityListScreenDefinition = {
425
- readonly id: string;
426
- readonly type: "entityList";
427
- /** Default true: the screen fills the shell content height, its body scrolls
428
- * inside and the pager / action bar stays pinned. `false` restores page
429
- * scroll (table grows with its rows, pager below the last row, form footer
430
- * in the flow). */
431
- readonly fillHeight?: boolean;
432
- readonly nav?: ScreenNavSugar;
433
- readonly detailFor?: string;
434
- readonly description?: string;
435
- readonly agent?: AgentScreenHints;
436
- /** Screen has no nav entry by design (opened via link or navved by the
437
- * app); exempts it from the nav-area boot check. */
438
- readonly dormant?: boolean;
439
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
440
- readonly visibleWhen?: DashboardPanelVisibility;
441
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
442
- readonly fallback?: string;
443
- readonly entity: string;
444
- /** Short screen id (same feature) the create button opens instead of the entity's default edit screen. */
445
- readonly createScreen?: string;
446
- /** Set by the role projection when `createScreen` is not reachable for the role: the renderer then shows no create button instead of falling back to the entity's edit screen. Not for app authors. */
447
- readonly createUnavailable?: boolean;
448
- /** `false` hides this list's "+ New" button (and the empty-state create CTA) while the entity's edit screen keeps its create branch for other entry points, e.g. a prefilled action. Default `true`. */
449
- readonly allowCreate?: boolean;
450
- readonly columns: readonly ListColumnSpec[];
451
- readonly rowRenderer?: PlatformComponent;
452
- readonly cardRenderer?: PlatformComponent;
453
- /** Per-row actions, rendered as an actions column on the right.
454
- * Adaptive (default, see `rowActionMode`): the primary action as a link,
455
- * the rest in the kebab menu; actions with an explicit `display` stay
456
- * inline. Array order is UI order. */
457
- readonly rowActions?: readonly RowAction[];
458
- /** Layout of the row-action column — see `RowActionMode`. */
459
- readonly rowActionMode?: RowActionMode;
460
- /** Toolbar-Aktionen (List-Header). "Open Incident", "Schedule Maintenance"
461
- * etc. — neben "+ Neu" wenn vorhanden. Reihenfolge im Array = UI-
462
- * Reihenfolge, primary-style links. */
463
- readonly toolbarActions?: readonly ToolbarAction[];
464
- /** Expandable area under each row: a relatedList of the row's record
465
- * (parentId = the row's `id`). Rows get a toggle button; several rows can
466
- * be open at once. Its own row actions run against the sub-list rows, and
467
- * a successful write also reloads this list. */
468
- readonly expandableRow?: EntityListExpandableRow;
469
- /** Server-side Filter, fest am Screen — drei Buckets derselben
470
- * Entity ohne Custom-Pages (z.B. "Upcoming" / "Active" / "Past"
471
- * Maintenance). User-side q-Search läuft AUF dem gefilterten Set
472
- * oben drauf. Boot-Validator pinst dass field in der Entity existiert. */
473
- readonly filter?: ScreenFilter;
474
- readonly pagination?: ListPaginationMode;
475
- readonly pageSize?: number;
476
- readonly defaultSort?: ListSortSpec;
477
- /** Initial facet selection, per filterable field: option values (string
478
- * facets) or a boolean. Applies only while the URL carries no value for
479
- * that field, so a link or the user's own choice wins; picking "no filter"
480
- * (an `extraOptions` entry with `values: []`, or clearing the facet) is
481
- * remembered in the URL and is not overridden again. */
482
- readonly defaultFilters?: Readonly<Record<string, readonly string[] | boolean>>;
483
- /** Per-facet display options for the entity's `filterable` fields, keyed by
484
- * field name. `false` hides the facet's control (its filter stays usable
485
- * through `defaultFilters` and the URL). Fields without an entry keep the
486
- * default dropdown. */
487
- readonly facets?: Readonly<Record<string, EntityListFacetConfig | false>>;
488
- /** Replaces the default empty-list title/hint; the create button stays when the list can create. */
489
- readonly emptyState?: ListEmptyStateSpec;
490
- /** i18n key for the primary create button label (default `kumiko.actions.create`). */
491
- readonly createLabel?: string;
492
- /** i18n key for the search field placeholder (default `kumiko.list.search-placeholder`). */
493
- readonly searchPlaceholder?: string;
494
- readonly searchable?: boolean;
495
- readonly slots?: ScreenSlots;
496
- readonly access?: AccessRule;
497
- };
498
- export type ListFacetSpec = {
499
- readonly field: string;
500
- readonly type: "select";
501
- readonly label: string;
502
- readonly options: readonly {
503
- readonly value: string;
504
- readonly label: string;
505
- }[];
506
- } | {
507
- readonly field: string;
508
- readonly type: "boolean";
509
- readonly label: string;
510
- readonly trueLabel: string;
511
- readonly falseLabel: string;
512
- } | {
513
- readonly field: string;
514
- readonly type: "reference";
515
- readonly label: string;
516
- /** Entity name (same feature) or `feature:entity` (cross-feature), same
517
- * convention as `ListColumnSpec.refEntity`. Options load at render time. */
518
- readonly entity: string;
519
- /** Row field on the referenced entity shown as the option label
520
- * (default "id"). */
521
- readonly labelField?: string;
522
- } | {
523
- /** Declarative only — the date field the range narrows. It is NOT sent
524
- * to the server (the bounds go under `params`); it namespaces the
525
- * facet's URL keys and tells a reading agent which column this list
526
- * is time-rangeable on. */
527
- readonly field: string;
528
- readonly type: "dateRange";
529
- readonly label: string;
530
- /** Query-payload keys the chosen bounds are sent under as ISO instants.
531
- * Explicit rather than a `from`/`to` convention — a query is free to
532
- * name its time bounds anything, and the boot-validator checks these
533
- * keys against the handler's Zod schema. */
534
- readonly params: {
535
- readonly from: string;
536
- readonly to: string;
537
- };
538
- };
539
- export type ProjectionListScreenDefinition = {
540
- readonly id: string;
541
- readonly type: "projectionList";
542
- /** Default true: the screen fills the shell content height, its body scrolls
543
- * inside and the pager / action bar stays pinned. `false` restores page
544
- * scroll (table grows with its rows, pager below the last row, form footer
545
- * in the flow). */
546
- readonly fillHeight?: boolean;
547
- readonly nav?: ScreenNavSugar;
548
- readonly detailFor?: string;
549
- readonly description?: string;
550
- readonly agent?: AgentScreenHints;
551
- /** Screen has no nav entry by design (opened via link or navved by the
552
- * app); exempts it from the nav-area boot check. */
553
- readonly dormant?: boolean;
554
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
555
- readonly visibleWhen?: DashboardPanelVisibility;
556
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
557
- readonly fallback?: string;
558
- readonly query: string;
559
- readonly columns: readonly ListColumnSpec[];
560
- readonly rowRenderer?: PlatformComponent;
561
- readonly cardRenderer?: PlatformComponent;
562
- readonly rowActions?: readonly RowAction[];
563
- /** Layout of the row-action column — see `RowActionMode`. */
564
- readonly rowActionMode?: RowActionMode;
565
- readonly toolbarActions?: readonly ToolbarAction[];
566
- readonly pagination?: ListPaginationMode;
567
- readonly pageSize?: number;
568
- readonly defaultSort?: ListSortSpec;
569
- /** Replaces the default empty-list title/hint; the create button stays when the list can create. */
570
- readonly emptyState?: ListEmptyStateSpec;
571
- /** i18n key for the primary create button label (default `kumiko.actions.create`). */
572
- readonly createLabel?: string;
573
- /** i18n key for the search field placeholder (default `kumiko.list.search-placeholder`). */
574
- readonly searchPlaceholder?: string;
575
- readonly searchable?: boolean;
576
- /** Derived by buildAppSchema from the query handler's Zod schema (`sort`
577
- * param present). Same type serves author and wire schema (no separate
578
- * client-facing screen type exists), so this stays structurally
579
- * writable — validateProjectionListScreens rejects any hand-authored
580
- * value at boot (fw#2165). */
581
- readonly sortable?: boolean;
582
- /** Derived by buildAppSchema from the query handler's Zod schema (`cursor`
583
- * or `offset` param present). See `sortable` doc for why this is
584
- * boot-enforced rather than type-enforced. */
585
- readonly paginated?: boolean;
586
- /** Server-side filter, fixed on the screen — see `ScreenFilter` doc above
587
- * (entityList). Field existence can't be checked without an entity; the
588
- * boot-validator only checks structure (`op:"in"` ⇒ array value). */
589
- readonly filter?: ScreenFilter;
590
- /** User-toggleable facet dropdowns — see `ListFacetSpec` doc. `field`
591
- * must be a declared column, except for `type: "reference"`: it filters
592
- * by id through the bound query's `filters` and needs a registered
593
- * `entity` instead. The bound query handler must accept `filters` in its
594
- * Zod schema — the boot-validator checks both. A
595
- * "dateRange" facet instead needs its two `params` keys in that schema;
596
- * it sends no `filters` entry. */
597
- readonly facets?: readonly ListFacetSpec[];
598
- readonly slots?: ScreenSlots;
599
- readonly access?: AccessRule;
600
- };
601
- export type RecordHeaderSubtitlePart = string | {
602
- readonly field: string;
603
- readonly navigate?: MetricNavigate;
604
- };
605
- export type RecordHeaderSpec = {
606
- readonly title: string;
607
- /** Field name, or several parts (field name or `{ field, navigate }`)
608
- * joined by a separator; parts with an empty value are dropped. */
609
- readonly subtitle?: string | readonly RecordHeaderSubtitlePart[];
610
- readonly status?: string;
611
- /** Record field holding an absolute http(s) URL. When present and the
612
- * field's value is such a URL, the subtitle renders as an external link
613
- * (`target="_blank"`) instead of plain text. Only valid with the string
614
- * form of `subtitle`. */
615
- readonly subtitleHref?: string;
616
- /** Record field whose value selects the `statusTones` entry, for a badge text that
617
- * carries more than the state (e.g. "Terminated on 31.03.2027"). Defaults to `status`. */
618
- readonly statusToneField?: string;
619
- /** Badge tone per `statusToneField` value (default: the `status` value); unlisted values fall back to the value heuristic. */
620
- readonly statusTones?: {
621
- readonly [statusValue: string]: SelectOptionTone;
622
- };
623
- };
624
- export type MetricNavigate = {
625
- readonly screen?: string;
626
- readonly entity?: string;
627
- readonly entityId?: string;
628
- readonly params?: RowFieldExtractor;
629
- /** Section id of the tab to activate at the destination. Alone (no
630
- * `screen`/`entity`), it activates that tab on the current record
631
- * instead of navigating away. */
632
- readonly tab?: string;
633
- };
634
- export type MetricSpec = {
635
- readonly field: string;
636
- readonly label: string;
637
- readonly navigate?: MetricNavigate;
638
- };
639
- export type ProjectionDetailScreenDefinition = {
640
- readonly id: string;
641
- readonly type: "projectionDetail";
642
- /** Default true: the screen fills the shell content height, its body scrolls
643
- * inside and the pager / action bar stays pinned. `false` restores page
644
- * scroll (table grows with its rows, pager below the last row, form footer
645
- * in the flow). */
646
- readonly fillHeight?: boolean;
647
- readonly nav?: ScreenNavSugar;
648
- readonly detailFor?: string;
649
- readonly description?: string;
650
- readonly agent?: AgentScreenHints;
651
- /** Screen has no nav entry by design (opened via link or navved by the
652
- * app); exempts it from the nav-area boot check. */
653
- readonly dormant?: boolean;
654
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
655
- readonly visibleWhen?: DashboardPanelVisibility;
656
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
657
- readonly fallback?: string;
658
- readonly query: string;
659
- /** Query output field whose value titles the page header (breadcrumb) when no `header` is set. */
660
- readonly recordTitleField?: string;
661
- /** Query-payload key for the row-id. Default "id". */
662
- readonly idParam?: string;
663
- /** The server determines the shown row from the caller's session/context
664
- * instead of a row id in the path — a self-service screen ("my profile",
665
- * "my data") that has no row to link to (unlike `EntityEditScreenDefinition.
666
- * singleton`, which still resolves via `<entity>:list` limit 1; here the
667
- * query itself owns the row selection, e.g. `user:query:user:me`). The
668
- * query is called WITHOUT the `idParam` key — there is no id to send, so
669
- * the boot-validator rejects declaring `idParam` alongside this flag
670
- * instead of letting one silently win. Stricter than an id-addressed
671
- * screen, not looser: the server picks the row, so no client-supplied id
672
- * can reach the query — a stray id on the path is ignored, never
673
- * forwarded. */
674
- readonly singleton?: boolean;
675
- readonly layout: EditLayout;
676
- /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key), analog zu
677
- * entityEdit.fieldLabels. Die Pseudo-Entity `__projection-detail__` hat
678
- * keinen natürlichen Field-Namespace — fehlt ein Eintrag, gilt die
679
- * Konvention `<feature>:entity:__projection-detail__:field:<name>`. */
680
- readonly fieldLabels?: Readonly<Record<string, string>>;
681
- /** Record header above the layout — title/subtitle/status name columns
682
- * in the query row (not literals). Rendered only when set. */
683
- readonly header?: RecordHeaderSpec;
684
- /** Metric band above the layout — column names in the query row, each with its own i18n `label`.
685
- * Rendered only when set. */
686
- readonly metrics?: readonly MetricSpec[];
687
- /** Parent list screen (kurze id) für eine "Zurück"-Navigation. */
688
- readonly listScreenId?: string;
689
- readonly slots?: ScreenSlots;
690
- readonly access?: AccessRule;
691
- /** Header action buttons. Reuses `RowAction` — the screen's single
692
- * displayed record stands in for the "row", so `pick`/`map`, `visible`,
693
- * `confirm`/`confirmLabel` and `style` keep their meaning unchanged.
694
- * `rowClick` has no target here (there is no row to click) and is
695
- * rejected by the boot-validator. */
696
- readonly actions?: readonly RowAction[];
697
- /** Id of the action that stays the visible header button when the actions
698
- * collapse into a menu (and the one that keeps the phone icon slot when it
699
- * has an icon and no confirm). An `actions` id or `"edit"` (the default edit
700
- * action); default precedence is `edit`, then `style: "primary"`, then the
701
- * first action. */
702
- readonly primaryAction?: string;
703
- /** How head fields render. Default "text" — plain text instead of a
704
- * disabled Input, since every field on this screen type is forced
705
- * readOnly anyway (there is no write path, see the shim doc above).
706
- * Set "form" to opt back into the disabled-Input look (fw#2245). */
707
- readonly valueDisplay?: "form" | "text";
708
- };
709
- /** Text a query handler can send untranslated: the renderer translates it. */
710
- export type DashboardI18nText = {
711
- readonly i18nKey: string;
712
- readonly i18nParams?: Readonly<Record<string, DashboardTextParam>>;
713
- };
714
- /** Minor units (like Money fields), formatted with the user locale. */
715
- export type DashboardMoneyParam = {
716
- readonly kind: "money";
717
- readonly amountMinor: number;
718
- readonly currency: string;
719
- };
720
- /** Epoch ms, rendered as a medium date in the user time zone. */
721
- export type DashboardDateParam = {
722
- readonly kind: "date";
723
- readonly atMs: number;
724
- };
725
- /** Epoch ms, rendered relative to now ("5 minutes ago", "in 30 seconds") in the
726
- * user locale; the text keeps ticking while it is on screen. */
727
- export type DashboardRelativeTimeParam = {
728
- readonly kind: "relativeTime";
729
- readonly atMs: number;
730
- };
731
- /** Nested DashboardI18nText params resolve first (e.g. a duration "{years} {months}"
732
- * with one plural key each); numbers stay raw so plural rules can read `count`. */
733
- export type DashboardTextParam = string | number | DashboardI18nText | DashboardMoneyParam | DashboardDateParam | DashboardRelativeTimeParam;
734
- /** Plain string is displayed as-is; DashboardI18nText is translated by the renderer. */
735
- export type DashboardText = string | DashboardI18nText;
736
- /** Chart/stat values are minor units, formatted with the user locale. */
737
- export type DashboardCurrencyFormat = {
738
- readonly kind: "currency";
739
- readonly currency: string;
740
- /** Defaults to the currency's own decimals. */
741
- readonly fractionDigits?: number;
742
- };
743
- /** The stat value is an epoch ms timestamp, shown relative to now and kept current. */
744
- export type DashboardRelativeTimeFormat = {
745
- readonly kind: "relativeTime";
746
- };
747
- /** Stat panels take both formats; chart panels only the currency one. */
748
- export type DashboardValueFormat = DashboardCurrencyFormat | DashboardRelativeTimeFormat;
749
- export type DashboardPanelQueryOptions = {
750
- /** Static, author-set query parameters merged on top of the screen's
751
- * dynamic filterParams — lets a panel pin a value (e.g. a status facet)
752
- * the screen-wide filter doesn't cover, without needing its own query.
753
- * The panel wins over the screen filter and the time range. */
754
- readonly params?: Readonly<Record<string, unknown>>;
755
- /** The screen `filter` value is NOT merged into this panel's query (a tenant
756
- * picker must not hit sources without a tenant column). The screen
757
- * `timeRange` still is. */
758
- readonly ignoreScreenFilter?: boolean;
759
- };
760
- export type DashboardPanelGate = {
761
- readonly visibleWhen?: DashboardPanelVisibility;
762
- };
763
- export type DashboardPanelSpan = "half" | "full";
764
- export type DashboardPanelEmptyState = {
765
- readonly span?: DashboardPanelSpan;
766
- readonly emptyLabel?: string;
767
- /** Second line under `emptyLabel`. */
768
- readonly emptyHint?: string;
769
- };
770
- export declare const DASHBOARD_STAT_TONES: readonly ["default", "positive", "ok", "warn", "negative"];
771
- export type DashboardStatTone = (typeof DASHBOARD_STAT_TONES)[number];
772
- export type DashboardStatPanel = DashboardPanelQueryOptions & DashboardPanelGate & {
773
- readonly kind: "stat";
774
- /** Stable id: kebab-case, unique within the panel set. */
775
- readonly id: string;
776
- /** Display text (i18n key). */
777
- readonly label: string;
778
- readonly query: string;
779
- readonly valueField: string;
780
- readonly subField?: string;
781
- readonly toneField?: string;
782
- /** Formats a numeric value as currency (minor units) or, with `relativeTime`, as
783
- * a ticking "5 minutes ago" from epoch ms; ignored for string values. The sparkline is not formatted. */
784
- readonly valueFormat?: DashboardValueFormat;
785
- /** Fixed tone when `toneField` is unset or yields none; not applied while the value is 0. */
786
- readonly tone?: DashboardStatTone;
787
- /** Record field holding `{ atMs, value | null }[]` (MetricResult.points),
788
- * drawn as a sparkline next to the value. */
789
- readonly sparklineField?: string;
790
- /** Delta chip (e.g. "↓23 %") next to the label. It renders only when BOTH
791
- * fields are set AND the query handler returns them; otherwise the tile
792
- * looks as it does without a delta. `deltaToneField` falls back to
793
- * `toneField`/"default" when unset. */
794
- readonly deltaField?: string;
795
- readonly deltaDirectionField?: string;
796
- readonly deltaToneField?: string;
797
- /** Static icon next to the label. Unlike value/sub/delta it does not vary
798
- * per query result; it is an author decision like the panel itself.
799
- * Resolved through the same extensionSectionComponents registry as custom
800
- * panels; the registered component usually ignores
801
- * entityName/entityId/filterParams (a plain icon has no entity context). */
802
- readonly icon?: PlatformComponent;
803
- /** Static CSS color value (e.g. "var(--color-debt)") for the icon chip,
804
- * passed straight to the tile without a registry lookup. Applies ONLY when
805
- * `icon` is set (StatCard renders the chip only together with an icon);
806
- * without an icon the value is silently dropped. Without `accentColor` the
807
- * chip follows the tone, including a `toneField` value delivered per result.
808
- * An accent that should change with the result (e.g. by sign) goes through
809
- * `toneField`; leave `accentColor` unset then. */
810
- readonly accentColor?: string;
811
- };
812
- export type DashboardChartKind = "timeseries" | "stacked-bars" | "segment-bars" | "stacked-area";
813
- export type DashboardChartTone = "positive" | "negative" | "active" | "neutral";
814
- /** Look of the markers whose `kind` matches: a dashed guide line through the
815
- * plot plus a colored pin. `color` (raw CSS color) wins over `tone`. */
816
- export type DashboardChartMarkerKind = {
817
- readonly tone?: DashboardChartTone;
818
- readonly color?: string;
819
- /** i18n key; with `markerLegend: "legend"` the chart legend lists the markers of this kind under it. */
820
- readonly label?: string;
821
- };
822
- /** One option of a stacked-area range switch. Without `months` the option
823
- * shows every bucket; with it, a window of that many months anchored at
824
- * `todayMs` (or the last bucket). `label` is an i18n key. */
825
- export type DashboardChartRangeOption = {
826
- readonly value: string;
827
- readonly label: string;
828
- readonly months?: number;
829
- };
830
- export type DashboardChartRanges = {
831
- readonly options: readonly DashboardChartRangeOption[];
832
- readonly default: string;
833
- };
834
- export type DashboardChartPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & {
835
- readonly kind: "chart";
836
- readonly id: string;
837
- readonly label: string;
838
- readonly chart: DashboardChartKind;
839
- readonly query: string;
840
- /** i18n key under the title, e.g. "7 days, by creation day". */
841
- readonly subtitle?: string;
842
- /** Series/segment/line key -> tone; unmapped keys fall back to a palette. */
843
- readonly seriesTones?: Readonly<Record<string, DashboardChartTone>>;
844
- /** Series/segment/line key -> raw CSS color (e.g. "var(--color-debt)"); wins over `seriesTones`. */
845
- readonly seriesColors?: Readonly<Record<string, string>>;
846
- /** Formats values as currency (minor units): y ticks, legend totals and tooltips on
847
- * stacked-bars/segment-bars/stacked-area, only the y ticks on timeseries. */
848
- readonly valueFormat?: DashboardCurrencyFormat;
849
- /** i18n key of a unit label. Only for chart "timeseries": shown once above the y ticks instead of on every tick. */
850
- readonly unit?: string;
851
- /** Only for chart "stacked-area": fixed width per bucket, plot scrolls horizontally. */
852
- readonly scrollable?: boolean;
853
- /** Only for chart "stacked-area": marker `kind` -> look. Markers without a
854
- * declared kind keep the neutral numbered pin. */
855
- readonly markerKinds?: Readonly<Record<string, DashboardChartMarkerKind>>;
856
- /** Only for chart "stacked-area": false hides the per-series sums in the
857
- * legend (they mean nothing for balances). Default true. */
858
- readonly legendTotals?: boolean;
859
- /** Only for chart "stacked-area": range switch in the panel header. */
860
- readonly ranges?: DashboardChartRanges;
861
- /** Only for chart "stacked-area": scrubber under the plot to drag the visible window; starts at today. */
862
- readonly brush?: boolean;
863
- /** Only for chart "stacked-area": date format of axis and marker labels.
864
- * Default automatic: day and month below 18 visible months, month and year above. */
865
- readonly dateFormat?: "day" | "month";
866
- /** Only for chart "stacked-area": window the uncontrolled chart starts in.
867
- * "from-today" starts at today even when `ranges.default` is set. Default "default-range". */
868
- readonly initialWindow?: "default-range" | "from-today";
869
- /** Only for chart "stacked-area": "legend" drops the numbered marker list; pins become
870
- * plain dots with a tooltip and the legend lists marker kinds (see `markerKinds[].label`). Default "list". */
871
- readonly markerLegend?: "list" | "legend";
872
- /** Only for chart "stacked-area": true writes each marker label vertically next to its guide line on wide panels; narrow panels keep the pins. Default false. */
873
- readonly markerLabels?: boolean;
874
- };
875
- export type DashboardListPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & {
876
- readonly kind: "list";
877
- readonly id: string;
878
- readonly label: string;
879
- readonly query: string;
880
- readonly columns: readonly ListColumnSpec[];
881
- };
882
- export type DashboardStatGroupPanel = DashboardPanelGate & {
883
- readonly kind: "stat-group";
884
- readonly id: string;
885
- readonly label?: string;
886
- /** i18n key, a line under `label`; the boot validator requires `label`. */
887
- readonly subtitle?: string;
888
- readonly span?: DashboardPanelSpan;
889
- readonly stats: readonly DashboardStatPanel[];
890
- };
891
- export type DashboardFeedPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & {
892
- readonly kind: "feed";
893
- readonly id: string;
894
- readonly label: string;
895
- readonly query: string;
896
- };
897
- export type DashboardProgressTone = "default" | "danger";
898
- export type DashboardProgressListPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & {
899
- readonly kind: "progress-list";
900
- readonly id: string;
901
- readonly label: string;
902
- readonly query: string;
903
- };
904
- export type DashboardCustomPanel = {
905
- readonly kind: "custom";
906
- readonly id: string;
907
- readonly component: PlatformComponent;
908
- };
909
- export type DashboardTextBlock = {
910
- readonly kind: "heading";
911
- readonly text: string;
912
- } | {
913
- readonly kind: "paragraph";
914
- readonly text: string;
915
- /** Default "muted", like body copy inside panels. */
916
- readonly emphasis?: "muted" | "strong";
917
- } | {
918
- readonly kind: "list";
919
- readonly items: readonly string[];
920
- } | {
921
- readonly kind: "code";
922
- readonly code: string;
923
- } | {
924
- readonly kind: "hint";
925
- readonly text: string;
926
- } | {
927
- readonly kind: "link";
928
- /** Link label (i18n key). */
929
- readonly text: string;
930
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>`. */
931
- readonly screen: string;
932
- /** Show the link only to users holding one of these roles. Display
933
- * gate only: the target screen's own `access` stays the enforcement. */
934
- readonly roles?: readonly string[];
935
- };
936
- export type DashboardTextPanel = DashboardPanelGate & {
937
- readonly kind: "text";
938
- readonly id: string;
939
- /** Panel title (i18n key). */
940
- readonly label?: string;
941
- readonly span?: DashboardPanelSpan;
942
- readonly blocks: readonly DashboardTextBlock[];
943
- };
944
- export type DashboardPanelVisibility = {
945
- readonly query: string;
946
- readonly field: string;
947
- readonly eq: string | number | boolean | null;
948
- };
949
- export type DashboardScreenPanel = {
950
- readonly kind: "screen";
951
- readonly id: string;
952
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>`. */
953
- readonly screen: string;
954
- readonly label?: string;
955
- readonly visibleWhen?: DashboardPanelVisibility;
956
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` embedded in this
957
- * panel's place when `visibleWhen` is unmet or its query fails (nothing while it loads).
958
- * Needs `visibleWhen`; the target must not have a `visibleWhen` of its own. */
959
- readonly fallback?: string;
960
- /** Render the embedded form without its own card frame and padding, aligned to
961
- * the dashboard's page grid like a standalone settings screen. Default false
962
- * (framed card); set for panels whose body is a settings-list form, where a
963
- * card inside the page would double the inset. */
964
- readonly chromeless?: boolean;
965
- };
966
- export type DashboardPanelDefinition = DashboardStatPanel | DashboardStatGroupPanel | DashboardChartPanel | DashboardListPanel | DashboardFeedPanel | DashboardProgressListPanel | DashboardTextPanel | DashboardCustomPanel | DashboardScreenPanel;
967
- export type DashboardFilterDefinition = {
968
- readonly id: string;
969
- readonly label: string;
970
- readonly kind: "select";
971
- readonly placeholder?: string;
972
- /** i18n-Key für den "(alle)"-Eintrag. */
973
- readonly allLabel?: string;
974
- readonly options?: readonly {
975
- readonly value: string;
976
- readonly label: string;
977
- }[];
978
- /** Query-Result-Contract: `{ rows: { value: string; label: string }[] }`. */
979
- readonly optionsQuery?: string;
980
- };
981
- export type DashboardTimeRangeDefinition = {
982
- readonly id: string;
983
- /** `label` is an i18n key. */
984
- readonly options: readonly {
985
- readonly value: string;
986
- readonly label: string;
987
- }[];
988
- readonly default: string;
989
- };
990
- export type DashboardScopeDefinition = {
991
- readonly badge: string;
992
- readonly notice?: string;
993
- };
994
- /** Minimum polling interval of `DashboardScreenDefinition.refresh`; shorter values are rejected
995
- * at boot (`seconds`) or raised at runtime (`field`). */
996
- export declare const DASHBOARD_MIN_REFRESH_SECONDS = 5;
997
- export type DashboardRecordBinding = {
998
- /** URL search param carrying the record id. Without it the dashboard shows a
999
- * hint instead of its panels and runs no query. */
1000
- readonly param: string;
1001
- /** Key of the id in each panel query payload. Default: `param`. */
1002
- readonly payloadKey?: string;
1003
- /** Optional query for the record row (QN). It receives `{ [payloadKey]: id }`
1004
- * and returns a flat row; `actions` take that row as their input and
1005
- * `refresh.field` and `titleField` read from it. */
1006
- readonly query?: string;
1007
- /** Field of the record row shown as the page title. Needs `query`. */
1008
- readonly titleField?: string;
1009
- };
1010
- /** Polling of the record and all panel queries, never faster than
1011
- * DASHBOARD_MIN_REFRESH_SECONDS. `field` reads the seconds from the record row
1012
- * (needs `record.query`), so a monitor can be polled at its own check interval. */
1013
- export type DashboardRefresh = {
1014
- readonly seconds: number;
1015
- } | {
1016
- readonly field: string;
1017
- };
1018
- export type DashboardScreenDefinition = {
1019
- readonly id: string;
1020
- readonly type: "dashboard";
1021
- readonly nav?: ScreenNavSugar;
1022
- readonly detailFor?: string;
1023
- readonly description?: string;
1024
- readonly agent?: AgentScreenHints;
1025
- /** Screen has no nav entry by design (opened via link or navved by the
1026
- * app); exempts it from the nav-area boot check. */
1027
- readonly dormant?: boolean;
1028
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
1029
- readonly visibleWhen?: DashboardPanelVisibility;
1030
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
1031
- readonly fallback?: string;
1032
- readonly panels: readonly DashboardPanelDefinition[];
1033
- /** Binds the dashboard to one record via a URL search param, see DashboardRecordBinding. */
1034
- readonly record?: DashboardRecordBinding;
1035
- /** Header actions on the bound record (writeHandler, navigate, clientAction).
1036
- * The row is the `record.query` row, or `{ id, [payloadKey]: id }` without a
1037
- * query. After a success the record and all panels refetch. Needs `record`;
1038
- * no drawer, redirect or rowClick. */
1039
- readonly actions?: readonly RowAction[];
1040
- readonly refresh?: DashboardRefresh;
1041
- readonly filter?: DashboardFilterDefinition;
1042
- /** Segmented control in the toolbar; the value lives in URL search param
1043
- * `id` and is merged into EVERY panel query (also ignoreScreenFilter ones). */
1044
- readonly timeRange?: DashboardTimeRangeDefinition;
1045
- /** Marks the screen as spanning more than the caller's own scope. */
1046
- readonly scope?: DashboardScopeDefinition;
1047
- /** Show the "Updated at" timestamp in the toolbar. Default true; turn off for
1048
- * dashboards that host forms rather than live metrics, where a load time
1049
- * next to the form reads as stale data. */
1050
- readonly showUpdatedAt?: boolean;
1051
- readonly slots?: ScreenSlots;
1052
- readonly access?: AccessRule;
1053
- };
1054
- export type EditFieldSpec = string | {
1055
- readonly field: string;
1056
- readonly span?: number;
1057
- readonly visible?: FieldCondition;
1058
- readonly readOnly?: FieldCondition;
1059
- readonly required?: FieldCondition;
1060
- readonly renderer?: FieldRenderer;
1061
- /** `false` keeps the field out of the submitted payload — for values
1062
- * that are shown or prefilled for display only (a label, a counter)
1063
- * and that the write handler's schema does not accept. Validation and
1064
- * rendering are unchanged. */
1065
- readonly submit?: false;
1066
- /** Prefix icon on the input — closed FieldIconKey vocabulary into
1067
- * the FIELD_ICONS registry (renderer/web), analogous to
1068
- * `ScreenNavSugar.icon` / NavIconKey. Only takes effect for
1069
- * single-line `type: "text"` and `"number"` — a `text` field with
1070
- * `multiline` routes to a textarea (no icon slot), and other input
1071
- * kinds (select, combobox, date, …) silently ignore the key. */
1072
- readonly icon?: FieldIconKey;
1073
- /** Input placeholder (i18n key, falls back to the literal). Only for
1074
- * single-line text, number and textarea inputs; other kinds ignore it. */
1075
- readonly placeholder?: string;
1076
- /** Static text shown before the input (i18n key, falls back to the
1077
- * literal), or `{ field }` for the current form value of another field
1078
- * (a hidden field carrying e.g. a base domain). Only for single-line
1079
- * text and number inputs; other kinds ignore it. */
1080
- readonly prefix?: FieldAffix;
1081
- /** Same as `prefix`, shown after the input. */
1082
- readonly suffix?: FieldAffix;
1083
- /** Button beside the input that runs a query and writes one scalar of
1084
- * its result into this field. Also offered on a readOnly field, where
1085
- * the value can only come from the suggestion. Only for single-line
1086
- * text and number inputs; other kinds ignore it. */
1087
- readonly fillAction?: FieldFillAction;
1088
- /** Marks this field as a reference lookup instead of a plain value —
1089
- * for projectionDetail fields, which have no `EntityDefinition` to
1090
- * carry a real `reference` field type. Same target convention as
1091
- * `parseRefTarget` (and `ListColumnSpec.refEntity`): an entity name
1092
- * (same feature) or `feature:entity` (cross-feature). Ignored on
1093
- * `entityEdit` fields, where the field's own declared type always
1094
- * wins. */
1095
- readonly refEntity?: string;
1096
- /** Row field on the referenced entity shown as the label (default
1097
- * "id"). Only meaningful together with `refEntity`. */
1098
- readonly refLabelField?: string;
1099
- /** Embedded-list fields only (`createEmbeddedListField`). */
1100
- readonly embeddedList?: EmbeddedListEditSpec;
1101
- /** Fills an empty value when the create form opens; the update form and
1102
- * values from URL params or a handoff are never touched. Unlike a
1103
- * field `default`, it is not applied server-side. */
1104
- readonly prefill?: EditFieldPrefill;
1105
- };
1106
- export type FieldAffix = string | {
1107
- readonly field: string;
1108
- };
1109
- export type FieldFillAction = {
1110
- /** i18n key (falls back to the literal) of the button label. */
1111
- readonly label: string;
1112
- /** Qualified query name, run with the current form values picked by `params`. */
1113
- readonly query: string;
1114
- /** Key in the query result written into the field; the value must be a
1115
- * string or a number, anything else counts as a failed fill. */
1116
- readonly field: string;
1117
- /** Form fields sent as the query payload (none: an empty payload). */
1118
- readonly params?: RowFieldExtractor;
1119
- };
1120
- export type EmbeddedListEditSpec = {
1121
- /** Sub-fields shown as read-only cells; every other cell stays editable. */
1122
- readonly readOnlyCells?: readonly string[];
1123
- /** `true` keeps the rows as they are: no add, remove, duplicate, move or
1124
- * paste-appended rows. Cells stay editable (unless in `readOnlyCells`). */
1125
- readonly fixedRows?: boolean;
1126
- };
1127
- export type EditFieldPrefill = {
1128
- readonly kind: "today";
1129
- } | {
1130
- readonly kind: "currentUser";
1131
- readonly property: "displayName" | "email" | "id";
1132
- };
1133
- export type EditSectionSpec = EditFieldsSection | EditExtensionSection | EditRelatedListSection | EditWriteFormSection;
1134
- type EditFieldsSectionBase = {
1135
- /** Kebab-case tab id, used as the `?tab=` value. Required by the
1136
- * boot-validator when the enclosing `EditLayout.mode` is "tabs". */
1137
- readonly id?: string;
1138
- readonly kind?: "fields";
1139
- /** Optional. Ohne Titel rendert die Section nur ihre Felder (keine h3-
1140
- * Überschrift) — für flache Forms (Card-Titel + Felder direkt, ein
1141
- * einzelner Abschnitt) wie bei den meisten shadcn-Form-Mustern. */
1142
- readonly title?: string;
1143
- /** Optional help text under the block heading (i18n key or raw string,
1144
- * like `title`) — makes a form usable without training instead of
1145
- * falling back to custom JSX for help text. Renders through the same
1146
- * `subtitle` slot as `FormProps.subtitle`. Works without `title` too
1147
- * (subtitle-only section). */
1148
- readonly description?: string;
1149
- readonly columns?: number;
1150
- /** Rendered left of the section title, `text-muted-foreground` — closed
1151
- * IconKey vocabulary into the ICONS registry (renderer/web), analogous
1152
- * to EditFieldSpec.icon. No title → no icon, and no heuristic derives
1153
- * one from the title (titles are free i18n strings). */
1154
- readonly icon?: IconKey;
1155
- /** Record field rendered as a count badge in the tab label when the
1156
- * enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
1157
- * Ignored outside tabs mode or when the field's value is not a finite
1158
- * number. */
1159
- readonly countField?: string;
1160
- /** Short line (i18n key) under this step's title in the wizard step rail
1161
- * and in the "up next" box. Wizard layouts only; unlike `description` it
1162
- * is not shown inside the step. */
1163
- readonly subtitle?: string;
1164
- /** Rendered top-right in the section's Card title row — same
1165
- * navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`,
1166
- * evaluated against the same record. Every action must resolve an icon
1167
- * (declared or id-derived); the boot-validator rejects one that doesn't. */
1168
- readonly actions?: readonly RowAction[];
1169
- };
1170
- export type EditFieldGroup = {
1171
- readonly title: string;
1172
- readonly fields: readonly EditFieldSpec[];
1173
- /** Default 2. */
1174
- readonly columns?: number;
1175
- };
1176
- /** Either a flat `fields` grid or titled `groups` cards, never both: the groups branch has no
1177
- * `fields` key, so a raw `.fields` read on a section does not compile — use `sectionFieldSpecs`. */
1178
- export type EditFieldsSection = EditFieldsSectionBase & ({
1179
- readonly fields: readonly EditFieldSpec[];
1180
- } | {
1181
- /** Splits the section into multiple titled cards instead of one flat grid; fields named
1182
- * here still need to exist. */
1183
- readonly groups: readonly EditFieldGroup[];
1184
- });
1185
- export type EditExtensionSection = {
1186
- /** Kebab-case tab id, used as the `?tab=` value. Required by the
1187
- * boot-validator when the enclosing `EditLayout.mode` is "tabs". */
1188
- readonly id?: string;
1189
- readonly kind: "extension";
1190
- readonly title: string;
1191
- readonly component: PlatformComponent;
1192
- /** When true, the extension registers via `useExtensionFormSubmit` and the
1193
- * surrounding form's Save button should activate for its dirty state.
1194
- * Omit/false for extensions that persist through their own dispatcher
1195
- * writes (e.g. notes-history NotesSection) — otherwise a read-only form
1196
- * shows a Save button that does nothing (fw#2359). */
1197
- readonly contributesToFormSubmit?: boolean;
1198
- /** Overrides the host-derived entity name passed to the mounted component.
1199
- * Required on `projectionDetail` (which has no real entity — the host
1200
- * value there is an internal placeholder) so a self-persisting extension
1201
- * like NotesSection filters/writes against the right domain entity. Takes
1202
- * precedence over the host value on every screen type. */
1203
- readonly entityName?: string;
1204
- /** Record field rendered as a count badge in the tab label when the
1205
- * enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
1206
- * Ignored outside tabs mode or when the field's value is not a finite
1207
- * number. */
1208
- readonly countField?: string;
1209
- /** Rendered top-right in the section's Card title row — same
1210
- * navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`,
1211
- * evaluated against the same record. Every action must resolve an icon
1212
- * (declared or id-derived); the boot-validator rejects one that doesn't. */
1213
- readonly actions?: readonly RowAction[];
1214
- };
1215
- /** Rows are grouped by `field` in order of first appearance. Every group gets
1216
- * a collapsible header. */
1217
- export type RelatedListGroupBy = {
1218
- readonly field: string;
1219
- /** Groups whose field value equals this start collapsed; the others start open. */
1220
- readonly collapsedWhen?: string | number | boolean | null;
1221
- /** i18n key of the group header. Placeholders: `{count}` rows in the group,
1222
- * `{value}` the group's field value, `{lastDate}` the group's latest
1223
- * `dateField` value as a locale date. A group with neither `label` nor an
1224
- * entry in `labels` renders its rows without a header and never collapses. */
1225
- readonly label?: string;
1226
- /** Header i18n key per group value (as string), overriding `label`. */
1227
- readonly labels?: Readonly<Record<string, string>>;
1228
- /** Row field (ISO date or timestamp) behind `{lastDate}`. */
1229
- readonly dateField?: string;
1230
- };
1231
- /** Group key of a field value; `collapsedWhen` and row values both go through it. */
1232
- export declare function relatedListGroupKey(value: unknown): string;
1233
- export declare function relatedListGroupHeaderLabel(groupBy: RelatedListGroupBy, groupKey: string): string | undefined;
1234
- /** Row tint rule: rows where the `FieldCondition` (`{ field, eq }`, `{ field,
1235
- * in }` …) holds get the tone. `bad` renders red. */
1236
- export type RelatedListRowTone = Exclude<FieldCondition, boolean> & {
1237
- readonly tone: SelectOptionTone;
1238
- };
1239
- /** Replacement for the default "no rows" box of a list. `title`/`description`
1240
- * are i18n keys. `action` is a record action evaluated against an empty
1241
- * record on list screens (the parent record on a relatedList); its icon must
1242
- * resolve, same as `ProjectionDetailScreenDefinition.actions`. A narrowed list
1243
- * (search term or changed filters) shows the neutral "no matches" text instead. */
1244
- export type ListEmptyStateSpec = {
1245
- readonly title: string;
1246
- readonly description?: string;
1247
- readonly action?: RowAction;
1248
- };
1249
- export type EditRelatedListSection = {
1250
- /** Kebab-case tab id, used as the `?tab=` value. Required by the
1251
- * boot-validator when the enclosing `EditLayout.mode` is "tabs". */
1252
- readonly id?: string;
1253
- readonly kind: "relatedList";
1254
- readonly title: string;
1255
- /** Fully qualified query QN. Same paged envelope as projectionList.query:
1256
- * `{ rows, nextCursor, total? }`. */
1257
- readonly query: string;
1258
- /** Query-payload key the parent record's id is passed under. Default "id". */
1259
- readonly parentParam?: string;
1260
- /** Server-side WHERE clause pinning this section to the parent record —
1261
- * sent as `payload.filter: { field, op: "eq", value: parentId }`, kept
1262
- * out of the user-facet `filters` array so it can't be cleared by facet
1263
- * interaction. Lets a tab reuse the generic `<entity>:list` query instead
1264
- * of a bespoke child-rows handler. Mutually exclusive with `parentParam`
1265
- * (the boot-validator rejects both). `field` must be a real field on the
1266
- * entity behind `query`, and that query's Zod schema must accept
1267
- * `filter` (same requirement `filter`/`facets` already have). The filter is
1268
- * client-supplied: any caller may omit it, so the bound query must authorize
1269
- * the ENTIRE row set it can return, not just the parent's children. */
1270
- readonly parentFilter?: {
1271
- readonly field: string;
1272
- };
1273
- readonly columns: readonly ListColumnSpec[];
1274
- /** Entity the query's rows belong to: an entity name (same feature) or
1275
- * `feature:entity`. Columns that name one of its fields render like an
1276
- * entityList column of that field (select as a status badge with the
1277
- * translated option label, dates locale-formatted, default header from the
1278
- * field's label key). The column's own `sortable` still decides the
1279
- * header sort. Without it every column renders as plain text. */
1280
- readonly entity?: string;
1281
- /** i18n key for the one-line hint at the left of the tab toolbar. */
1282
- readonly description?: string;
1283
- /** i18n key holding plural forms for the footer count ("1 Position",
1284
- * "3 Positionen"). Omitted: generic "entries" wording. */
1285
- readonly itemNoun?: string;
1286
- readonly pageSize?: number;
1287
- /** Initial sort on mount, applied client-side over the already-loaded rows
1288
- * (this section has no pager; if the fetch is truncated, `nextCursor !== null`,
1289
- * the section shows a hint banner that the sort covers only the loaded rows).
1290
- * `field` must name a column with `sortable: true` — boot-validator pins
1291
- * that, same as `entityList.defaultSort`. */
1292
- readonly defaultSort?: ListSortSpec;
1293
- /** Renders a search box whose debounced term rides along as
1294
- * `payload.search` on this section's own query. Opt-in, unlike
1295
- * `projectionList.searchable` (derived from the bound query's schema): a
1296
- * relatedList query is an ordinary child-rows handler, so a `search`
1297
- * parameter in its schema is not by itself a statement that this section
1298
- * should offer search. The boot-validator rejects `true` when the query's
1299
- * Zod schema has no `search` parameter. */
1300
- readonly searchable?: boolean;
1301
- /** User-toggleable facet dropdowns — same explicit-label `ListFacetSpec` as
1302
- * `projectionList.facets` (a relatedList has no entity to derive filterable
1303
- * fields from). `field` must be a declared column, except for
1304
- * `type: "reference"`: it filters by id through the bound query's
1305
- * `filters` and needs a registered `entity` instead. The bound query
1306
- * handler must accept `filters` in its Zod schema — the boot-validator
1307
- * checks both. Selected values ride along as `payload.filters`. */
1308
- readonly facets?: readonly ListFacetSpec[];
1309
- /** Row click opens the target entity's detail screen via ObjectTarget —
1310
- * the `detailFor` lookup owns the entity→screen mapping, so no screenId
1311
- * is named here. `idColumn` names the row key holding that id (default
1312
- * "id"). Omit `rowClick` for a non-interactive list. */
1313
- readonly rowClick?: {
1314
- readonly entity: string;
1315
- readonly idColumn?: string;
1316
- };
1317
- /** Per-row actions — same type and dispatch semantics as `entityList`/
1318
- * `projectionList`'s `rowActions` (writeHandler dispatch with a
1319
- * declarative `payload` extractor, or navigate). A successful
1320
- * writeHandler action re-runs this section's own query, same as a
1321
- * projectionList row action re-running its list query. */
1322
- readonly rowActions?: readonly RowAction[];
1323
- /** Layout of the row-action column — see `RowActionMode`. */
1324
- readonly rowActionMode?: RowActionMode;
1325
- /** Groups rows by the value of a field under collapsible group headers. */
1326
- readonly groupBy?: RelatedListGroupBy;
1327
- /** Tints rows whose field matches — a declarative rule, not a function, so
1328
- * the screen definition stays serializable. */
1329
- readonly rowTone?: RelatedListRowTone;
1330
- /** Toolbar actions above the table — same type and dispatch semantics as
1331
- * `entityList`/`projectionList`'s `toolbarActions` ("+ Anlegen" etc.),
1332
- * plus `visible`/`params` evaluated against the parent record (see
1333
- * RelatedListToolbarAction). */
1334
- readonly toolbarActions?: readonly RelatedListToolbarAction[];
1335
- /** Record field rendered as a count badge in the tab label when the
1336
- * enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
1337
- * Ignored outside tabs mode or when the field's value is not a finite
1338
- * number. */
1339
- readonly countField?: string;
1340
- /** Rendered top-right in the section's Card title row — same
1341
- * navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`,
1342
- * evaluated against the parent record. Every action must resolve an icon
1343
- * (declared or id-derived); the boot-validator rejects one that doesn't.
1344
- * Distinct from `toolbarActions` (rendered inside the DataTable toolbar) —
1345
- * this is the Card-level title-row action. */
1346
- readonly actions?: readonly RowAction[];
1347
- /** Rendered inside the section's Card in place of the table when this
1348
- * section's query returns zero rows. Omitted: the DataTable default
1349
- * ("kumiko.list.no-entries") applies. `action`'s icon must resolve, same
1350
- * as `actions` above. */
1351
- readonly emptyState?: ListEmptyStateSpec;
1352
- };
1353
- export type EditWriteFormSection = {
1354
- /** Kebab-case tab id, used as the `?tab=` value. Required by the
1355
- * boot-validator when the enclosing `EditLayout.mode` is "tabs". */
1356
- readonly id?: string;
1357
- readonly kind: "writeForm";
1358
- readonly title?: string;
1359
- readonly description?: string;
1360
- readonly columns?: number;
1361
- readonly icon?: IconKey;
1362
- /** Field-type map for this section's own form — same shape as
1363
- * `ActionFormScreenDefinition.fields`. */
1364
- readonly fieldDefs: Readonly<Record<string, FieldDefinition>>;
1365
- /** Rendered fields, in order — same shape as `EditFieldsSection.fields`.
1366
- * Every referenced field must have an entry in `fieldDefs`. */
1367
- readonly fields: readonly EditFieldSpec[];
1368
- /** Write-handler QN dispatched on submit. The submitted values (not a
1369
- * diff) land 1:1 as payload, same as `ActionFormScreenDefinition.handler`.
1370
- * The payload carries only this section's own fields — no parent/record
1371
- * id is injected automatically. To thread the host record's id through,
1372
- * declare it in `fieldDefs` with `visible: false` on its `fields` entry:
1373
- * it gets prefilled from the loaded record like any other field and rides
1374
- * along in the submit payload without being shown in the form. */
1375
- readonly handler: string;
1376
- /** i18n-key for the submit button. Default: "kumiko.actions.save". */
1377
- readonly submitLabel?: string;
1378
- /** Rendered top-right in the section's Card title row — same
1379
- * navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`,
1380
- * evaluated against the same record. Every action must resolve an icon
1381
- * (declared or id-derived); the boot-validator rejects one that doesn't. */
1382
- readonly actions?: readonly RowAction[];
1383
- };
1384
- export type FormWidth = "3xl" | "4xl" | "full";
1385
- export type EditLayout = {
1386
- readonly sections: readonly EditSectionSpec[];
1387
- /** Default "4xl" (a centered column, narrower than list chrome), unless
1388
- * the app overrides it via `createKumikoApp({ screenWidth })`. Override
1389
- * per-screen to "3xl" for a narrower column or "full" to match list
1390
- * chrome. */
1391
- readonly width?: FormWidth;
1392
- /** Default "single". "wizard" renders one section per step (with
1393
- * progress + per-step validation) instead of all sections at once —
1394
- * boot-validator requires >= 2 sections, each with a title, when set.
1395
- * "tabs" renders one section per tab (projectionDetail only — the
1396
- * boot-validator rejects it on entityEdit, see the check for the reason);
1397
- * requires >= 2 sections, each with a title and an `id`. */
1398
- readonly mode?: "single" | "wizard" | "tabs";
1399
- /** "settings-list" lays each section out as a band with a title column on
1400
- * the left and one hairline-separated row per field (label + help left,
1401
- * control right) — the familiar settings pattern, for screens that are a
1402
- * list of independent preferences rather than one record form. Default
1403
- * "form" keeps the grid of cells. */
1404
- readonly variant?: "form" | "settings-list";
1405
- /** Persists in-progress wizard state as a resumable draft instead of
1406
- * discarding it on navigation away. */
1407
- readonly draft?: boolean;
1408
- /** Wizard layouts only. `upNext: true` adds an "up next" box to the step
1409
- * rail with the following step's title and `subtitle`; the last step
1410
- * shows none. */
1411
- readonly wizard?: {
1412
- readonly aside?: {
1413
- readonly upNext: true;
1414
- };
1415
- };
1416
- };
1417
- export type EntityWriteExcludedFields = {
1418
- readonly create?: readonly string[];
1419
- readonly update?: readonly string[];
1420
- };
1421
- export type EntityEditScreenDefinition = {
1422
- readonly id: string;
1423
- readonly type: "entityEdit";
1424
- /** Default true: the screen fills the shell content height, its body scrolls
1425
- * inside and the pager / action bar stays pinned. `false` restores page
1426
- * scroll (table grows with its rows, pager below the last row, form footer
1427
- * in the flow). */
1428
- readonly fillHeight?: boolean;
1429
- readonly nav?: ScreenNavSugar;
1430
- readonly detailFor?: string;
1431
- readonly description?: string;
1432
- readonly agent?: AgentScreenHints;
1433
- /** Screen has no nav entry by design (opened via link or navved by the
1434
- * app); exempts it from the nav-area boot check. */
1435
- readonly dormant?: boolean;
1436
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
1437
- readonly visibleWhen?: DashboardPanelVisibility;
1438
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
1439
- readonly fallback?: string;
1440
- /** Derived by buildAppSchema from the navigate `params` targeting this
1441
- * screen — the only URL query keys the create form prefills. An authored
1442
- * value is overwritten. */
1443
- readonly urlPrefillFields?: readonly string[];
1444
- /** Derived by buildAppSchema from the entity's create/update write handlers
1445
- * (`excludeFields`): create hides these fields, update shows them read-only.
1446
- * An authored value is overwritten. */
1447
- readonly writeExcludedFields?: EntityWriteExcludedFields;
1448
- readonly entity: string;
1449
- /** Entity field whose value names the loaded record in the header breadcrumb (edit mode only). */
1450
- readonly recordTitleField?: string;
1451
- /** Screen title (i18n key) with `{field}` placeholders filled from the
1452
- * current form values, so the title follows what the user types (create
1453
- * wizard: "Add {make} {model}"). Falls back to the plain title while the
1454
- * placeholders are still empty. */
1455
- readonly titleTemplate?: string;
1456
- /** Shows a saved record read-only while the condition holds for the loaded
1457
- * record (not for unsaved form values): all fields disabled, no save, discard
1458
- * or delete button; record `actions` stay. The title uses
1459
- * `screen:<id>.view.title`, then `screen:<id>.title`. Never applies to create.
1460
- * The entity's `lockedWhen` always applies as well; set this only to add a
1461
- * UI-only condition (either one shows the view).
1462
- * UI only: the update handler still has to enforce access. */
1463
- readonly readOnly?: FieldCondition;
1464
- readonly layout: EditLayout;
1465
- /** Optionaler i18n-Key (oder Roh-String) für den Submit-Button. Default
1466
- * `kumiko.actions.save`. Lässt den Auto-Edit-Screen domain-spezifische
1467
- * CTAs zeigen ("Save Address", "Create item") statt generisch "Speichern". */
1468
- readonly submitLabel?: string;
1469
- /** Default true. `false` für Entities deren Create über einen eigenen
1470
- * Lifecycle-Write läuft (z.B. incident:open mit Event-Stream + Joins)
1471
- * statt über `<entity>:create`: unterdrückt den automatischen
1472
- * „+ Neu"-Button auf entityList-Screens dieser Entity und rendert den
1473
- * Create-Branch (Aufruf ohne entityId) als Fehler statt eines Forms,
1474
- * dessen Submit gegen einen nicht registrierten Handler liefe. */
1475
- readonly allowCreate?: boolean;
1476
- /** Default true. `false` wenn kein `<entity>:delete`-Handler existiert
1477
- * (History-/Audit-Erhalt): unterdrückt den Löschen-Button im
1478
- * Update-Form. */
1479
- readonly allowDelete?: boolean;
1480
- /** Default false. `true` for singleton entities (exactly one record per
1481
- * tenant: organization, settings, tenant profile). A call without an
1482
- * `entityId` first resolves the existing record via `<entity>:list`
1483
- * (limit 1) and renders the update branch with prefill on a hit,
1484
- * instead of blindly creating another one. The create branch only
1485
- * fires when the table is empty (and `allowCreate: false` still
1486
- * applies there). */
1487
- readonly singleton?: boolean;
1488
- /** Navigate to this screen ID after a successful save (create or update).
1489
- * Either a same-feature short screen-ID, or a fully-qualified
1490
- * cross-feature screen QN (`<feature>:screen:<id>`) — the nav-router
1491
- * resolves both against the schema; boot-validator checks the target
1492
- * resolves to a registered screen. Overrides the default post-save
1493
- * target (the entity's list screen); delete is unaffected and still
1494
- * always navigates to the list. A cross-feature QN hard-couples this
1495
- * feature to the target at boot time — an app that doesn't mount the
1496
- * target feature fails the boot-validator; only use this form in a
1497
- * reusable feature when the target feature is guaranteed to be
1498
- * mounted alongside it.
1499
- *
1500
- * The object form additionally names the success-payload field the
1501
- * navigation id comes from (`ActionFormRedirect.idFrom`) — needed when
1502
- * the edited record is a child and the target screen is the parent's
1503
- * detail screen. Update falls back to the loaded record when the
1504
- * success payload doesn't carry that field flatly; create falls back
1505
- * to the submitted form values. */
1506
- readonly redirect?: string | ActionFormRedirect;
1507
- /** Parent list screen (short id) for breadcrumb + nav highlighting when
1508
- * this screen has no nav entry of its own. Same field/semantics as on
1509
- * `custom`/`projectionDetail`; an explicit value here wins over the
1510
- * entity-list heuristic (rowAction target / same-entity entityList). */
1511
- readonly listScreenId?: string;
1512
- /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key), überschreibt
1513
- * die Default-Konvention `<feature>:entity:<entity>:field:<name>`.
1514
- * Primär für configEdit: dessen Pseudo-Entity `__config-edit__` hat
1515
- * keinen natürlichen Field-Namespace — der Settings-Hub injiziert hier
1516
- * das `mask.title`-Label des Config-Keys. Fehlt ein Eintrag, gilt die
1517
- * Konvention. */
1518
- readonly fieldLabels?: Readonly<Record<string, string>>;
1519
- /** Record actions. Reuses `RowAction` — analogous to
1520
- * `ProjectionDetailScreenDefinition.actions` (the loaded record stands
1521
- * in for the "row"). `rowClick` has no target here and is rejected by
1522
- * the boot-validator, same as on projectionDetail. Not rendered in
1523
- * create mode — every action targets an existing record, which the
1524
- * create branch doesn't have yet. By default an action renders with the
1525
- * secondary form actions (footer, next to copy-link/delete/cancel);
1526
- * `placement: "menu"` moves it into the header "…" menu, ahead of
1527
- * copy-link/delete. Without a header menu (drawer, card form, shell
1528
- * without a header slot) a "menu" action falls back to inline. */
1529
- readonly actions?: readonly EntityEditAction[];
1530
- readonly slots?: ScreenSlots;
1531
- readonly access?: AccessRule;
1532
- };
1533
- export type EntityEditActionPlacement = "inline" | "menu";
1534
- export type EntityEditAction = RowAction & {
1535
- readonly placement?: EntityEditActionPlacement;
1536
- };
1537
- /** A form field computed from other fields of the same form. `listSum` adds one
1538
- * numeric cell over all rows of an embedded list field; its `whenEmpty`
1539
- * fallback applies while the list has no rows (it must not be a `listSum`). */
1540
- export type FormFieldDerivation = {
1541
- readonly op: "sum" | "subtract" | "multiply" | "percent";
1542
- readonly from: readonly string[];
1543
- } | {
1544
- readonly op: "listSum";
1545
- readonly list: string;
1546
- readonly cell: string;
1547
- readonly whenEmpty?: FormFieldDerivationWithoutList;
1548
- };
1549
- export type FormFieldDerivationWithoutList = Extract<FormFieldDerivation, {
1550
- from: unknown;
1551
- }>;
1552
- export type ActionFormScreenDefinition = {
1553
- readonly id: string;
1554
- readonly type: "actionForm";
1555
- /** Default true: the screen fills the shell content height, its body scrolls
1556
- * inside and the pager / action bar stays pinned. `false` restores page
1557
- * scroll (table grows with its rows, pager below the last row, form footer
1558
- * in the flow). */
1559
- readonly fillHeight?: boolean;
1560
- readonly nav?: ScreenNavSugar;
1561
- readonly detailFor?: string;
1562
- readonly description?: string;
1563
- readonly agent?: AgentScreenHints;
1564
- /** Screen has no nav entry by design (opened via link or navved by the
1565
- * app); exempts it from the nav-area boot check. */
1566
- readonly dormant?: boolean;
1567
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
1568
- readonly visibleWhen?: DashboardPanelVisibility;
1569
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
1570
- readonly fallback?: string;
1571
- /** Derived by buildAppSchema — see EntityEditScreenDefinition.urlPrefillFields. */
1572
- readonly urlPrefillFields?: readonly string[];
1573
- /** Derived by buildAppSchema from the `excludedFields` of the called `handler`; the form
1574
- * shows these fields read-only, not required and never in the payload. An authored value is overwritten. */
1575
- readonly writeExcludedFields?: readonly string[];
1576
- /** Submit is enabled without edits: values seeded from navigate `params` count as the
1577
- * user's intent. Every required field the layout hides with `visible: false` must hold a
1578
- * value, otherwise submit stays disabled (boot-checked against navigate `params`). */
1579
- readonly submitPrefilled?: boolean;
1580
- /** Write-Handler-QN der bei Submit gerufen wird. Form-Object landet
1581
- * 1:1 als payload — Handler-Schema (Zod) validiert weiter. */
1582
- readonly handler: string;
1583
- /** Form-Shape: Field-Map pro Name. Nutzt dieselben FieldDefinitions
1584
- * wie Entity-Felder. Mindestens ein Feld erforderlich (Boot-
1585
- * Validator).
1586
- *
1587
- * A `money` field here must name its own currency source: this screen
1588
- * has no entity whose `defaultCurrency` it could inherit (fw#2839). */
1589
- readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1590
- /** Fields computed from other fields, shown read-only and recomputed on every
1591
- * change. A `listSum` over an empty list without `whenEmpty` leaves the field
1592
- * a normal input, and then the client value counts. Otherwise the client value
1593
- * is never trusted: the handler must wrap its schema with
1594
- * `withFormDerivations(schema, screen)`, which the boot validator enforces.
1595
- * A field read only as `whenEmpty` operand is read-only while every such list has rows. */
1596
- readonly derive?: Readonly<Record<string, FormFieldDerivation>>;
1597
- /** Layout analog zu EntityEditScreen: sections mit fields aus dem
1598
- * fields-Map oben. */
1599
- readonly layout: EditLayout;
1600
- /** Per-field label i18n key override, same type/semantics as
1601
- * `EntityEditScreenDefinition.fieldLabels`. Falls back to the convention when absent. */
1602
- readonly fieldLabels?: Readonly<Record<string, string>>;
1603
- /** i18n-key für den Submit-Button. Default: i18n-Default des
1604
- * Renderers (typischerweise "actions.submit"). */
1605
- readonly submitLabel?: string;
1606
- /** Extra buttons in the form footer next to the submit button. A click
1607
- * applies `patch` to the form values, then submits the form (same
1608
- * validation, same handler) — e.g. a "Mark as posted" button that sets
1609
- * `posted: true`. The patched field need not be rendered by the layout.
1610
- * Order in the array = order in the footer, before the submit button. */
1611
- readonly footerActions?: readonly ActionFormFooterAction[];
1612
- /** Context box above the fields (i18n keys). `{name}` placeholders resolve from the
1613
- * drawer prefill (`params` of the opening row action), formatted for display. */
1614
- readonly summary?: {
1615
- readonly title: string;
1616
- readonly subtitle?: string;
1617
- };
1618
- /** Visual style of the submit button. "danger" renders it red — for a form
1619
- * whose handler is destructive (terminate, revoke, delete), so the warning
1620
- * isn't lost on the way from the calling screen to this one. Default "primary". */
1621
- readonly submitStyle?: "primary" | "danger";
1622
- /** i18n key of a confirmation shown above the form after a successful submit
1623
- * when the screen stays put (no `redirect`, not drawer-hosted). `{field}`
1624
- * placeholders resolve from the submitted values; a `reference` field shows
1625
- * the chosen record's label (its `labelField`), not the id — so a
1626
- * cross-tenant admin form can name the record it just changed. */
1627
- readonly successMessage?: string;
1628
- /** Navigate to this screen ID after a successful submit: either a short
1629
- * ID (e.g. "item-list" — same feature, the nav-router resolves to the
1630
- * full path) or a fully-qualified cross-feature QN
1631
- * (`<feature>:screen:<id>`). If not set, the user stays on the form
1632
- * screen. Boot-validator checks that the target resolves to a
1633
- * registered screen. A cross-feature QN hard-couples this feature to
1634
- * the target at boot time — an app that doesn't mount the target
1635
- * feature fails the boot-validator; only use this form in a reusable
1636
- * feature when the target feature is guaranteed to be mounted
1637
- * alongside it.
1638
- *
1639
- * The object form additionally names the success-payload field the
1640
- * navigation id comes from (`ActionFormRedirect.idFrom`) — needed when
1641
- * the handler creates a child record but the target screen is the
1642
- * parent's detail screen (fw#2670). */
1643
- readonly redirect?: string | ActionFormRedirect;
1644
- /** Links shown in the success box after a successful submit while the
1645
- * screen stays put. Targets resolve like `redirect` (the id comes from the
1646
- * handler's success payload); a link without a string id is hidden.
1647
- * Mutually exclusive with `redirect`. */
1648
- readonly successLinks?: readonly ActionFormSuccessLink[];
1649
- /** Key figures from the handler's success payload, shown in the success box
1650
- * while the screen stays put (e.g. charged/skipped/failed of a batch run).
1651
- * Mutually exclusive with `redirect`. */
1652
- readonly successFacts?: readonly ActionFormSuccessFact[];
1653
- /** Reset the form to its initial values after a successful submit (prefill
1654
- * kept, typed values cleared). Only applies while the screen stays put,
1655
- * i.e. without `redirect`. */
1656
- readonly resetOnSuccess?: boolean;
1657
- /** Load the initial values from a query. The record id comes from the route
1658
- * (or the drawer row id) and is sent as `{ [idParam]: id }`; the result,
1659
- * picked to `fields`, becomes the initial values. Turns URL prefill off:
1660
- * search params never reach the form. */
1661
- readonly load?: ActionFormLoad;
1662
- /** Target of the Cancel button. Default: `redirect` (historical
1663
- * behavior — Cancel and the submit-redirect then land in the same
1664
- * place). `false` = no Cancel button; correct for single-action
1665
- * screens with no discardable state (e.g. "Send test mail"), where
1666
- * Cancel would just be a second path to the same target. Accepts the
1667
- * same string targets as `redirect` (short ID or cross-feature QN);
1668
- * boot-validator checks it. A cross-feature QN hard-couples this
1669
- * feature to the target at boot time — an app that doesn't mount the
1670
- * target feature fails the boot-validator; only use this form in a
1671
- * reusable feature when the target feature is guaranteed to be
1672
- * mounted alongside it. */
1673
- readonly cancelTarget?: string | false;
1674
- /** Parent list screen (short id) for breadcrumb + nav highlighting when
1675
- * this screen has no nav entry of its own. Same field/semantics as on
1676
- * `custom`/`projectionDetail`/`entityEdit`; an explicit value here wins
1677
- * over the rowAction heuristic. */
1678
- readonly listScreenId?: string;
1679
- readonly slots?: ScreenSlots;
1680
- readonly access?: AccessRule;
1681
- };
1682
- export type ActionFormFooterAction = {
1683
- /** Stable id (kebab-case), used for the test id. */
1684
- readonly id: string;
1685
- /** i18n key. */
1686
- readonly label: string;
1687
- readonly icon?: IconKey;
1688
- /** Button style. Default "secondary". */
1689
- readonly variant?: "primary" | "secondary" | "danger";
1690
- /** Form values set before the submit. JSON-safe literals only. */
1691
- readonly patch: Readonly<Record<string, string | number | boolean | null>>;
1692
- };
1693
- /** Redirect target plus the success-payload field carrying the navigation
1694
- * id. The write-handler reports the id of what it wrote (`data.id`); when
1695
- * that record is a child and the target screen shows its parent, the
1696
- * parent id has to be read from a different field instead of forcing the
1697
- * handler to misreport its own result (fw#2670). */
1698
- export type ActionFormRedirect = {
1699
- /** Same target forms as the string `redirect`: short screen ID
1700
- * (same-feature) or a fully-qualified cross-feature screen QN. */
1701
- readonly screen: string;
1702
- /** Flat field name in the handler's success payload, e.g. "leaseId".
1703
- * Only used when the target screen type carries an id (`entityEdit`,
1704
- * `projectionDetail`); a non-string or missing value navigates without
1705
- * an id, same as a payload without `id` does today. */
1706
- readonly idFrom: string;
1707
- /** Section id to open on the target `projectionDetail` with `layout.mode:
1708
- * "tabs"` (the `tab` search param, same as `ToolbarAction` navigate `tab`).
1709
- * Boot rejects other targets and unknown section ids. */
1710
- readonly tab?: string;
1711
- };
1712
- export type ActionFormLoad = {
1713
- /** Query QN that returns the record. */
1714
- readonly query: string;
1715
- /** Payload key carrying the record id (default "id"). */
1716
- readonly idParam?: string;
1717
- /** The query picks the row from the caller's tenant/session context, so the
1718
- * form loads without a record id and the query is called WITHOUT `idParam`
1719
- * (same semantics as `ProjectionDetailScreenDefinition.singleton`). Boot
1720
- * rejects declaring `idParam` alongside it. */
1721
- readonly singleton?: boolean;
1722
- };
1723
- /** One key figure of the success box, read from the handler's success payload. */
1724
- export type ActionFormSuccessFact = {
1725
- /** Flat field name in the handler's success payload. A number or string is
1726
- * shown as one value, an array a comma-separated list (numbers, strings and
1727
- * `{ label }` objects). A missing field hides the fact. */
1728
- readonly field: string;
1729
- /** Label (i18n key). */
1730
- readonly label: string;
1731
- };
1732
- /** A redirect target rendered as a link in the success box. */
1733
- export type ActionFormSuccessLink = Omit<ActionFormRedirect, "idFrom"> & {
1734
- /** Success-payload field carrying the navigation id. Required for a record
1735
- * target (`entityEdit`, `projectionDetail`); omit it for a list, dashboard
1736
- * or form target, which opens without an id. */
1737
- readonly idFrom?: string;
1738
- /** Link text (i18n key). */
1739
- readonly label: string;
1740
- };
1741
- /** Author-declared field shown once after a mint-form submit — API token,
1742
- * recovery codes, similar one-time secrets. */
1743
- export type SecretRevealField = {
1744
- /** Flat field name in the handler's success payload. */
1745
- readonly field: string;
1746
- /** i18n key for this value's label. */
1747
- readonly label: string;
1748
- /** "code" (default) renders one monospaced value; "list" renders a
1749
- * string-array payload one value per line (recovery codes); "qr" renders
1750
- * the value as a scannable QR code (otpauth://-style enrollment URI) —
1751
- * platforms without a QR-capable SecretReveal primitive fall back to the
1752
- * monospaced text display. */
1753
- readonly display?: "code" | "list" | "qr";
1754
- /** Default true — offer a copy-to-clipboard control next to the value. */
1755
- readonly copyable?: boolean;
1756
- };
1757
- /** Post-reveal proof-of-receipt step. A mint whose effect is only armed once
1758
- * the user proves they received the secret (TOTP enrollment: scan the code,
1759
- * then enter one) declares it here — the reveal card renders this form in
1760
- * place of the bare acknowledge button. */
1761
- export type SecretMintConfirmStep = {
1762
- /** Write-handler QN dispatched when the confirm form is submitted. */
1763
- readonly handler: string;
1764
- readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1765
- readonly layout: EditLayout;
1766
- /** Mint success-payload fields merged into the confirm payload (e.g. a
1767
- * short-lived setup token). Held in component state only — never rendered,
1768
- * never in the URL, a query cache, nav or a persisted draft. */
1769
- readonly carry?: readonly string[];
1770
- readonly submitLabel?: string;
1771
- /** i18n key for the banner shown after a successful confirm when the screen
1772
- * declares no `redirect`. Default "kumiko.secretMint.done". */
1773
- readonly doneMessage?: string;
1774
- };
1775
- export type SecretReveal = {
1776
- readonly fields: readonly SecretRevealField[];
1777
- /** i18n key. Default "kumiko.secretMint.title". */
1778
- readonly title?: string;
1779
- /** i18n key for the "this is the only time you see it" warning.
1780
- * Default "kumiko.secretMint.warning". */
1781
- readonly warning?: string;
1782
- /** i18n key for the acknowledge button. Default "kumiko.secretMint.confirm". */
1783
- readonly confirmLabel?: string;
1784
- /** i18n key for a checkbox on the reveal card ("I have saved my recovery
1785
- * codes"). While unchecked, the confirm step's submit or the acknowledge
1786
- * button stays disabled. The tick is UI-only and never part of a payload. */
1787
- readonly acknowledge?: string;
1788
- };
1789
- /** Mint form → one-time reveal → explicit confirm. The secret (API token,
1790
- * recovery codes) lives only in the write-handler's own success payload —
1791
- * no stored value backs it, so no query can redisplay it later. That gap
1792
- * is why this needs its own screen type instead of a flag on `actionForm`,
1793
- * which discards the success payload after extracting the navigation id. */
1794
- export type SecretMintScreenDefinition = {
1795
- readonly id: string;
1796
- readonly type: "secretMint";
1797
- readonly nav?: ScreenNavSugar;
1798
- readonly detailFor?: string;
1799
- readonly description?: string;
1800
- readonly agent?: AgentScreenHints;
1801
- /** Screen has no nav entry by design (opened via link or navved by the
1802
- * app); exempts it from the nav-area boot check. */
1803
- readonly dormant?: boolean;
1804
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
1805
- readonly visibleWhen?: DashboardPanelVisibility;
1806
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
1807
- readonly fallback?: string;
1808
- /** Derived by buildAppSchema — see EntityEditScreenDefinition.urlPrefillFields. */
1809
- readonly urlPrefillFields?: readonly string[];
1810
- /** Submit is enabled without edits: values seeded from navigate `params` count as the
1811
- * user's intent. Every required field the layout hides with `visible: false` must hold a
1812
- * value, otherwise submit stays disabled (boot-checked against navigate `params`). */
1813
- readonly submitPrefilled?: boolean;
1814
- /** Write-handler QN dispatched on submit. */
1815
- readonly handler: string;
1816
- readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1817
- readonly layout: EditLayout;
1818
- /** Navigate here after the reveal is confirmed. Short id (same-feature) or
1819
- * a fully-qualified cross-feature screen QN — no result-id navigation
1820
- * form exists here (unlike `ActionFormScreenDefinition.redirect`), since
1821
- * the confirmed screen carries no entity id to thread through. */
1822
- readonly redirect?: string;
1823
- readonly cancelTarget?: string | false;
1824
- readonly listScreenId?: string;
1825
- readonly access?: AccessRule;
1826
- readonly reveal: SecretReveal;
1827
- /** i18n-key for the mint button. Default: renderer's own default. */
1828
- readonly submitLabel?: string;
1829
- /** Proof-of-receipt step rendered on the reveal card in place of the bare
1830
- * acknowledge button — see `SecretMintConfirmStep` doc. */
1831
- readonly confirm?: SecretMintConfirmStep;
1832
- };
1833
- export type CustomScreenRoute = {
1834
- readonly path: string;
1835
- readonly component: PlatformComponent;
1836
- };
1837
- export type CustomScreenDefinition = {
1838
- readonly id: string;
1839
- readonly type: "custom";
1840
- readonly nav?: ScreenNavSugar;
1841
- readonly detailFor?: string;
1842
- readonly description?: string;
1843
- readonly agent?: AgentScreenHints;
1844
- readonly renderer: PlatformComponent;
1845
- readonly routes?: readonly CustomScreenRoute[];
1846
- /** Parent list screen for breadcrumb when this detail is not in nav. */
1847
- readonly listScreenId?: string;
1848
- readonly access?: AccessRule;
1849
- /** This screen is registered without a self-owned `r.nav()` entry by
1850
- * design — an app opts in by navving it explicitly (e.g. a settings-area
1851
- * self-service screen). `createKumikoApp`'s boot diagnostic
1852
- * (kumiko-framework#2025) skips dormant screens instead of flagging
1853
- * every consumer that doesn't mount the client plugin as a false
1854
- * positive (kumiko-framework#2034). Only set this on screens the
1855
- * feature itself never navs — a screen the feature DOES nav still needs
1856
- * its client plugin mounted by every consumer, and should keep
1857
- * triggering the diagnostic if it's missing. Also gates the
1858
- * boot-validator's nav-area check, same as `dormant` on other screens. */
1859
- readonly dormant?: boolean;
1860
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
1861
- readonly visibleWhen?: DashboardPanelVisibility;
1862
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
1863
- readonly fallback?: string;
1864
- };
1865
- export type ConfigEditScreenDefinition = {
1866
- readonly id: string;
1867
- readonly type: "configEdit";
1868
- /** Default true: the form fills the shell content height with a pinned
1869
- * footer. `false` restores the card layout with the footer in the flow. */
1870
- readonly fillHeight?: boolean;
1871
- readonly nav?: ScreenNavSugar;
1872
- readonly detailFor?: string;
1873
- readonly description?: string;
1874
- readonly agent?: AgentScreenHints;
1875
- /** Screen has no nav entry by design (opened via link or navved by the
1876
- * app); exempts it from the nav-area boot check. */
1877
- readonly dormant?: boolean;
1878
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
1879
- readonly visibleWhen?: DashboardPanelVisibility;
1880
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
1881
- readonly fallback?: string;
1882
- /** scope für config:write:set Calls. Muss zur Scope-Deklaration der
1883
- * in `configKeys` referenzierten Keys passen — Boot-Validator
1884
- * prüft das gegen die Registry. */
1885
- readonly scope: "tenant" | "system" | "user";
1886
- /** Map: form-field-name (kurz, wie im Layout referenziert) → voll-
1887
- * qualifizierter Config-Key (`<feature>:config:<short>`). Boot-
1888
- * Validator prüft dass jeder qualifizierte Key in der Registry
1889
- * bekannt ist. */
1890
- readonly configKeys: Readonly<Record<string, string>>;
1891
- /** Field name → qualified secret key (`<feature>:secret:<short>`), tenant scope only.
1892
- * Such a field is a writeOnly text field with no `configKeys` entry; the same save
1893
- * writes it through secrets:write:set (or secrets:write:delete on removal). */
1894
- readonly secretKeys?: Readonly<Record<string, string>>;
1895
- /** Form-Shape pro Field-Name. Selbe FieldDefinitions wie auf
1896
- * Entities/ActionForm. Field-Names matchen die Keys in `configKeys`
1897
- * — Boot-Validator pinnt das. */
1898
- readonly fields: Readonly<Record<string, FieldDefinition>>;
1899
- /** Layout: Sections mit Field-Refs. Identisch zu entityEdit/
1900
- * actionForm. */
1901
- readonly layout: EditLayout;
1902
- /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key). Der
1903
- * Settings-Hub setzt hier `mask.title` des jeweiligen Config-Keys,
1904
- * damit das am Key deklarierte Label am generierten Feld erscheint —
1905
- * ohne es unter der `__config-edit__`-Konvention zu duplizieren.
1906
- * Fehlt ein Eintrag, gilt die Konvention. */
1907
- readonly fieldLabels?: Readonly<Record<string, string>>;
1908
- /** Per-field help text i18n key (Field-Name → Key), from `mask.description`. */
1909
- readonly fieldDescriptions?: Readonly<Record<string, string>>;
1910
- /** Field names whose config key declares `required: true` — drives the
1911
- * "Not set. Required." origin line, not form validation. */
1912
- readonly requiredFields?: readonly string[];
1913
- /** i18n-key für den Submit-Button. Default: "kumiko.actions.save". */
1914
- readonly submitLabel?: string;
1915
- readonly slots?: ScreenSlots;
1916
- readonly access?: AccessRule;
1917
- };
1918
- export type SecretsEditSection = {
1919
- readonly title?: string;
1920
- readonly fields: readonly string[];
1921
- };
1922
- export type SecretsEditScreenDefinition = {
1923
- readonly id: string;
1924
- readonly type: "secretsEdit";
1925
- readonly nav?: ScreenNavSugar;
1926
- readonly detailFor?: string;
1927
- readonly description?: string;
1928
- readonly agent?: AgentScreenHints;
1929
- /** Screen has no nav entry by design (opened via link or navved by the
1930
- * app); exempts it from the nav-area boot check. */
1931
- readonly dormant?: boolean;
1932
- /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
1933
- readonly visibleWhen?: DashboardPanelVisibility;
1934
- /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
1935
- readonly fallback?: string;
1936
- /** field id -> qualified secret name (`<feature>:secret:<kebab>`). */
1937
- readonly secretKeys: Readonly<Record<string, string>>;
1938
- /** field id -> i18n key for the label. */
1939
- readonly fieldLabels: Readonly<Record<string, string>>;
1940
- /** field id -> i18n key for the hint, only where the declaration carries one. */
1941
- readonly fieldHints?: Readonly<Record<string, string>>;
1942
- /** field ids whose declaration set `required: true`. */
1943
- readonly requiredFields?: readonly string[];
1944
- /** field id -> write access derived from the key's `writeRoles`. The screen
1945
- * hides fields the viewer fails; UI only, the secrets write gate stays
1946
- * authoritative. */
1947
- readonly fieldAccess?: Readonly<Record<string, AccessRule>>;
1948
- readonly sections: readonly SecretsEditSection[];
1949
- readonly access?: AccessRule;
1950
- };
1951
85
  export type ScreenSlots = {
1952
86
  readonly header?: PlatformComponent;
1953
87
  /** entityEdit / actionForm: rendered on the right of the form title, same row. */
@@ -1973,3 +107,11 @@ export type ScreenNavSugar = {
1973
107
  readonly order?: number;
1974
108
  };
1975
109
  export type ScreenDefinition = EntityListScreenDefinition | ProjectionListScreenDefinition | ProjectionDetailScreenDefinition | DashboardScreenDefinition | EntityEditScreenDefinition | ActionFormScreenDefinition | ConfigEditScreenDefinition | SecretsEditScreenDefinition | SecretMintScreenDefinition | CustomScreenDefinition;
110
+ export type { ActionFormFooterAction, ActionFormLoad, ActionFormRedirect, ActionFormScreenDefinition, ActionFormSuccessFact, ActionFormSuccessLink, FormFieldDerivation, FormFieldDerivationWithoutList, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, } from "./screen-action-form.ts";
111
+ export type { ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, SecretsEditScreenDefinition, SecretsEditSection, } from "./screen-custom-config.ts";
112
+ export type { DashboardChartKind, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardChartTone, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelGate, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardStatTone, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, } from "./screen-dashboard.ts";
113
+ export { DASHBOARD_MIN_REFRESH_SECONDS, DASHBOARD_STAT_TONES } from "./screen-dashboard.ts";
114
+ export type { EditExtensionSection, EditFieldGroup, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityWriteExcludedFields, FieldAffix, FieldFillAction, FormWidth, ListEmptyStateSpec, RelatedListGroupBy, RelatedListRowTone, } from "./screen-entity-edit.ts";
115
+ export { relatedListGroupHeaderLabel, relatedListGroupKey } from "./screen-entity-edit.ts";
116
+ export type { EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListPaginationMode, ListSortDir, ListSortSpec, RelatedListToolbarAction, RowAction, RowActionClientAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenFilter, ScreenFilterOp, ToolbarAction, } from "./screen-entity-list.ts";
117
+ export type { ListFacetSpec, MetricNavigate, MetricSpec, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, } from "./screen-projection.ts";