@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.
@@ -0,0 +1,427 @@
1
+ import type { FieldCondition } from "./fields.ts";
2
+ import type { AccessRule, AgentScreenHints } from "./handlers.ts";
3
+ import type { IconKey } from "./nav-icon.ts";
4
+ import type { FieldRenderer, PlatformComponent, ScreenNavSugar, ScreenSlots } from "./screen.ts";
5
+ import type { ActionFormRedirect } from "./screen-action-form.ts";
6
+ import type { DashboardPanelVisibility } from "./screen-dashboard.ts";
7
+ import type { EditRelatedListSection, ListEmptyStateSpec } from "./screen-entity-edit.ts";
8
+ export type ListColumnSpec = string | {
9
+ readonly field: string;
10
+ readonly renderer?: FieldRenderer;
11
+ /** Optional header, overriding the default
12
+ * `<feature>:entity:<entity>:field:<field>` i18n convention. Resolved
13
+ * through `translate` like any header — an i18n key, or a plain literal
14
+ * shown verbatim if it isn't a key. Also the way to declare a *virtual*
15
+ * column whose `field` is NOT an entity field — a presentational column
16
+ * drawn entirely by a `columnRenderer` component that reads the `row`
17
+ * (e.g. a tag-chips cell), not a stored value. Such a column needs a
18
+ * label; without one it is rejected at boot as an unknown field. `field`
19
+ * is then just a stable column key (pick any unique slug). */
20
+ readonly label?: string;
21
+ /** Marks a column on a query-backed list (projectionList, relatedList,
22
+ * dashboard list, expandableRow) as drawn by a renderer rather than read
23
+ * from a row field. Without it, a query that declares an `outputSchema`
24
+ * must return `field`; a `label` alone does not exempt the column, since
25
+ * nearly every column carries one. */
26
+ readonly virtual?: boolean;
27
+ /** Marks this column as a reference lookup instead of a plain value —
28
+ * for projectionList/relatedList columns, which have no
29
+ * `EntityDefinition` to carry a real `reference` field type. Same
30
+ * target convention as `parseRefTarget`: an entity name (same
31
+ * feature) or `feature:entity` (cross-feature). Ignored on
32
+ * `entityList` columns, where the field's own declared type always
33
+ * wins. */
34
+ readonly refEntity?: string;
35
+ /** Row field on the referenced entity shown as the label (default
36
+ * "id"). Only meaningful together with `refEntity`. */
37
+ readonly refLabelField?: string;
38
+ /** Marks this column as sortable via a clickable header — for
39
+ * relatedList columns, which have no `EntityDefinition` field to carry
40
+ * `sortable` on. Ignored on `entityList` columns, where the entity
41
+ * field's own `sortable` flag wins. */
42
+ readonly sortable?: boolean;
43
+ /** Value type of a relatedList column, which has no `EntityDefinition`
44
+ * field to derive it from. Numeric types right-align the column and its
45
+ * header with tabular figures, like an entityList column of that field
46
+ * type. Ignored on entityList columns, where the field's own type wins. */
47
+ readonly valueType?: "number" | "decimal" | "bigInt" | "money";
48
+ /** Leaves the column out of the compact card layout used below the
49
+ * `md` breakpoint (tables keep it). For columns that only make sense
50
+ * next to the others, like a sort key. */
51
+ readonly hideOnNarrow?: boolean;
52
+ /** Dashboard list panels only. "bar": field holds a 0..1 fraction, drawn
53
+ * as bar + percent. "badge": field holds a label, drawn as a pill whose
54
+ * tone comes from `badgeToneField`. "datetime": field holds an ISO string or
55
+ * epoch ms, shown in the user's locale and time zone. Other list renderers
56
+ * ignore it. */
57
+ readonly display?: ListColumnDisplay;
58
+ /** Row field naming the badge tone; only with `display: "badge"`. */
59
+ readonly badgeToneField?: string;
60
+ };
61
+ export type ListColumnDisplay = "bar" | "badge" | "datetime";
62
+ export type ListBadgeTone = "positive" | "warn" | "negative" | "neutral";
63
+ export type ListPaginationMode = "pages" | "infinite" | false;
64
+ export type ListSortDir = "asc" | "desc";
65
+ export type ListSortSpec = {
66
+ readonly field: string;
67
+ readonly dir: ListSortDir;
68
+ };
69
+ export type ScreenFilterOp = "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
70
+ export type ScreenFilter = {
71
+ readonly field: string;
72
+ readonly op: ScreenFilterOp;
73
+ readonly value: unknown;
74
+ };
75
+ /** Declarative row-field extractor — JSON-safe (no function prop, survives
76
+ * GET /api/schema / buildAppSchema).
77
+ *
78
+ * `pick`: extracts fields 1:1. `{ pick: ["id", "version"] }` → `{ id: row.id, version: row.version }`
79
+ * `map`: renames fields. `{ map: { incidentId: "id" } }` → `{ incidentId: row.id }`
80
+ *
81
+ * Limitation: computed/templated values can't be expressed this way —
82
+ * that logic belongs server-side in the write handler. */
83
+ export type RowFieldExtractor = {
84
+ readonly pick: readonly string[];
85
+ } | {
86
+ readonly map: Readonly<Record<string, string>>;
87
+ };
88
+ /** How a row action renders inline in the row-action column. `"button"` is a
89
+ * bordered button (its colour follows the action's `style`), `"link"` a text
90
+ * link, `"icon"` an icon-only button (needs a resolved icon, falls back to
91
+ * `"link"` without one). `"responsive"` is a `"button"` with icon and label
92
+ * from tablet width up and an icon-only button below it, the label staying the
93
+ * accessible name (needs a resolved icon, falls back to `"button"` without
94
+ * one). Unset: the renderer decides (primary action as link, the rest in the
95
+ * kebab menu). */
96
+ export type RowActionDisplay = "button" | "link" | "icon" | "responsive";
97
+ /** Row-action column layout. `"adaptive"` (default): primary action as link,
98
+ * the rest in a kebab menu — except actions with an explicit `display`, which
99
+ * stay inline. `"inline"`: every action inline, no kebab. */
100
+ export type RowActionMode = "adaptive" | "inline";
101
+ export type RowAction = RowActionWriteHandler | RowActionNavigate | RowActionDrawer | RowActionClientAction;
102
+ export type RowActionWriteHandler = {
103
+ /** Default for RowActions without an explicitly set `kind` —
104
+ * backwards-compatible form. Can also be set explicitly. */
105
+ readonly kind?: "writeHandler";
106
+ /** Stable id per screen — kebab-case, unique within the action set. */
107
+ readonly id: string;
108
+ /** Anzeige-Text (i18n-Key). */
109
+ readonly label: string;
110
+ /** Qualified name of the server handler, e.g.
111
+ * "publicstatus:write:maintenance:start". Dispatched via
112
+ * useDispatcher. */
113
+ readonly handler: string;
114
+ /** Declarative payload per row. Default = `{ id: row.id }`.
115
+ * `pick` extracts fields of the same name; `map` renames. */
116
+ readonly payload?: RowFieldExtractor;
117
+ /** i18n key for the confirm dialog description. When set, a modal opens
118
+ * before execution — the user must confirm explicitly. Together with
119
+ * `style: "danger"` this is the standard safety guard for destructive
120
+ * actions. */
121
+ readonly confirm?: string;
122
+ /** i18n key for the confirm button text in the dialog. Default = `label`
123
+ * (so "Delete" → "Delete" button in the confirm). Set it when the action
124
+ * has a long name ("Mark Subscription as Cancelled") and the button
125
+ * should be shorter ("Cancel Subscription"). */
126
+ readonly confirmLabel?: string;
127
+ /** Conditional visibility per row. */
128
+ readonly visible?: FieldCondition;
129
+ /** Visual style. "danger" renders red + forces a confirm dialog (even
130
+ * without an explicit `confirm` key). */
131
+ readonly style?: "primary" | "secondary" | "danger";
132
+ /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
133
+ * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
134
+ * (renderer/web), analogous to EditFieldSpec.icon. */
135
+ readonly icon?: IconKey;
136
+ /** Inline render form in the row-action column — see `RowActionDisplay`. */
137
+ readonly display?: RowActionDisplay;
138
+ /** Where to go after the write succeeded instead of staying on the shown
139
+ * record. Same forms as entityEdit `redirect`. Honored on record actions
140
+ * (projectionDetail header/section actions, entityEdit header/section
141
+ * actions); a valid `returnTo` wins under the same rule as
142
+ * entityEdit.redirect. Not used by list row actions. Without it, an action
143
+ * deleting the shown record (`<feature>:write:<entity>:delete` with that
144
+ * record's id) leaves to `returnTo`/the list; any other action refetches. */
145
+ readonly redirect?: string | ActionFormRedirect;
146
+ };
147
+ export type RowActionNavigateBase = {
148
+ readonly kind: "navigate";
149
+ readonly id: string;
150
+ readonly label: string;
151
+ /** Field name whose value is embedded as entityId in the URL path
152
+ * (`/<workspace>/<screen>/<entityId>`). entityEdit reads the id
153
+ * EXCLUSIVELY from the path. Default: "id" if the target screen is an
154
+ * entityEdit OR if `entity` (instead of `screen`) is set. */
155
+ readonly entityId?: string;
156
+ /** Declarative URL search params extracted from the clicked row.
157
+ * Both actionForm and entityEdit-create targets read `params` as
158
+ * initial values (pre-filling the form). entityEdit-update does
159
+ * not — the existing record's values take precedence.
160
+ * `pick` extracts fields of the same name; `map` renames them. */
161
+ readonly params?: RowFieldExtractor;
162
+ /** Section id of the tab to activate on the target projectionDetail
163
+ * (layout.mode "tabs"); merged into the navigate search params next to
164
+ * returnTo. Boot validator checks the id exists on the target. */
165
+ readonly tab?: string;
166
+ /** Conditional visibility per row. */
167
+ readonly visible?: FieldCondition;
168
+ /** Visual style only — unlike the writeHandler variants, "danger" does not
169
+ * force a confirm dialog here: the target form is the confirmation. */
170
+ readonly style?: "primary" | "secondary" | "danger";
171
+ /** When true, a click on the whole row (not just the action menu) triggers
172
+ * this navigate action. At most one per list (the boot validator checks).
173
+ * Only on navigate — a row click must not trigger a (possibly destructive,
174
+ * unconfirmed) write, hence not on writeHandler actions. */
175
+ readonly rowClick?: boolean;
176
+ /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
177
+ * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
178
+ * (renderer/web), analogous to EditFieldSpec.icon. */
179
+ readonly icon?: IconKey;
180
+ /** Inline render form in the row-action column — see `RowActionDisplay`. */
181
+ readonly display?: RowActionDisplay;
182
+ };
183
+ /** Exactly one of `screen` / `entity` — mutual exclusivity is type-enforced
184
+ * (boot validator still rejects both/neither for untyped schemas). */
185
+ export type RowActionNavigate = RowActionNavigateBase & ({
186
+ /** Screen id (short, unqualified). The boot validator checks that it
187
+ * exists in the same feature. */
188
+ readonly screen: string;
189
+ readonly entity?: never;
190
+ } | {
191
+ /** Entity name instead of screen id (fw#2228) — resolves at boot time to
192
+ * the screen that declares `detailFor: "<entity>"` (see ObjectTarget/
193
+ * resolveTarget in @cosmicdrift/kumiko-renderer's nav.tsx). */
194
+ readonly entity: string;
195
+ readonly screen?: never;
196
+ });
197
+ export type RowActionDrawer = {
198
+ readonly kind: "drawer";
199
+ readonly id: string;
200
+ readonly label: string;
201
+ /** Short, unqualified id of an `actionForm` screen in the same feature.
202
+ * Boot validator rejects a missing screen and a screen whose `type`
203
+ * isn't `actionForm` — same check as ToolbarAction's drawer variant
204
+ * (see docs/plans/bundled-features-screen-standardisierung.md §2.6c). */
205
+ readonly screen: string;
206
+ /** Declarative prefill extracted from the clicked row. `pick` extracts
207
+ * fields of the same name; `map` renames them. */
208
+ readonly params?: RowFieldExtractor;
209
+ /** Per-row conditional visibility. */
210
+ readonly visible?: FieldCondition;
211
+ /** Visual style only — unlike the writeHandler variants, "danger" does not
212
+ * force a confirm dialog here: the target form is the confirmation. */
213
+ readonly style?: "primary" | "secondary" | "danger";
214
+ /** Overrides the id-based default icon (see ACTION_ICON_BY_ID in
215
+ * kumiko-renderer) — closed IconKey vocabulary into the ICONS registry
216
+ * (renderer/web), analogous to EditFieldSpec.icon. */
217
+ readonly icon?: IconKey;
218
+ /** Inline render form in the row-action column — see `RowActionDisplay`. */
219
+ readonly display?: RowActionDisplay;
220
+ /** Drawer heading (i18n key) instead of the action `label`. `{name}`
221
+ * placeholders resolve from the drawer prefill (`params`), formatted for
222
+ * display like `ActionFormScreenDefinition.summary`. */
223
+ readonly title?: string;
224
+ /** Line under the drawer heading (i18n key or a bare `{name}` template),
225
+ * resolved like `title`. */
226
+ readonly subtitle?: string;
227
+ /** Clicking the row opens this drawer with that row's `params`. At most one
228
+ * `rowClick` action (navigate or drawer) per list; rejected on
229
+ * projectionDetail header actions, like navigate's. */
230
+ readonly rowClick?: boolean;
231
+ };
232
+ export type RowActionClientAction = {
233
+ readonly kind: "clientAction";
234
+ readonly id: string;
235
+ readonly label: string;
236
+ /** Registered client handler name, `<feature>:<kebab>`, e.g.
237
+ * "auth-email-password:request-password-reset". Resolved when the action is
238
+ * triggered; a name no client feature registered shows an error toast. */
239
+ readonly action: string;
240
+ /** Payload handed to the handler, picked from the row (or the shown record).
241
+ * Default `{ id: row.id }`, like a writeHandler. */
242
+ readonly payload?: RowFieldExtractor;
243
+ /** i18n key for the confirm dialog description. */
244
+ readonly confirm?: string;
245
+ /** i18n key for the confirm button, default `label`. */
246
+ readonly confirmLabel?: string;
247
+ /** Conditional visibility per row. */
248
+ readonly visible?: FieldCondition;
249
+ /** Visual style. "danger" forces a confirm dialog, like a writeHandler. */
250
+ readonly style?: "primary" | "secondary" | "danger";
251
+ /** Overrides the id-based default icon, see `RowActionWriteHandler.icon`. */
252
+ readonly icon?: IconKey;
253
+ /** Inline render form in the row-action column — see `RowActionDisplay`. */
254
+ readonly display?: RowActionDisplay;
255
+ /** i18n key of the toast shown after the handler reported `{ ok: true }`
256
+ * (default `kumiko.clientAction.success`). */
257
+ readonly successMessage?: string;
258
+ };
259
+ export type ToolbarAction = {
260
+ readonly kind: "navigate";
261
+ readonly id: string;
262
+ readonly label: string;
263
+ /** Screen id (short, unqualified) to navigate to. */
264
+ readonly screen: string;
265
+ /** Section id of the tab to activate on the target projectionDetail
266
+ * (layout.mode "tabs"); merged into the navigate search params next to
267
+ * returnTo. Boot validator checks the id exists on the target. */
268
+ readonly tab?: string;
269
+ /** Visual style only — unlike the writeHandler variant, "danger" does
270
+ * not force a confirm dialog here: the target form is the
271
+ * confirmation. */
272
+ readonly style?: "primary" | "secondary" | "danger";
273
+ } | {
274
+ readonly kind: "writeHandler";
275
+ readonly id: string;
276
+ readonly label: string;
277
+ readonly handler: string;
278
+ /** Static payload without row context. Default = `{}`. */
279
+ readonly payload?: Record<string, unknown>;
280
+ /** i18n key for the confirm dialog description. When set AND/OR
281
+ * style="danger": modal before execution. */
282
+ readonly confirm?: string;
283
+ /** i18n key for the confirm button text in the dialog. Default = `label`. */
284
+ readonly confirmLabel?: string;
285
+ readonly style?: "primary" | "secondary" | "danger";
286
+ } | {
287
+ readonly kind: "drawer";
288
+ readonly id: string;
289
+ readonly label: string;
290
+ /** Short, unqualified id of an `actionForm` screen in the same
291
+ * feature. Boot validator rejects a missing screen and a screen
292
+ * whose `type` isn't `actionForm` — the caller decides full-page
293
+ * vs. drawer, not the form (see docs/plans/bundled-features-screen-
294
+ * standardisierung.md §2.6c). */
295
+ readonly screen: string;
296
+ /** Visual style only — unlike the writeHandler variant, "danger" does
297
+ * not force a confirm dialog here: the target form is the
298
+ * confirmation. */
299
+ readonly style?: "primary" | "secondary" | "danger";
300
+ /** Drawer heading (i18n key) instead of `label`. */
301
+ readonly title?: string;
302
+ /** Line under the drawer heading (i18n key). */
303
+ readonly subtitle?: string;
304
+ };
305
+ export type RelatedListToolbarAction = (Extract<ToolbarAction, {
306
+ readonly kind: "navigate";
307
+ }> & {
308
+ /** Conditional visibility, evaluated against the relatedList's parent
309
+ * record (the "Akte"). */
310
+ readonly visible?: FieldCondition;
311
+ /** Declarative URL search params extracted from the parent record,
312
+ * prefilling the target screen. Replaces the implicit
313
+ * `{ [parentParam]: parentId }` default when set. */
314
+ readonly params?: RowFieldExtractor;
315
+ }) | (Extract<ToolbarAction, {
316
+ readonly kind: "writeHandler";
317
+ }> & {
318
+ readonly visible?: FieldCondition;
319
+ }) | (Extract<ToolbarAction, {
320
+ readonly kind: "drawer";
321
+ }> & {
322
+ readonly visible?: FieldCondition;
323
+ });
324
+ /** Expandable area of an `entityList` row: a relatedList of the row's record.
325
+ * `id` (tab id) and `countField` (tab badge) have no meaning outside a tabs
326
+ * layout and are therefore omitted. `kind` stays the discriminator so further
327
+ * expansion kinds can be added without a breaking change. */
328
+ export type EntityListExpandableRow = Omit<EditRelatedListSection, "id" | "countField">;
329
+ /** An extra choice on a facet next to the field's own options: a named
330
+ * bundle of values ("Running" = several statuses) or "no filter" (`values:
331
+ * []`, e.g. "All"). Chips only. */
332
+ export type EntityListFacetExtraOption = {
333
+ readonly id: string;
334
+ /** i18n key. */
335
+ readonly label: string;
336
+ /** Option values this choice selects; `[]` selects nothing, i.e. no filter. */
337
+ readonly values: readonly string[];
338
+ /** Where the chip sits relative to the field's own options. Default "start". */
339
+ readonly position?: "start" | "end";
340
+ };
341
+ export type EntityListFacetConfig = {
342
+ /** `"select"` (default): dropdown with checkboxes. `"chips"`: one toggle
343
+ * chip per option, single choice. */
344
+ readonly display?: "select" | "chips";
345
+ /** Chips show how many rows each option matches (other facets and the
346
+ * search apply). Counts come from the list query itself, one `limit: 1`
347
+ * count request per chip. */
348
+ readonly showCounts?: boolean;
349
+ /** Chips with zero matching rows are not shown. Needs counts; implies
350
+ * `showCounts`'s requests even when the number itself is not shown. */
351
+ readonly hideEmpty?: boolean;
352
+ readonly extraOptions?: readonly EntityListFacetExtraOption[];
353
+ };
354
+ export type EntityListScreenDefinition = {
355
+ readonly id: string;
356
+ readonly type: "entityList";
357
+ /** Default true: the screen fills the shell content height, its body scrolls
358
+ * inside and the pager / action bar stays pinned. `false` restores page
359
+ * scroll (table grows with its rows, pager below the last row, form footer
360
+ * in the flow). */
361
+ readonly fillHeight?: boolean;
362
+ readonly nav?: ScreenNavSugar;
363
+ readonly detailFor?: string;
364
+ readonly description?: string;
365
+ readonly agent?: AgentScreenHints;
366
+ /** Screen has no nav entry by design (opened via link or navved by the
367
+ * app); exempts it from the nav-area boot check. */
368
+ readonly dormant?: boolean;
369
+ /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
370
+ readonly visibleWhen?: DashboardPanelVisibility;
371
+ /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
372
+ readonly fallback?: string;
373
+ readonly entity: string;
374
+ /** Short screen id (same feature) the create button opens instead of the entity's default edit screen. */
375
+ readonly createScreen?: string;
376
+ /** 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. */
377
+ readonly createUnavailable?: boolean;
378
+ /** `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`. */
379
+ readonly allowCreate?: boolean;
380
+ readonly columns: readonly ListColumnSpec[];
381
+ readonly rowRenderer?: PlatformComponent;
382
+ readonly cardRenderer?: PlatformComponent;
383
+ /** Per-row actions, rendered as an actions column on the right.
384
+ * Adaptive (default, see `rowActionMode`): the primary action as a link,
385
+ * the rest in the kebab menu; actions with an explicit `display` stay
386
+ * inline. Array order is UI order. */
387
+ readonly rowActions?: readonly RowAction[];
388
+ /** Layout of the row-action column — see `RowActionMode`. */
389
+ readonly rowActionMode?: RowActionMode;
390
+ /** Toolbar actions (list header). "Open Incident", "Schedule Maintenance"
391
+ * etc. — next to "+ New" if present. Array order = UI order,
392
+ * primary-style links. */
393
+ readonly toolbarActions?: readonly ToolbarAction[];
394
+ /** Expandable area under each row: a relatedList of the row's record
395
+ * (parentId = the row's `id`). Rows get a toggle button; several rows can
396
+ * be open at once. Its own row actions run against the sub-list rows, and
397
+ * a successful write also reloads this list. */
398
+ readonly expandableRow?: EntityListExpandableRow;
399
+ /** Server-side filter, fixed on the screen — three buckets of the same
400
+ * entity without custom pages (e.g. "Upcoming" / "Active" / "Past"
401
+ * Maintenance). The user-side q search runs ON the filtered set on top.
402
+ * The boot validator pins that the field exists in the entity. */
403
+ readonly filter?: ScreenFilter;
404
+ readonly pagination?: ListPaginationMode;
405
+ readonly pageSize?: number;
406
+ readonly defaultSort?: ListSortSpec;
407
+ /** Initial facet selection, per filterable field: option values (string
408
+ * facets) or a boolean. Applies only while the URL carries no value for
409
+ * that field, so a link or the user's own choice wins; picking "no filter"
410
+ * (an `extraOptions` entry with `values: []`, or clearing the facet) is
411
+ * remembered in the URL and is not overridden again. */
412
+ readonly defaultFilters?: Readonly<Record<string, readonly string[] | boolean>>;
413
+ /** Per-facet display options for the entity's `filterable` fields, keyed by
414
+ * field name. `false` hides the facet's control (its filter stays usable
415
+ * through `defaultFilters` and the URL). Fields without an entry keep the
416
+ * default dropdown. */
417
+ readonly facets?: Readonly<Record<string, EntityListFacetConfig | false>>;
418
+ /** Replaces the default empty-list title/hint; the create button stays when the list can create. */
419
+ readonly emptyState?: ListEmptyStateSpec;
420
+ /** i18n key for the primary create button label (default `kumiko.actions.create`). */
421
+ readonly createLabel?: string;
422
+ /** i18n key for the search field placeholder (default `kumiko.list.search-placeholder`). */
423
+ readonly searchPlaceholder?: string;
424
+ readonly searchable?: boolean;
425
+ readonly slots?: ScreenSlots;
426
+ readonly access?: AccessRule;
427
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,217 @@
1
+ import type { SelectOptionTone } from "./fields.ts";
2
+ import type { AccessRule, AgentScreenHints } from "./handlers.ts";
3
+ import type { PlatformComponent, ScreenNavSugar, ScreenSlots } from "./screen.ts";
4
+ import type { DashboardPanelVisibility } from "./screen-dashboard.ts";
5
+ import type { EditLayout, ListEmptyStateSpec } from "./screen-entity-edit.ts";
6
+ import type { ListColumnSpec, ListPaginationMode, ListSortSpec, RowAction, RowActionMode, RowFieldExtractor, ScreenFilter, ToolbarAction } from "./screen-entity-list.ts";
7
+ export type ListFacetSpec = {
8
+ readonly field: string;
9
+ readonly type: "select";
10
+ readonly label: string;
11
+ readonly options: readonly {
12
+ readonly value: string;
13
+ readonly label: string;
14
+ }[];
15
+ } | {
16
+ readonly field: string;
17
+ readonly type: "boolean";
18
+ readonly label: string;
19
+ readonly trueLabel: string;
20
+ readonly falseLabel: string;
21
+ } | {
22
+ readonly field: string;
23
+ readonly type: "reference";
24
+ readonly label: string;
25
+ /** Entity name (same feature) or `feature:entity` (cross-feature), same
26
+ * convention as `ListColumnSpec.refEntity`. Options load at render time. */
27
+ readonly entity: string;
28
+ /** Row field on the referenced entity shown as the option label
29
+ * (default "id"). */
30
+ readonly labelField?: string;
31
+ } | {
32
+ /** Declarative only — the date field the range narrows. It is NOT sent
33
+ * to the server (the bounds go under `params`); it namespaces the
34
+ * facet's URL keys and tells a reading agent which column this list
35
+ * is time-rangeable on. */
36
+ readonly field: string;
37
+ readonly type: "dateRange";
38
+ readonly label: string;
39
+ /** Query-payload keys the chosen bounds are sent under as ISO instants.
40
+ * Explicit rather than a `from`/`to` convention — a query is free to
41
+ * name its time bounds anything, and the boot-validator checks these
42
+ * keys against the handler's Zod schema. */
43
+ readonly params: {
44
+ readonly from: string;
45
+ readonly to: string;
46
+ };
47
+ };
48
+ export type ProjectionListScreenDefinition = {
49
+ readonly id: string;
50
+ readonly type: "projectionList";
51
+ /** Default true: the screen fills the shell content height, its body scrolls
52
+ * inside and the pager / action bar stays pinned. `false` restores page
53
+ * scroll (table grows with its rows, pager below the last row, form footer
54
+ * in the flow). */
55
+ readonly fillHeight?: boolean;
56
+ readonly nav?: ScreenNavSugar;
57
+ readonly detailFor?: string;
58
+ readonly description?: string;
59
+ readonly agent?: AgentScreenHints;
60
+ /** Screen has no nav entry by design (opened via link or navved by the
61
+ * app); exempts it from the nav-area boot check. */
62
+ readonly dormant?: boolean;
63
+ /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
64
+ readonly visibleWhen?: DashboardPanelVisibility;
65
+ /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
66
+ readonly fallback?: string;
67
+ readonly query: string;
68
+ readonly columns: readonly ListColumnSpec[];
69
+ readonly rowRenderer?: PlatformComponent;
70
+ readonly cardRenderer?: PlatformComponent;
71
+ readonly rowActions?: readonly RowAction[];
72
+ /** Layout of the row-action column — see `RowActionMode`. */
73
+ readonly rowActionMode?: RowActionMode;
74
+ readonly toolbarActions?: readonly ToolbarAction[];
75
+ readonly pagination?: ListPaginationMode;
76
+ readonly pageSize?: number;
77
+ readonly defaultSort?: ListSortSpec;
78
+ /** Replaces the default empty-list title/hint; the create button stays when the list can create. */
79
+ readonly emptyState?: ListEmptyStateSpec;
80
+ /** i18n key for the primary create button label (default `kumiko.actions.create`). */
81
+ readonly createLabel?: string;
82
+ /** i18n key for the search field placeholder (default `kumiko.list.search-placeholder`). */
83
+ readonly searchPlaceholder?: string;
84
+ readonly searchable?: boolean;
85
+ /** Derived by buildAppSchema from the query handler's Zod schema (`sort`
86
+ * param present). Same type serves author and wire schema (no separate
87
+ * client-facing screen type exists), so this stays structurally
88
+ * writable — validateProjectionListScreens rejects any hand-authored
89
+ * value at boot (fw#2165). */
90
+ readonly sortable?: boolean;
91
+ /** Derived by buildAppSchema from the query handler's Zod schema (`cursor`
92
+ * or `offset` param present). See `sortable` doc for why this is
93
+ * boot-enforced rather than type-enforced. */
94
+ readonly paginated?: boolean;
95
+ /** Server-side filter, fixed on the screen — see `ScreenFilter` doc above
96
+ * (entityList). Field existence can't be checked without an entity; the
97
+ * boot-validator only checks structure (`op:"in"` ⇒ array value). */
98
+ readonly filter?: ScreenFilter;
99
+ /** User-toggleable facet dropdowns — see `ListFacetSpec` doc. `field`
100
+ * must be a declared column, except for `type: "reference"`: it filters
101
+ * by id through the bound query's `filters` and needs a registered
102
+ * `entity` instead. The bound query handler must accept `filters` in its
103
+ * Zod schema — the boot-validator checks both. A
104
+ * "dateRange" facet instead needs its two `params` keys in that schema;
105
+ * it sends no `filters` entry. */
106
+ readonly facets?: readonly ListFacetSpec[];
107
+ readonly slots?: ScreenSlots;
108
+ readonly access?: AccessRule;
109
+ };
110
+ export type RecordHeaderSubtitlePart = string | {
111
+ readonly field: string;
112
+ readonly navigate?: MetricNavigate;
113
+ };
114
+ export type RecordHeaderSpec = {
115
+ readonly title: string;
116
+ /** Field name, or several parts (field name or `{ field, navigate }`)
117
+ * joined by a separator; parts with an empty value are dropped. */
118
+ readonly subtitle?: string | readonly RecordHeaderSubtitlePart[];
119
+ readonly status?: string;
120
+ /** Record field holding an absolute http(s) URL. When present and the
121
+ * field's value is such a URL, the subtitle renders as an external link
122
+ * (`target="_blank"`) instead of plain text. Only valid with the string
123
+ * form of `subtitle`. */
124
+ readonly subtitleHref?: string;
125
+ /** Record field whose value selects the `statusTones` entry, for a badge text that
126
+ * carries more than the state (e.g. "Terminated on 31.03.2027"). Defaults to `status`. */
127
+ readonly statusToneField?: string;
128
+ /** Badge tone per `statusToneField` value (default: the `status` value); unlisted values fall back to the value heuristic. */
129
+ readonly statusTones?: {
130
+ readonly [statusValue: string]: SelectOptionTone;
131
+ };
132
+ };
133
+ export type MetricNavigate = {
134
+ readonly screen?: string;
135
+ readonly entity?: string;
136
+ readonly entityId?: string;
137
+ readonly params?: RowFieldExtractor;
138
+ /** Section id of the tab to activate at the destination. Alone (no
139
+ * `screen`/`entity`), it activates that tab on the current record
140
+ * instead of navigating away. */
141
+ readonly tab?: string;
142
+ };
143
+ export type MetricSpec = {
144
+ readonly field: string;
145
+ readonly label: string;
146
+ readonly navigate?: MetricNavigate;
147
+ };
148
+ export type ProjectionDetailScreenDefinition = {
149
+ readonly id: string;
150
+ readonly type: "projectionDetail";
151
+ /** Default true: the screen fills the shell content height, its body scrolls
152
+ * inside and the pager / action bar stays pinned. `false` restores page
153
+ * scroll (table grows with its rows, pager below the last row, form footer
154
+ * in the flow). */
155
+ readonly fillHeight?: boolean;
156
+ readonly nav?: ScreenNavSugar;
157
+ readonly detailFor?: string;
158
+ readonly description?: string;
159
+ readonly agent?: AgentScreenHints;
160
+ /** Screen has no nav entry by design (opened via link or navved by the
161
+ * app); exempts it from the nav-area boot check. */
162
+ readonly dormant?: boolean;
163
+ /** UI-only gate, also on direct URL access; handlers still enforce access. Unmet or failed renders `fallback`. */
164
+ readonly visibleWhen?: DashboardPanelVisibility;
165
+ /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
166
+ readonly fallback?: string;
167
+ readonly query: string;
168
+ /** Query output field whose value titles the page header (breadcrumb) when no `header` is set. */
169
+ readonly recordTitleField?: string;
170
+ /** Query-payload key for the row-id. Default "id". */
171
+ readonly idParam?: string;
172
+ /** The server determines the shown row from the caller's session/context
173
+ * instead of a row id in the path — a self-service screen ("my profile",
174
+ * "my data") that has no row to link to (unlike `EntityEditScreenDefinition.
175
+ * singleton`, which still resolves via `<entity>:list` limit 1; here the
176
+ * query itself owns the row selection, e.g. `user:query:user:me`). The
177
+ * query is called WITHOUT the `idParam` key — there is no id to send, so
178
+ * the boot-validator rejects declaring `idParam` alongside this flag
179
+ * instead of letting one silently win. Stricter than an id-addressed
180
+ * screen, not looser: the server picks the row, so no client-supplied id
181
+ * can reach the query — a stray id on the path is ignored, never
182
+ * forwarded. */
183
+ readonly singleton?: boolean;
184
+ readonly layout: EditLayout;
185
+ /** Optional per-field label i18n key (field name → key), analogous to
186
+ * entityEdit.fieldLabels. The pseudo entity `__projection-detail__` has
187
+ * no natural field namespace — a missing entry falls back to the
188
+ * convention `<feature>:entity:__projection-detail__:field:<name>`. */
189
+ readonly fieldLabels?: Readonly<Record<string, string>>;
190
+ /** Record header above the layout — title/subtitle/status name columns
191
+ * in the query row (not literals). Rendered only when set. */
192
+ readonly header?: RecordHeaderSpec;
193
+ /** Metric band above the layout — column names in the query row, each with its own i18n `label`.
194
+ * Rendered only when set. */
195
+ readonly metrics?: readonly MetricSpec[];
196
+ /** Parent list screen (short id) for a "back" navigation. */
197
+ readonly listScreenId?: string;
198
+ readonly slots?: ScreenSlots;
199
+ readonly access?: AccessRule;
200
+ /** Header action buttons. Reuses `RowAction` — the screen's single
201
+ * displayed record stands in for the "row", so `pick`/`map`, `visible`,
202
+ * `confirm`/`confirmLabel` and `style` keep their meaning unchanged.
203
+ * `rowClick` has no target here (there is no row to click) and is
204
+ * rejected by the boot-validator. */
205
+ readonly actions?: readonly RowAction[];
206
+ /** Id of the action that stays the visible header button when the actions
207
+ * collapse into a menu (and the one that keeps the phone icon slot when it
208
+ * has an icon and no confirm). An `actions` id or `"edit"` (the default edit
209
+ * action); default precedence is `edit`, then `style: "primary"`, then the
210
+ * first action. */
211
+ readonly primaryAction?: string;
212
+ /** How head fields render. Default "text" — plain text instead of a
213
+ * disabled Input, since every field on this screen type is forced
214
+ * readOnly anyway (there is no write path, see the shim doc above).
215
+ * Set "form" to opt back into the disabled-Input look (fw#2245). */
216
+ readonly valueDisplay?: "form" | "text";
217
+ };
@@ -0,0 +1 @@
1
+ export {};