@cosmicdrift/kumiko-types 0.413.1 → 0.415.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/access-predicates.d.ts +1 -0
  2. package/dist/access-predicates.js +3 -3
  3. package/dist/config.d.ts +4 -2
  4. package/dist/config.js +10 -1
  5. package/dist/db-connection.d.ts +0 -3
  6. package/dist/define-handler.d.ts +1 -1
  7. package/dist/delivery-attempt-types.d.ts +22 -0
  8. package/dist/delivery-attempt-types.js +31 -0
  9. package/dist/entity-table-meta-types.d.ts +2 -0
  10. package/dist/event-store-executor-types.d.ts +4 -1
  11. package/dist/feature.d.ts +1 -0
  12. package/dist/fields.d.ts +2 -0
  13. package/dist/handler-context-facets.d.ts +5 -2
  14. package/dist/handler-context-services.d.ts +2 -1
  15. package/dist/handlers.d.ts +17 -15
  16. package/dist/handlers.js +4 -32
  17. package/dist/hooks.d.ts +1 -0
  18. package/dist/identifiers.d.ts +7 -0
  19. package/dist/identifiers.js +20 -0
  20. package/dist/minor-units.d.ts +3 -0
  21. package/dist/minor-units.js +11 -0
  22. package/dist/multi-stream-apply-context-types.d.ts +3 -1
  23. package/dist/named-list-filters.d.ts +39 -0
  24. package/dist/nav-icon.js +1 -1
  25. package/dist/observability/provider.d.ts +0 -2
  26. package/dist/projection.d.ts +2 -1
  27. package/dist/screen-dashboard.d.ts +139 -22
  28. package/dist/screen-entity-edit.d.ts +2 -0
  29. package/dist/screen-entity-list.d.ts +83 -19
  30. package/dist/screen-list-table.d.ts +49 -3
  31. package/dist/screen-list-table.js +35 -1
  32. package/dist/screen-projection.d.ts +58 -4
  33. package/dist/screen-record-guidance.d.ts +96 -0
  34. package/dist/screen-record-guidance.js +1 -0
  35. package/dist/screen.d.ts +9 -5
  36. package/dist/screen.js +1 -1
  37. package/package.json +9 -1
  38. package/src/changes.json +6 -0
@@ -1,3 +1,4 @@
1
+ import type { SelectOptionTone } from "./fields.ts";
1
2
  import type { AccessRule, AgentScreenHints } from "./handlers.ts";
2
3
  import type { PlatformComponent, ScreenNavSugar, ScreenSlots } from "./screen.ts";
3
4
  import type { ListColumnSpec, RowAction } from "./screen-entity-list.ts";
