@cosmicdrift/kumiko-types 0.413.1 → 0.414.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.
@@ -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 } from "./screen-list-table.ts";
13
13
  export type ListColumnSpec = string | {
14
14
  readonly field: string;
15
15
  readonly renderer?: FieldRenderer;
@@ -45,47 +45,81 @@ 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;
57
62
  /** Start state for the column picker: the column begins hidden and the
58
63
  * user can show it. Only valid together with `table.columnPicker`. */
59
64
  readonly hidden?: boolean;
60
65
  /** Start pin for the column: it sticks to this edge while the table scrolls
61
66
  * horizontally. Only valid together with `table.pinning`. */
62
67
  readonly pinned?: ListColumnPin;
63
- /** Start width in px. Only valid together with `table.resizing`. */
68
+ /** Start width in px. Needs `table.resizing`, except on a hint column, which sizes itself. */
64
69
  readonly width?: number;
65
- /** Smallest width in px the user can resize to. Only valid together with `table.resizing`. */
70
+ /** Smallest width in px the user can resize to. Needs `table.resizing`, except on a hint column. */
66
71
  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. */
72
+ /** One of "bar" | "badge" | "datetime" | "button". "bar": field holds a 0..1 fraction,
73
+ * drawn as bar + percent (dashboard list panels only). "badge": field holds a label,
74
+ * drawn as a pill whose tone comes from `badgeToneField` (dashboard list panels and the
75
+ * RenderList tables of entityList and projectionList). "datetime": field holds an ISO
76
+ * string or epoch ms, shown in the user's locale and time zone (dashboard list panels
77
+ * only). "button": a column with a screen link (linkScreenField or refEntity) draws the
78
+ * link as a small outline button, in dashboard list panels only; a row button in an
79
+ * entityList or projectionList table is a rowAction with display "button" (boot-rejected
80
+ * on columns there). Other list renderers ignore `display`. */
72
81
  readonly display?: ListColumnDisplay;
73
82
  /** Row field naming the badge tone; only with `display: "badge"`. */
74
83
  readonly badgeToneField?: string;
84
+ /** `false` draws the badge pill without its leading dot; only with `display: "badge"`. */
85
+ readonly badgeDot?: false;
75
86
  /** Row field holding a screen QN (`<feature>:screen:<id>`): the cell links
76
87
  * to that screen when the row sets it and the user may open it. For
77
88
  * query-backed lists (projectionList, relatedList), whose rows can point
78
89
  * at a different screen each. */
79
90
  readonly linkScreenField?: string;
91
+ /** 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. */
92
+ readonly linkParamsField?: string;
93
+ /** "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`. */
94
+ readonly hint?: ListColumnHint;
80
95
  };
81
- export type ListColumnDisplay = "bar" | "badge" | "datetime";
96
+ export type ListColumnHint = {
97
+ /** Row field with a second, muted line (e.g. the due date as text). */
98
+ readonly detailField?: string;
99
+ /** Row field shown as the detail line below `sm` instead of `detailField` (a shorter line, e.g. "5 days · Anna Schneider"). Needs `detailField`. */
100
+ readonly narrowDetailField?: string;
101
+ /** 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. */
102
+ readonly toneField?: string;
103
+ /** Row field with a longer reason, shown behind a "Why?" toggle in the cell. */
104
+ readonly reasonField?: string;
105
+ /** 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`. */
106
+ readonly reasonOpenField?: string;
107
+ /** Row field with a bold lead-in of the reason paragraph ("Rule 10 · Case open."); without it the reason has none. Needs `reasonField`. */
108
+ readonly reasonTitleField?: string;
109
+ /** i18n key for an empty sentence; default `kumiko.list.hint.empty`. */
110
+ readonly emptyLabel?: string;
111
+ /** The sentence keeps the normal text color (still bold); the tone stays on the row stripe, a badge and the detail line. */
112
+ readonly plainSentence?: true;
113
+ };
114
+ /** Computed fields for the loaded rows: one `query` call per page answer with `{ ...params, ids }`, answering `{ rows: [{ id, ...fields }] }`. Fields are merged into the rows and win over same-named row fields. */
115
+ export type ListRowDecoration = {
116
+ readonly query: string;
117
+ readonly params?: Readonly<Record<string, unknown>>;
118
+ };
119
+ export type ListColumnDisplay = "bar" | "badge" | "datetime" | "button";
82
120
  export type ListBadgeTone = "positive" | "warn" | "negative" | "neutral";