@@ -56,7 +57,14 @@ export type DashboardPanelQueryOptions = {
56
57
  export type DashboardPanelGate = {
57
58
  readonly visibleWhen?: DashboardPanelVisibility;
58
59
  };
59
- export type DashboardPanelSpan = "half" | "full";
60
+ /** Phone layout switch shared by every panel kind. */
61
+ export type DashboardPanelNarrow = {
62
+ /** Leaves the panel out below the `sm` breakpoint (phone). Not valid on a stat-group child: hide the group. */
63
+ readonly hideOnNarrow?: true;
64
+ /** Position below `sm`, like CSS `order`: panels are laid out by ascending value, a panel without one counts as 0, equal values keep the declared order. Integer ≥ 0. Desktop keeps the declared order. */
65
+ readonly narrowOrder?: number;
66
+ };
67
+ export type DashboardPanelSpan = "half" | "full" | "third" | "two-thirds";
60
68
  export type DashboardPanelEmptyState = {
61
69
  readonly span?: DashboardPanelSpan;
62
70
  readonly emptyLabel?: string;
@@ -65,7 +73,7 @@ export type DashboardPanelEmptyState = {
65
73
  };
66
74
  export declare const DASHBOARD_STAT_TONES: readonly ["default", "positive", "ok", "warn", "negative"];
67
75
  export type DashboardStatTone = (typeof DASHBOARD_STAT_TONES)[number];
68
- export type DashboardStatPanel = DashboardPanelQueryOptions & DashboardPanelGate & {
76
+ export type DashboardStatPanel = DashboardPanelQueryOptions & DashboardPanelGate & DashboardPanelNarrow & {
69
77
  readonly kind: "stat";
70
78
  /** Stable id: kebab-case, unique within the panel set. */
71
79
  readonly id: string;
@@ -73,6 +81,10 @@ export type DashboardStatPanel = DashboardPanelQueryOptions & DashboardPanelGate
73
81
  readonly label: string;
74
82
  readonly query: string;
75
83
  readonly valueField: string;
84
+ /** Field of the query answer holding the label text, e.g. "Contracts ending by 31.01.2027"; it replaces the static `label` while it holds a non-empty string. The static `label` stays the fallback and the loading/error title. */
85
+ readonly labelField?: string;
86
+ /** i18n key shown instead of the label in the one-line phone strip of a titled stat-group ("2 requests"). Only used for stats inside a stat-group. */
87
+ readonly narrowLabel?: string;
76
88
  readonly subField?: string;
77
89
  readonly toneField?: string;
78
90
  /** Formats a numeric value as currency (minor units) or, with `relativeTime`, as
@@ -110,6 +122,8 @@ export type DashboardStatPanel = DashboardPanelQueryOptions & DashboardPanelGate
110
122
  readonly navigate?: {
111
123
  readonly screen: string;
112
124
  readonly namedFilter?: string;
125
+ /** Search params for the target list, e.g. { tab: "failed" }; merged with the namedFilter param. */
126
+ readonly params?: Readonly<Record<string, string>>;
113
127
  };
114
128
  };
115
129
  export type DashboardChartKind = "timeseries" | "stacked-bars" | "segment-bars" | "stacked-area";
@@ -134,7 +148,7 @@ export type DashboardChartRanges = {
134
148
  readonly options: readonly DashboardChartRangeOption[];
135
149
  readonly default: string;
136
150
  };
137
- export type DashboardChartPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & {
151
+ export type DashboardChartPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & DashboardPanelNarrow & {
138
152
  readonly kind: "chart";
139
153
  readonly id: string;
140
154
  readonly label: string;
@@ -175,77 +189,181 @@ export type DashboardChartPanel = DashboardPanelQueryOptions & DashboardPanelEmp
175
189
  /** 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. */
176
190
  readonly markerLabels?: boolean;
177
191
  };
178
- export type DashboardListPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & {
192
+ export type DashboardListPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & DashboardPanelNarrow & {
179
193
  readonly kind: "list";
180
194
  readonly id: string;
181
195
  readonly label: string;
182
196
  readonly query: string;
183
197
  readonly columns: readonly ListColumnSpec[];
198
+ /** Row field holding a SelectOptionTone; tints the row and is the default tone of `hint` columns. */
199
+ readonly rowToneField?: string;
200
+ /** How `rowToneField` marks a table row from `sm` up: "bar" (default) draws a stripe at the left edge, "dot" a small dot in front of the first cell. Cards below `sm` always get the stripe. */
201
+ readonly rowToneMarker?: DashboardListRowToneMarker;
202
+ /** Icon left of the one-line phone variant (`narrow.layout: "line"`); not drawn from `sm` up. Resolved like `DashboardStatGroupPanel.icon`. */
203
+ readonly icon?: PlatformComponent;
204
+ /** `false` drops the column header row; default true. */
205
+ readonly showHeader?: boolean;
206
+ /** i18n key, a line under `label`; with `showTotal` it stands before the count. */
207
+ readonly subtitle?: string;
208
+ /** Adds "n of m" to the subtitle from the rows shown and the query's `total`. An object sets the text: `text` is an i18n key with `{shown}` and `{total}` ("the {shown} most urgent of {total} items"), `narrowText` replaces it below `sm`. */
209
+ readonly showTotal?: true | DashboardListShowTotal;
210
+ /** Link under the table to the full list; with a known `total` above the rows shown, "k more" stands before it. */
211
+ readonly footerLink?: DashboardListFooterLink;
212
+ /** Phone layout below `sm`; the table stays from `sm` up. */
213
+ readonly narrow?: DashboardListNarrow;
214
+ /** Makes each table row a link to a screen; a small icon at the right edge marks it. The cards below `sm` get no extra icon. */
215
+ readonly rowLink?: DashboardListRowLink;
216
+ };
217
+ /** "bar": stripe at the left edge of the row. "dot": small tone dot in front of the first cell. */
218
+ export type DashboardListRowToneMarker = "bar" | "dot";
219
+ /** Row link of a list panel: one fixed target screen (`screen`) or, when the rows point to different places, the screen named by each row (`screenField`). */
220
+ export type DashboardListRowLink = {
221
+ /** "arrow" (default) draws ↗ for a jump to another place, "chevron" draws › for opening the row. */
222
+ readonly icon?: "arrow" | "chevron";
223
+ } & ({
224
+ /** Target screen, `feature:screen:name` or the short id in the same feature. */
225
+ readonly screen: string;
226
+ /** Row field holding the params object of the target (`entityId` opens that record, other keys become search params). */
227
+ readonly paramField?: string;
228
+ readonly screenField?: never;
229
+ readonly paramsField?: never;
230
+ } | {
231
+ /** Row field holding the qualified screen name of the target; a row without one is no link. */
232
+ readonly screenField: string;
233
+ /** Row field holding the params object of the target, like `paramField`. */
234
+ readonly paramsField?: string;
235
+ readonly screen?: never;
236
+ readonly paramField?: never;
237
+ });
238
+ /** Texts of the "n of m" count in a list panel's subtitle; without `text` the default i18n text stands. */
239
+ export type DashboardListShowTotal = {
240
+ /** i18n key, placeholders `{shown}` and `{total}`. */
241
+ readonly text?: string;
242
+ /** i18n key used instead of `text` below `sm`. */
243
+ readonly narrowText?: string;
244
+ };
245
+ /** "cards": one card per row (tone stripe; badge column and `hint.detailField` on top, the hint sentence bold, button column bottom right,
246
+ * `showOnNarrowOnly` columns bottom left; the button column bottom right). "line": a single clickable line "<label> · <value of the first row> · k more" to `footerLink`, which it needs;
247
+ * k is the total of the list (the query's `total`, else the rows loaded) minus one.
248
+ * Default "cards". `limit` shows only the first n rows; subtitle count and footer follow it. */
249
+ export type DashboardListNarrow = {
250
+ readonly limit?: number;
251
+ readonly layout?: "cards" | "line";
252
+ /** Query output field whose value of the first row stands behind the label in the "line" layout; it need not be a column of the panel. Default: the first text column. */
253
+ readonly field?: string;
254
+ /** i18n key replacing "k more" in the "line" layout; `{count}` is the total minus the first row. */
255
+ readonly moreText?: string;
256
+ /** Row field replacing the card title (the hint sentence) in the "cards" layout, e.g. a shorter sentence for the phone. Needs a hint column. */
257
+ readonly titleField?: string;
258
+ };
259
+ /** Link to a screen from a panel; `label` is an i18n key, `namedFilter` only for list screens. */
260
+ export type DashboardListFooterLink = {
261
+ readonly label: string;
262
+ /** i18n key of the text before the link when more rows exist than shown; `{count}` is the rest. Default "k more". */
263
+ readonly moreText?: string;
264
+ /** i18n key used instead of `moreText` below `sm`. */
265
+ readonly narrowMoreText?: string;
266
+ /** Draw the link only below `sm`, where it is the target of the "line" layout; from `sm` up the panel shows no footer link. */
267
+ readonly narrowOnly?: boolean;
268
+ readonly screen: string;
269
+ readonly namedFilter?: string;
270
+ readonly params?: Readonly<Record<string, string>>;
271
+ };
272
+ /** Link at the right edge of a stat-group panel; `label` is an i18n key. */
273
+ export type DashboardStatGroupLink = {
274
+ readonly label: string;
275
+ readonly screen: string;
276
+ readonly params?: Readonly<Record<string, string>>;
184
277
  };
185
- export type DashboardStatGroupPanel = DashboardPanelGate & {
278
+ export type DashboardStatGroupLayout = "cards" | "strip";
279
+ export type DashboardStatGroupPanel = DashboardPanelGate & DashboardPanelNarrow & {
186
280
  readonly kind: "stat-group";
187
281
  readonly id: string;
188
282
  readonly label?: string;
189
283
  /** i18n key, a line under `label`; the boot validator requires `label`. */
190
284
  readonly subtitle?: string;
191
285
  readonly span?: DashboardPanelSpan;
286
+ /** Default: "strip" without `label`, "cards" with. "strip" with a `label` puts label and subtitle
287
+ * left, then one box per stat (number large, label bold, sub line small) and the link at the right edge; under sm it collapses to
288
+ * one line "<title> · <n> <narrowLabel ?? label> · …" with a chevron, clickable to the `link`. */
289
+ readonly layout?: DashboardStatGroupLayout;
290
+ /** Icon left of the one-line phone strip (below `sm`); not drawn on wider screens. Resolved like `DashboardStatPanel.icon`. */
291
+ readonly icon?: PlatformComponent;
292
+ /** Link at the right edge of the panel (label + chevron). */
293
+ readonly link?: DashboardStatGroupLink;
294
+ /** Phone look of the one-line strip; needs a label and layout "strip". */
295
+ readonly narrow?: DashboardStatGroupNarrow;
296
+ /** Tiles per row from `md` up, only with `layout: "cards"`; one column below `sm`. Default: follows the number of tiles. */
297
+ readonly columns?: 2 | 3 | 4;
192
298
  readonly stats: readonly DashboardStatPanel[];
193
299
  };
194
- export type DashboardFeedPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & {
300
+ export type DashboardStatGroupNarrow = {
301
+ /** Tints the one-line phone strip (background and border) in this tone, e.g. "accent" for a promo-blue inbox line. */
302
+ readonly tone?: SelectOptionTone;
303
+ };
304
+ export type DashboardFeedPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & DashboardPanelNarrow & {
195
305
  readonly kind: "feed";
196
306
  readonly id: string;
197
307
  readonly label: string;
198
308
  readonly query: string;
199
309
  };
200
310
  export type DashboardProgressTone = "default" | "danger";
201
- export type DashboardProgressListPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & {
311
+ export type DashboardProgressListPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & DashboardPanelNarrow & {
202
312
  readonly kind: "progress-list";
203
313
  readonly id: string;
204
314
  readonly label: string;
205
315
  readonly query: string;
206
316
  };
207
- export type DashboardCustomPanel = {
317
+ export type DashboardCustomPanel = DashboardPanelNarrow & {
208
318
  readonly kind: "custom";
209
319
  readonly id: string;
210
320
  readonly component: PlatformComponent;
321
+ /** Grid width like the other panels; default is the full row. */
322
+ readonly span?: DashboardPanelSpan;
211
323
  /** Static author values laid over the screen's filterParams, like
212
324
  * `DashboardPanelQueryOptions.params`; the panel wins on a name clash. */
213
325
  readonly params?: Readonly<Record<string, unknown>>;
214
326
  };
215
327
  export type DashboardTextBlock = {
216
328
  readonly kind: "heading";
217
- readonly text: string;
329
+ readonly text: DashboardText;
218
330
  } | {
219
331
  readonly kind: "paragraph";
220
- readonly text: string;
221
- /** Default "muted", like body copy inside panels. */
222
- readonly emphasis?: "muted" | "strong";
332
+ readonly text: DashboardText;
333
+ /** Default "muted", like body copy inside panels. "lead" is the larger serif opening sentence of a briefing. */
334
+ readonly emphasis?: "muted" | "strong" | "lead";
223
335
  } | {
224
336
  readonly kind: "list";
225
- readonly items: readonly string[];
337
+ readonly items: readonly DashboardText[];
226
338
  } | {
227
339
  readonly kind: "code";
228
340
  readonly code: string;
229
341
  } | {
230
342
  readonly kind: "hint";
231
- readonly text: string;
343
+ readonly text: DashboardText;
232
344
  } | {
233
345
  readonly kind: "link";
234
- /** Link label (i18n key). */
235
- readonly text: string;
346
+ /** Link label (i18n key or DashboardI18nText). */
347
+ readonly text: DashboardText;
236
348
  /** Same-feature short id or cross-feature QN `<feature>:screen:<id>`. */
237
349
  readonly screen: string;
238
350
  /** Show the link only to users holding one of these roles. Display
239
351
  * gate only: the target screen's own `access` stays the enforcement. */
240
352
  readonly roles?: readonly string[];
241
353
  };
242
- export type DashboardTextPanel = DashboardPanelGate & {
354
+ export type DashboardTextPanel = DashboardPanelGate & DashboardPanelQueryOptions & DashboardPanelNarrow & {
243
355
  readonly kind: "text";
244
356
  readonly id: string;
245
357
  /** Panel title (i18n key). */
246
358
  readonly label?: string;
247
359
  readonly span?: DashboardPanelSpan;
248
- readonly blocks: readonly DashboardTextBlock[];
360
+ /** Query answering `{ blocks: DashboardTextBlock[] }`; its blocks replace the static ones.
361
+ * Static blocks show while it loads, on error and when it answers empty. It may add
362
+ * `narrowBlocks`, which the renderer shows below `sm` instead of `blocks`. */
363
+ readonly query?: string;
364
+ readonly blocks?: readonly DashboardTextBlock[];
365
+ /** Shown instead of `blocks` below `sm`; a query answering `{ blocks, narrowBlocks }` replaces both. Needs `blocks`. */
366
+ readonly narrowBlocks?: readonly DashboardTextBlock[];
249
367
  };
250
368
  export type DashboardPanelVisibility = {
251
369
  readonly query: string;
@@ -257,7 +375,7 @@ export type ScreenVisibility = DashboardPanelVisibility & {
257
375
  * the gate query runs with an empty payload. */
258
376
  readonly idParam?: string;
259
377
  };
260
- export type DashboardScreenPanel = {
378
+ export type DashboardScreenPanel = DashboardPanelNarrow & {
261
379
  readonly kind: "screen";
262
380
  readonly id: string;
263
381
  /** Same-feature short id or cross-feature QN `<feature>:screen:<id>`. */
@@ -355,9 +473,8 @@ export type DashboardScreenDefinition = {
355
473
  readonly timeRange?: DashboardTimeRangeDefinition;
356
474
  /** Marks the screen as spanning more than the caller's own scope. */
357
475
  readonly scope?: DashboardScopeDefinition;
358
- /** Show the "Updated at" timestamp in the toolbar. Default true; turn off for
359
- * dashboards that host forms rather than live metrics, where a load time
360
- * next to the form reads as stale data. */
476
+ /** Show today's date (server clock, weekday and day, no time) in the top bar next to the title; hidden below `sm`.
477
+ * Default true; turn off for dashboards that host forms rather than live metrics. */
361
478
  readonly showUpdatedAt?: boolean;
362
479
  readonly slots?: ScreenSlots;
363
480
  readonly access?: AccessRule;
@@ -440,6 +440,8 @@ export type EntityEditScreenDefinition = {
440
440
  readonly entity: string;
441
441
  /** Entity field whose value names the loaded record in the header breadcrumb (edit mode only). */
442
442
  readonly recordTitleField?: string;
443
+ /** Focused shell while the record is open: the app header actions leave the shell header, and on phones a top bar with the record title, a back arrow to the parent list and the page-header overflow menu replaces it. Default off. */
444
+ readonly focus?: boolean;
443
445
  /** Screen title (i18n key) with `{field}` placeholders filled from the
444
446
  * current form values, so the title follows what the user types (create
445
447
  * wizard: "Add {make} {model}"). Falls back to the plain title while the
@@ -1,6 +1,6 @@
1
1
  import type { FieldCondition } from "./fields.ts";
2
2
  import type { AccessRule, AgentScreenHints } from "./handlers.ts";
3
- import type { NamedListFilterRef } from "./named-list-filters.ts";
3
+ import type { ListSortDir, ListSortOption, ListSortSpec, NamedFilterFacetOptions, NamedListFilterRef } from "./named-list-filters.ts";
4
4
  import type { IconKey } from "./nav-icon.ts";
5
5
  import type { FieldRenderer, PlatformComponent, ScreenNavSugar, ScreenSlots } from "./screen.ts";
6
6
  import type { ActionFormRedirect } from "./screen-action-form.ts";
@@ -9,7 +9,7 @@ import type { ListEmptyStateSpec } from "./screen-entity-edit.ts";
9
9
  import type { EntityListExpandableRow } from "./screen-expandable-row.ts";
10
10
  import type { ScreenFallback } from "./screen-fallback.ts";
11
11
  import type { ListBulkAction } from "./screen-list-bulk.ts";
12
- import type { ListColumnPin, ListTableOptions } from "./screen-list-table.ts";
12
+ import type { ListColumnPin, ListNarrowFacets, ListTableOptions, ListToolbarActionsPlacement } from "./screen-list-table.ts";
13
13
  export type ListColumnSpec = string | {
14
14
  readonly field: string;
15
15
  readonly renderer?: FieldRenderer;
@@ -45,47 +45,85 @@ export type ListColumnSpec = string | {
45
45
  * `sortable` on. Ignored on `entityList` columns, where the entity
46
46
  * field's own `sortable` flag wins. */
47
47
  readonly sortable?: boolean;
48
- /** Value type of a relatedList column, which has no `EntityDefinition`
48
+ /** Value type of a projectionList or relatedList column, which has no `EntityDefinition`
49
49
  * field to derive it from. Numeric types right-align the column and its
50
50
  * header with tabular figures, like an entityList column of that field
51
51
  * type. Ignored on entityList columns, where the field's own type wins. */
52
52
  readonly valueType?: "number" | "decimal" | "bigInt" | "money";
53
+ /** `false` leaves an empty cell blank instead of drawing the "—" placeholder. Not combinable with `hint`, whose empty sentence is `hint.emptyLabel`. */
54
+ readonly emptyPlaceholder?: false;
53
55
  /** Leaves the column out of the compact card layout used below the
54
56
  * `md` breakpoint (tables keep it). For columns that only make sense
55
57
  * next to the others, like a sort key. */
56
58
  readonly hideOnNarrow?: boolean;
59
+ /** Counterpart of `hideOnNarrow`: the column shows only in the compact card layout and not in the table.
60
+ * Not combinable with `hideOnNarrow`, `hidden` or `pinned`. */
61
+ readonly showOnNarrowOnly?: true;
62
+ /** In the compact card, consecutive `narrowInline` columns stand side by side on one line (12px gap, no separator) instead of one line each. */
63
+ readonly narrowInline?: true;
57
64
  /** Start state for the column picker: the column begins hidden and the
58
65
  * user can show it. Only valid together with `table.columnPicker`. */
59
66
  readonly hidden?: boolean;
60
67
  /** Start pin for the column: it sticks to this edge while the table scrolls
61
68
  * horizontally. Only valid together with `table.pinning`. */
62
69
  readonly pinned?: ListColumnPin;
63
- /** Start width in px. Only valid together with `table.resizing`. */
70
+ /** Start width in px. Needs `table.resizing`, except on a hint column, which sizes itself. */
64
71
  readonly width?: number;
65
- /** Smallest width in px the user can resize to. Only valid together with `table.resizing`. */
72
+ /** Smallest width in px the user can resize to. Needs `table.resizing`, except on a hint column. */
66
73
  readonly minWidth?: number;
67
- /** Dashboard list panels only. "bar": field holds a 0..1 fraction, drawn
68
- * as bar + percent. "badge": field holds a label, drawn as a pill whose
69
- * tone comes from `badgeToneField`. "datetime": field holds an ISO string or
70
- * epoch ms, shown in the user's locale and time zone. Other list renderers
71
- * ignore it. */
74
+ /** One of "bar" | "badge" | "datetime" | "button". "bar": field holds a 0..1 fraction,
75
+ * drawn as bar + percent (dashboard list panels only). "badge": field holds a label,
76
+ * drawn as a pill whose tone comes from `badgeToneField` (dashboard list panels and the
77
+ * RenderList tables of entityList and projectionList). "datetime": field holds an ISO
78
+ * string or epoch ms, shown in the user's locale and time zone (dashboard list panels
79
+ * only). "button": a column with a screen link (linkScreenField or refEntity) draws the
80
+ * link as a small outline button, in dashboard list panels only; a row button in an
81
+ * entityList or projectionList table is a rowAction with display "button" (boot-rejected
82
+ * on columns there). Other list renderers ignore `display`. */
72
83
  readonly display?: ListColumnDisplay;
73
84
  /** Row field naming the badge tone; only with `display: "badge"`. */
74
85
  readonly badgeToneField?: string;
86
+ /** `false` draws the badge pill without its leading dot; only with `display: "badge"`. */
87
+ readonly badgeDot?: false;
75
88
  /** Row field holding a screen QN (`<feature>:screen:<id>`): the cell links
76
89
  * to that screen when the row sets it and the user may open it. For
77
90
  * query-backed lists (projectionList, relatedList), whose rows can point
78
91
  * at a different screen each. */
79
92
  readonly linkScreenField?: string;
93
+ /** Row field holding a params object for the linkScreenField target: `entityId` opens that record, the other keys become search params (e.g. actionForm prefill). Only with linkScreenField. */
94
+ readonly linkParamsField?: string;
95
+ /** "To do" cell: the column's field holds a sentence, shown emphasized in the row tone; an empty value shows `emptyLabel`. Owns the cell rendering, so no `renderer`. */
96
+ readonly hint?: ListColumnHint;
80
97
  };
81
- export type ListColumnDisplay = "bar" | "badge" | "datetime";
98
+ export type ListColumnHint = {
99
+ /** Row field with a second, muted line (e.g. the due date as text). */
100
+ readonly detailField?: string;
101
+ /** Row field shown as the detail line below `sm` instead of `detailField` (a shorter line, e.g. "5 days · Anna Schneider"). Needs `detailField`. */
102
+ readonly narrowDetailField?: string;
103
+ /** Row field shown as the sentence on the phone card instead of the column's own value (a shorter sentence); the table keeps the column value. An empty value falls back to it. */
104
+ readonly narrowField?: string;
105
+ /** Row field holding a SelectOptionTone; defaults to the screen's `rowToneField`. Tone `neutral` draws the sentence muted and not bold, like `emptyLabel`, and no stripe on the phone card. */
106
+ readonly toneField?: string;
107
+ /** Row field with a longer reason, shown behind a "Why?" toggle in the cell. */
108
+ readonly reasonField?: string;
109
+ /** Row field (boolean): while true, the `reasonField` text stands open as a box under the row, over the full row width, in the table view (entity/projection lists from `md` up, dashboard lists from `sm` up); the (i) stays and closes it. Cards and narrower views keep the plain toggle. Needs `reasonField`. */
110
+ readonly reasonOpenField?: string;
111
+ /** Row field with a bold lead-in of the reason paragraph ("Rule 10 · Case open."); without it the reason has none. Needs `reasonField`. */
112
+ readonly reasonTitleField?: string;
113
+ /** i18n key for an empty sentence; default `kumiko.list.hint.empty`. */
114
+ readonly emptyLabel?: string;
115
+ /** The sentence keeps the normal text color (still bold); the tone stays on the row stripe, a badge and the detail line. */
116
+ readonly plainSentence?: true;
117
+ };
118
+ /** Computed fields for the loaded rows: `query` calls with `{ ...params, ids }`, answering `{ rows: [{ id, ...fields }] }`. Fields are merged into the rows and win over same-named row fields. The ids go out in batches of the first page size (later pages send only the newly loaded ids; a reload re-asks all loaded rows batch by batch), so `ids.max` must cover at least the page size; a failing call shows plain rows. */
119
+ export type ListRowDecoration = {
120
+ readonly query: string;
121
+ readonly params?: Readonly<Record<string, unknown>>;
122
+ };
123
+ export type ListColumnDisplay = "bar" | "badge" | "datetime" | "button";
82
124
  export type ListBadgeTone = "positive" | "warn" | "negative" | "neutral";
83
125
  export type ListPaginationMode = "pages" | "infinite" | false;
84
- export type ListSortDir = "asc" | "desc";
85
- export type ListSortSpec = {
86
- readonly field: string;
87
- readonly dir: ListSortDir;
88
- };
126
+ export type { ListSortDir, ListSortOption, ListSortSpec };
89
127
  export type ScreenFilterOp = "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
90
128
  export type ScreenFilter = {
91
129
  readonly field: string;
@@ -116,8 +154,11 @@ export type RowFieldExtractor = {
116
154
  export type RowActionDisplay = "button" | "link" | "icon" | "responsive";
117
155
  /** Row-action column layout. `"adaptive"` (default): primary action as link,
118
156
  * the rest in a kebab menu — except actions with an explicit `display`, which
119
- * stay inline. `"inline"`: every action inline, no kebab. */
120
- export type RowActionMode = "adaptive" | "inline";
157
+ * stay inline. `"inline"`: every action inline, no kebab. `"menu"`: every
158
+ * action in the kebab menu at the row end, none inline, the row opener
159
+ * (`rowClick: true`) included as an entry, so every row has a kebab; phone
160
+ * cards draw no kebab when the row opens, a card without a row target keeps it. */
161
+ export type RowActionMode = "adaptive" | "inline" | "menu";
121
162
  export type RowAction = RowActionWriteHandler | RowActionNavigate | RowActionDrawer | RowActionClientAction;
122
163
  export type RowActionWriteHandler = {
123
164
  /** Default for RowActions without an explicitly set `kind` —
@@ -330,6 +371,8 @@ export type ToolbarAction = ({
330
371
  /** Line under the drawer heading (i18n key). */
331
372
  readonly subtitle?: string;
332
373
  }) & {
374
+ /** Icon of the button, in every list (entityList, projectionList, relatedList). Without it the id-derived default applies (`create` → plus). */
375
+ readonly icon?: IconKey;
333
376
  /** Shown only while the query's `field` equals `eq` (live, empty payload). Hidden while the
334
377
  * query loads and when it fails. Named like the screen and panel gates because it is
335
378
  * query-based; a list toolbar has no record to evaluate a row condition against. */
@@ -372,6 +415,8 @@ export type EntityListFacetExtraOption = {
372
415
  readonly values: readonly string[];
373
416
  /** Where the chip sits relative to the field's own options. Default "start". */
374
417
  readonly position?: "start" | "end";
418
+ /** Draws a divider line below this choice in the select dropdown (e.g. after a "needs action" bundle). */
419
+ readonly separatorAfter?: boolean;
375
420
  };
376
421
  export type EntityListFacetConfig = {
377
422
  /** `"select"` (default): dropdown with checkboxes. `"chips"`: one toggle
@@ -386,6 +431,8 @@ export type EntityListFacetConfig = {
386
431
  * `showCounts`'s requests even when the number itself is not shown. */
387
432
  readonly hideEmpty?: boolean;
388
433
  readonly extraOptions?: readonly EntityListFacetExtraOption[];
434
+ /** "behind-filter": the facet opens behind the filter icon next to the search field instead of standing as a pill. Only below `sm`; otherwise it stays a pill. The remaining facets follow the usual phone rules and share the toggle panel. */
435
+ readonly narrowPlacement?: "behind-filter";
389
436
  };
390
437
  export type EntityListScreenDefinition = {
391
438
  readonly id: string;
@@ -414,8 +461,11 @@ export type EntityListScreenDefinition = {
414
461
  /** `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`. */
415
462
  readonly allowCreate?: boolean;
416
463
  readonly columns: readonly ListColumnSpec[];
464
+ /** Row field holding a SelectOptionTone; the row is toned like relatedList `rowTone`, other values leave it untoned. */
465
+ readonly rowToneField?: string;
466
+ /** Computed row fields from an app query, readable by `rowToneField`, column `hint`, `linkScreenField`/`linkParamsField` and rowAction `visible`/`params`/`payload`; a column may name a decoration field. */
467
+ readonly rowDecoration?: ListRowDecoration;
417
468
  readonly rowRenderer?: PlatformComponent;
418
- readonly cardRenderer?: PlatformComponent;
419
469
  /** Per-row actions, rendered as an actions column on the right.
420
470
  * Adaptive (default, see `rowActionMode`): the primary action as a link,
421
471
  * the rest in the kebab menu; actions with an explicit `display` stay
@@ -444,6 +494,8 @@ export type EntityListScreenDefinition = {
444
494
  /** Opt-in table behaviour: remembered state, reset, column picker, density. */
445
495
  readonly table?: ListTableOptions;
446
496
  readonly defaultSort?: ListSortSpec;
497
+ /** Choices of the "Sorted by" select in the toolbar (and the narrow-width sort control) instead of one entry per column; every field must be sortable. */
498
+ readonly sortOptions?: readonly ListSortOption[];
447
499
  /** Initial facet selection, per filterable field: option values (string
448
500
  * facets) or a boolean. Applies only while the URL carries no value for
449
501
  * that field, so a link or the user's own choice wins; picking "no filter"
@@ -459,10 +511,22 @@ export type EntityListScreenDefinition = {
459
511
  * `label` an i18n key. Selected through the `<screenId>.filter` URL key; a dashboard stat's
460
512
  * `navigate.namedFilter` links here. While one is active `defaultFilters` are not applied. */
461
513
  readonly namedFilters?: readonly NamedListFilterRef[];
514
+ /** Presentation of `namedFilters` in the toolbar; needs `namedFilters`. */
515
+ readonly namedFilterFacet?: NamedFilterFacetOptions;
462
516
  /** Replaces the default empty-list title/hint; the create button stays when the list can create. */
463
517
  readonly emptyState?: ListEmptyStateSpec;
464
518
  /** i18n key for the primary create button label (default `kumiko.actions.create`). */
465
519
  readonly createLabel?: string;
520
+ /** "toolbar": from `md` up (below md the compact header keeps its "+") the create button ends the filter row instead of standing in the header. Default "header". Needs a create button (no `allowCreate: false`). */
521
+ readonly createPlacement?: "header" | "toolbar";
522
+ /** From `sm` up, a "Sorted by" select in the filter row (same options as the narrow sort control); column headers stay sortable. */
523
+ readonly toolbarSort?: true;
524
+ /** "pills": below `sm` the select facets render as rounded pill triggers with a stronger border; from `sm` up unchanged. */
525
+ readonly narrowFacets?: ListNarrowFacets;
526
+ /** "toolbar": from `md` up the `toolbarActions` end the filter row, after the "Sorted by" select and the create button, instead of standing in the page header. Below `md` they stay in the header. Default "header". Needs `toolbarActions`. */
527
+ readonly toolbarActionsPlacement?: ListToolbarActionsPlacement;
528
+ /** i18n key of the plural noun for the list size in the phone sort line (`{count}` pluralised). Default `<feature>:entity:<entity>:noun`. */
529
+ readonly itemNounKey?: string;
466
530
  /** i18n key for the search field placeholder (default `kumiko.list.search-placeholder`). */
467
531
  readonly searchPlaceholder?: string;
468
532
  readonly searchable?: boolean;
@@ -1,9 +1,41 @@
1
1
  import type { ListAggregateMeasureKind } from "./list-aggregate-types.ts";
2
- export declare const LIST_TABLE_DENSITIES: readonly ["comfortable", "compact"];
2
+ export declare const LIST_TABLE_DENSITIES: readonly ["comfortable", "compact", "relaxed"];
3
3
  export type ListTableDensity = (typeof LIST_TABLE_DENSITIES)[number];
4
+ /** Upper bound for a list column width in pixels; the renderer clamps to it, the boot validator rejects above it. */
5
+ export declare const LIST_COLUMN_MAX_WIDTH_PX = 2000;
4
6
  export declare const LIST_COLUMN_PINS: readonly ["left", "right"];
5
7
  export type ListColumnPin = (typeof LIST_COLUMN_PINS)[number];
6
- export declare const LIST_TABLE_OPTION_KEYS: readonly ["rememberState", "resetAll", "columnPicker", "density", "pinning", "resizing", "keyboardNavigation", "export", "grouping", "totals", "compactHeader"];
8
+ export declare const LIST_TABLE_OPTION_KEYS: readonly ["rememberState", "resetAll", "columnPicker", "density", "pinning", "resizing", "keyboardNavigation", "export", "grouping", "totals", "summaryRow", "compactHeader", "frame"];
9
+ export declare const LIST_TABLE_FRAMES: readonly ["card"];
10
+ export type ListTableFrame = (typeof LIST_TABLE_FRAMES)[number];
11
+ export declare const LIST_TOOLBAR_ACTIONS_PLACEMENTS: readonly ["header", "toolbar"];
12
+ export type ListToolbarActionsPlacement = (typeof LIST_TOOLBAR_ACTIONS_PLACEMENTS)[number];
13
+ export declare const LIST_NARROW_FACETS: readonly ["pills"];
14
+ export type ListNarrowFacets = (typeof LIST_NARROW_FACETS)[number];
15
+ export declare const LIST_TABLE_SUMMARY_ROW_KEYS: readonly ["query", "params"];
16
+ export type ListPayloadCapabilities = {
17
+ readonly searchable: boolean;
18
+ readonly filters: boolean;
19
+ readonly namedFilters: boolean;
20
+ readonly screenFilter: boolean;
21
+ };
22
+ export declare function listPayloadKeysSent(capabilities: ListPayloadCapabilities): readonly string[];
23
+ type FlaggedFields = {
24
+ readonly fields: Readonly<Record<string, object>>;
25
+ } | undefined;
26
+ /** True when the entity has a `filterable` field; the facet dropdowns and the `filters` payload key rest on it. */
27
+ export declare function entityHasFilterableFields(entity: FlaggedFields): boolean;
28
+ /** An explicit `searchable` wins, otherwise a searchable field turns search on. The renderer additionally needs a search adapter. */
29
+ export declare function entityListDeclaresSearch(screen: {
30
+ readonly searchable?: boolean;
31
+ }, entity: FlaggedFields): boolean;
32
+ /** Whether an entityList can send `filters`: filterable fields, default filters or a range facet. */
33
+ export declare function entityListDeclaresFilters(screen: {
34
+ readonly defaultFilters?: object;
35
+ readonly facets?: Readonly<Record<string, false | {
36
+ readonly display?: string;
37
+ }>>;
38
+ }, entity: FlaggedFields): boolean;
7
39
  export declare const LIST_TABLE_GROUPING_KEYS: readonly ["field", "label", "labels", "collapsedWhen", "userChoice", "fields"];
8
40
  export declare const LIST_TABLE_TOTAL_KEYS: readonly ["field", "kind"];
9
41
  export declare const LIST_TABLE_EXPORT_KEYS: readonly ["xlsx", "maxRows"];
@@ -27,8 +59,10 @@ export type ListTableOptions = {
27
59
  readonly keyboardNavigation?: boolean;
28
60
  /** Renders the header row smaller (12px) and semibold. */
29
61
  readonly compactHeader?: boolean;
62
+ /** "card": from `sm` up the table, summary row and pager sit in one bordered, rounded card (toolbar and description stay above it); below `sm` the cards of the card layout are bordered, rounded cards with a gap. Default: the table runs frameless over the page width. */
63
+ readonly frame?: ListTableFrame;
30
64
  readonly density?: {
31
- /** Row density on first visit. Default "comfortable". */
65
+ /** Row density on first visit. Default "comfortable"; "relaxed" gives the body cells 12px vertical padding from sm, the header row stays as is. */
32
66
  readonly default?: ListTableDensity;
33
67
  /** Shows the density choice in the view menu and remembers it per session. */
34
68
  readonly userChoice?: boolean;
@@ -39,6 +73,17 @@ export type ListTableOptions = {
39
73
  readonly grouping?: ListTableGrouping;
40
74
  /** Footer row under the rows, computed by the server over all rows matching the current filters and search. In grouped mode the same measures show in each group header. */
41
75
  readonly totals?: readonly ListTableTotal[];
76
+ /** One line under the rows (above `totals`), computed by a query of your own: "label value · label value"; an item without `label` shows its value alone. */
77
+ readonly summaryRow?: ListTableSummaryRow;
78
+ };
79
+ /** The query receives the list's current search, filters and named filter (no paging, no sort) plus `params`, and answers `{ items: [{ label?, value, tone?, column? }] }`.
80
+ * When an item names a `column` (a column field of the list), the table draws the row as a table row: the values stand in the cells under their columns, without labels; items without `column` go to the first column.
81
+ * Without any `column` the row stays one line "label value · label value". */
82
+ export type ListTableSummaryRow = {
83
+ /** Qualified query name, e.g. `feature:query:order:summary`. Its Zod schema accepts every list key the screen sends (search, filters, namedFilter, ...). */
84
+ readonly query: string;
85
+ /** Extra payload keys merged over the list's own. */
86
+ readonly params?: Readonly<Record<string, unknown>>;
42
87
  };
43
88
  export type ListTableGrouping = {
44
89
  /** Start grouping. Without it the list starts ungrouped and the user picks from `fields`. */
@@ -70,3 +115,4 @@ export type BundledListsOptions = {
70
115
  /** Pins the first column left on every bundled list where no column is pinned. Needs `table.pinning: true`. */
71
116
  readonly pinFirstColumn?: boolean;
72
117
  };
118
+ export {};
@@ -1,4 +1,6 @@
1
- export const LIST_TABLE_DENSITIES = ["comfortable", "compact"];
1
+ export const LIST_TABLE_DENSITIES = ["comfortable", "compact", "relaxed"];
2
+ /** Upper bound for a list column width in pixels; the renderer clamps to it, the boot validator rejects above it. */
3
+ export const LIST_COLUMN_MAX_WIDTH_PX = 2000;
2
4
  export const LIST_COLUMN_PINS = ["left", "right"];
3
5
  export const LIST_TABLE_OPTION_KEYS = [
4
6
  "rememberState",
@@ -11,8 +13,40 @@ export const LIST_TABLE_OPTION_KEYS = [
11
13
  "export",
12
14
  "grouping",
13
15
  "totals",
16
+ "summaryRow",
14
17
  "compactHeader",
18
+ "frame",
15
19
  ];
20
+ export const LIST_TABLE_FRAMES = ["card"];
21
+ export const LIST_TOOLBAR_ACTIONS_PLACEMENTS = ["header", "toolbar"];
22
+ export const LIST_NARROW_FACETS = ["pills"];
23
+ export const LIST_TABLE_SUMMARY_ROW_KEYS = ["query", "params"];
24
+ // The optional list-query keys a list screen sends for what it declares, in one place so the boot
25
+ // validator (summary-row schema check) and the renderer's payload tests agree on them.
26
+ export function listPayloadKeysSent(capabilities) {
27
+ return [
28
+ ...(capabilities.searchable ? ["search"] : []),
29
+ ...(capabilities.filters ? ["filters"] : []),
30
+ ...(capabilities.namedFilters ? ["namedFilter"] : []),
31
+ ...(capabilities.screenFilter ? ["filter"] : []),
32
+ ];
33
+ }
34
+ function someFieldFlagged(entity, flag) {
35
+ return Object.values(entity?.fields ?? {}).some((field) => Reflect.get(field, flag) === true);
36
+ }
37
+ /** True when the entity has a `filterable` field; the facet dropdowns and the `filters` payload key rest on it. */
38
+ export function entityHasFilterableFields(entity) {
39
+ return someFieldFlagged(entity, "filterable");
40
+ }
41
+ /** An explicit `searchable` wins, otherwise a searchable field turns search on. The renderer additionally needs a search adapter. */
42
+ export function entityListDeclaresSearch(screen, entity) {
43
+ return screen.searchable ?? someFieldFlagged(entity, "searchable");
44
+ }
45
+ /** Whether an entityList can send `filters`: filterable fields, default filters or a range facet. */
46
+ export function entityListDeclaresFilters(screen, entity) {
47
+ const hasRangeFacet = Object.values(screen.facets ?? {}).some((facet) => facet !== false && facet.display === "range");
48
+ return entityHasFilterableFields(entity) || screen.defaultFilters !== undefined || hasRangeFacet;
49
+ }
16
50
  export const LIST_TABLE_GROUPING_KEYS = [
17
51
  "field",
18
52
  "label",