83
121
  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
- };
122
+ export type { ListSortDir, ListSortOption, ListSortSpec };
89
123
  export type ScreenFilterOp = "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
90
124
  export type ScreenFilter = {
91
125
  readonly field: string;
@@ -116,8 +150,11 @@ export type RowFieldExtractor = {
116
150
  export type RowActionDisplay = "button" | "link" | "icon" | "responsive";
117
151
  /** Row-action column layout. `"adaptive"` (default): primary action as link,
118
152
  * 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";
153
+ * stay inline. `"inline"`: every action inline, no kebab. `"menu"`: every
154
+ * action in the kebab menu at the row end, none inline, the row opener
155
+ * (`rowClick: true`) included as an entry, so every row has a kebab; phone
156
+ * cards draw no kebab. */
157
+ export type RowActionMode = "adaptive" | "inline" | "menu";
121
158
  export type RowAction = RowActionWriteHandler | RowActionNavigate | RowActionDrawer | RowActionClientAction;
122
159
  export type RowActionWriteHandler = {
123
160
  /** Default for RowActions without an explicitly set `kind` —
@@ -330,6 +367,8 @@ export type ToolbarAction = ({
330
367
  /** Line under the drawer heading (i18n key). */
331
368
  readonly subtitle?: string;
332
369
  }) & {
370
+ /** Icon of the button, in every list (entityList, projectionList, relatedList). Without it the id-derived default applies (`create` → plus). */
371
+ readonly icon?: IconKey;
333
372
  /** Shown only while the query's `field` equals `eq` (live, empty payload). Hidden while the
334
373
  * query loads and when it fails. Named like the screen and panel gates because it is
335
374
  * query-based; a list toolbar has no record to evaluate a row condition against. */
@@ -414,8 +453,11 @@ export type EntityListScreenDefinition = {
414
453
  /** `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
454
  readonly allowCreate?: boolean;
416
455
  readonly columns: readonly ListColumnSpec[];
456
+ /** Row field holding a SelectOptionTone; the row is toned like relatedList `rowTone`, other values leave it untoned. */
457
+ readonly rowToneField?: string;
458
+ /** 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. */
459
+ readonly rowDecoration?: ListRowDecoration;
417
460
  readonly rowRenderer?: PlatformComponent;
418
- readonly cardRenderer?: PlatformComponent;
419
461
  /** Per-row actions, rendered as an actions column on the right.
420
462
  * Adaptive (default, see `rowActionMode`): the primary action as a link,
421
463
  * the rest in the kebab menu; actions with an explicit `display` stay
@@ -444,6 +486,8 @@ export type EntityListScreenDefinition = {
444
486
  /** Opt-in table behaviour: remembered state, reset, column picker, density. */
445
487
  readonly table?: ListTableOptions;
446
488
  readonly defaultSort?: ListSortSpec;
489
+ /** 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. */
490
+ readonly sortOptions?: readonly ListSortOption[];
447
491
  /** Initial facet selection, per filterable field: option values (string
448
492
  * facets) or a boolean. Applies only while the URL carries no value for
449
493
  * that field, so a link or the user's own choice wins; picking "no filter"
@@ -459,10 +503,22 @@ export type EntityListScreenDefinition = {
459
503
  * `label` an i18n key. Selected through the `<screenId>.filter` URL key; a dashboard stat's
460
504
  * `navigate.namedFilter` links here. While one is active `defaultFilters` are not applied. */
461
505
  readonly namedFilters?: readonly NamedListFilterRef[];
506
+ /** Presentation of `namedFilters` in the toolbar; needs `namedFilters`. */
507
+ readonly namedFilterFacet?: NamedFilterFacetOptions;
462
508
  /** Replaces the default empty-list title/hint; the create button stays when the list can create. */
463
509
  readonly emptyState?: ListEmptyStateSpec;
464
510
  /** i18n key for the primary create button label (default `kumiko.actions.create`). */
465
511
  readonly createLabel?: string;
512
+ /** "toolbar": from `sm` up the create button ends the filter row instead of standing in the header. Default "header". Needs a create button (no `allowCreate: false`). */
513
+ readonly createPlacement?: "header" | "toolbar";
514
+ /** From `sm` up, a "Sorted by" select in the filter row (same options as the narrow sort control); column headers stay sortable. */
515
+ readonly toolbarSort?: true;
516
+ /** "pills": below `sm` the select facets render as rounded pill triggers with a stronger border; from `sm` up unchanged. */
517
+ readonly narrowFacets?: ListNarrowFacets;
518
+ /** "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`. */
519
+ readonly toolbarActionsPlacement?: "header" | "toolbar";
520
+ /** i18n key of the plural noun for the list size in the phone sort line (`{count}` pluralised). Default `<feature>:entity:<entity>:noun`. */
521
+ readonly itemNounKey?: string;
466
522
  /** i18n key for the search field placeholder (default `kumiko.list.search-placeholder`). */
467
523
  readonly searchPlaceholder?: string;
468
524
  readonly searchable?: boolean;
@@ -1,9 +1,16 @@
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_NARROW_FACETS: readonly ["pills"];
12
+ export type ListNarrowFacets = (typeof LIST_NARROW_FACETS)[number];
13
+ export declare const LIST_TABLE_SUMMARY_ROW_KEYS: readonly ["query", "params"];
7
14
  export declare const LIST_TABLE_GROUPING_KEYS: readonly ["field", "label", "labels", "collapsedWhen", "userChoice", "fields"];
8
15
  export declare const LIST_TABLE_TOTAL_KEYS: readonly ["field", "kind"];
9
16
  export declare const LIST_TABLE_EXPORT_KEYS: readonly ["xlsx", "maxRows"];
@@ -27,8 +34,10 @@ export type ListTableOptions = {
27
34
  readonly keyboardNavigation?: boolean;
28
35
  /** Renders the header row smaller (12px) and semibold. */
29
36
  readonly compactHeader?: boolean;
37
+ /** "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. */
38
+ readonly frame?: ListTableFrame;
30
39
  readonly density?: {
31
- /** Row density on first visit. Default "comfortable". */
40
+ /** Row density on first visit. Default "comfortable"; "relaxed" gives the body cells 12px vertical padding from sm, the header row stays as is. */
32
41
  readonly default?: ListTableDensity;
33
42
  /** Shows the density choice in the view menu and remembers it per session. */
34
43
  readonly userChoice?: boolean;
@@ -39,6 +48,17 @@ export type ListTableOptions = {
39
48
  readonly grouping?: ListTableGrouping;
40
49
  /** 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
50
  readonly totals?: readonly ListTableTotal[];
51
+ /** 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. */
52
+ readonly summaryRow?: ListTableSummaryRow;
53
+ };
54
+ /** 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? }] }`.
55
+ * 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.
56
+ * Without any `column` the row stays one line "label value · label value". */
57
+ export type ListTableSummaryRow = {
58
+ /** Qualified query name, e.g. `feature:query:order:summary`. Its Zod schema accepts every list key the screen sends (search, filters, namedFilter, ...). */
59
+ readonly query: string;
60
+ /** Extra payload keys merged over the list's own. */
61
+ readonly params?: Readonly<Record<string, unknown>>;
42
62
  };
43
63
  export type ListTableGrouping = {
44
64
  /** Start grouping. Without it the list starts ungrouped and the user picks from `fields`. */
@@ -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,13 @@ 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_NARROW_FACETS = ["pills"];
22
+ export const LIST_TABLE_SUMMARY_ROW_KEYS = ["query", "params"];
16
23
  export const LIST_TABLE_GROUPING_KEYS = [
17
24
  "field",
18
25
  "label",