@cosmicdrift/kumiko-renderer 0.387.0 → 0.389.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 (210) hide show
  1. package/dist/app/action-form-body.js +10 -2
  2. package/dist/app/action-form-support.d.ts +1 -0
  3. package/dist/app/action-form-support.js +1 -1
  4. package/dist/app/base-path-context.d.ts +9 -0
  5. package/dist/app/base-path-context.js +13 -0
  6. package/dist/app/base-path.d.ts +3 -0
  7. package/dist/app/base-path.js +27 -0
  8. package/dist/app/config-edit-body.d.ts +1 -1
  9. package/dist/app/config-edit-body.js +19 -6
  10. package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
  11. package/dist/app/config-edit-without-secret-fields.js +28 -0
  12. package/dist/app/entity-edit-prefill.d.ts +11 -0
  13. package/dist/app/entity-edit-prefill.js +9 -4
  14. package/dist/app/entity-list-facet-specs.d.ts +3 -0
  15. package/dist/app/entity-list-facet-specs.js +65 -0
  16. package/dist/app/entity-list-screen.js +74 -206
  17. package/dist/app/extension-sections.d.ts +5 -3
  18. package/dist/app/form-schema.js +1 -1
  19. package/dist/app/header-actions-bar.js +1 -1
  20. package/dist/app/kumiko-screen.js +6 -14
  21. package/dist/app/list-bulk-actions.d.ts +18 -0
  22. package/dist/app/list-bulk-actions.js +54 -0
  23. package/dist/app/locked-screen-header.d.ts +9 -0
  24. package/dist/app/locked-screen-header.js +33 -0
  25. package/dist/app/projection-list-body.js +51 -46
  26. package/dist/app/row-actions.d.ts +3 -0
  27. package/dist/app/row-actions.js +10 -5
  28. package/dist/app/screen-shared.js +9 -2
  29. package/dist/app/use-entity-range-facets.d.ts +24 -0
  30. package/dist/app/use-entity-range-facets.js +64 -0
  31. package/dist/app/use-projection-date-range.d.ts +12 -0
  32. package/dist/app/use-projection-date-range.js +21 -0
  33. package/dist/app/use-screen-list-rows.d.ts +37 -0
  34. package/dist/app/use-screen-list-rows.js +104 -0
  35. package/dist/app/use-screen-list-table.d.ts +24 -0
  36. package/dist/app/use-screen-list-table.js +46 -0
  37. package/dist/components/edit-mounts.d.ts +2 -2
  38. package/dist/components/embedded-list-field.js +1 -1
  39. package/dist/components/list-group-page-bridges.d.ts +3 -0
  40. package/dist/components/list-group-page-bridges.js +31 -0
  41. package/dist/components/list-state-store.d.ts +16 -0
  42. package/dist/components/list-state-store.js +31 -0
  43. package/dist/components/related-list-section.js +51 -90
  44. package/dist/components/render-edit-action-button.d.ts +1 -3
  45. package/dist/components/render-edit-action-button.js +2 -2
  46. package/dist/components/render-edit.js +11 -8
  47. package/dist/components/render-field.js +11 -7
  48. package/dist/components/render-list-toolbar-action.d.ts +27 -0
  49. package/dist/components/render-list-toolbar-action.js +40 -0
  50. package/dist/components/render-list.d.ts +13 -29
  51. package/dist/components/render-list.js +72 -67
  52. package/dist/current-user.d.ts +2 -0
  53. package/dist/data-table-aggregate-types.d.ts +47 -0
  54. package/dist/data-table-aggregate-types.js +1 -0
  55. package/dist/data-table-bulk-types.d.ts +27 -0
  56. package/dist/data-table-bulk-types.js +1 -0
  57. package/dist/data-table-facet-types.d.ts +36 -0
  58. package/dist/data-table-facet-types.js +1 -0
  59. package/dist/data-table-view-types.d.ts +73 -0
  60. package/dist/data-table-view-types.js +1 -0
  61. package/dist/flow-diagram/render.js +9 -1
  62. package/dist/headless/form/form-controller.js +8 -0
  63. package/dist/headless/form/types.d.ts +1 -0
  64. package/dist/headless/format/index.js +89 -64
  65. package/dist/headless/view-model/edit.js +5 -1
  66. package/dist/headless/view-model/list.js +184 -149
  67. package/dist/headless/view-model/types.d.ts +7 -2
  68. package/dist/hooks/list-column-layout.d.ts +19 -0
  69. package/dist/hooks/list-column-layout.js +36 -0
  70. package/dist/hooks/list-group-pages.d.ts +26 -0
  71. package/dist/hooks/list-group-pages.js +37 -0
  72. package/dist/hooks/list-reset.d.ts +8 -0
  73. package/dist/hooks/list-reset.js +15 -0
  74. package/dist/hooks/list-server-groups.d.ts +39 -0
  75. package/dist/hooks/list-server-groups.js +84 -0
  76. package/dist/hooks/list-state-schema.d.ts +28 -0
  77. package/dist/hooks/list-state-schema.js +76 -0
  78. package/dist/hooks/live-refetch-scheduler.js +4 -0
  79. package/dist/hooks/use-bulk-run.d.ts +41 -0
  80. package/dist/hooks/use-bulk-run.js +65 -0
  81. package/dist/hooks/use-group-load-state.d.ts +11 -0
  82. package/dist/hooks/use-group-load-state.js +31 -0
  83. package/dist/hooks/use-list-aggregates.d.ts +38 -0
  84. package/dist/hooks/use-list-aggregates.js +107 -0
  85. package/dist/hooks/use-list-table.d.ts +38 -0
  86. package/dist/hooks/use-list-table.js +91 -0
  87. package/dist/hooks/use-list-view-state.d.ts +43 -0
  88. package/dist/hooks/use-list-view-state.js +161 -0
  89. package/dist/hooks/use-reference-lookup.d.ts +9 -1
  90. package/dist/hooks/use-reference-lookup.js +17 -3
  91. package/dist/hooks/use-related-list-rows.d.ts +27 -0
  92. package/dist/hooks/use-related-list-rows.js +81 -0
  93. package/dist/hooks/use-related-list-state.d.ts +15 -0
  94. package/dist/hooks/use-related-list-state.js +31 -0
  95. package/dist/hooks/use-remembered-list-params.d.ts +13 -0
  96. package/dist/hooks/use-remembered-list-params.js +53 -0
  97. package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
  98. package/dist/hooks/use-remembered-local-list-state.js +63 -0
  99. package/dist/i18n-defaults.js +59 -2
  100. package/dist/index.d.ts +4 -1
  101. package/dist/index.js +3 -0
  102. package/dist/primitives.d.ts +30 -47
  103. package/dist/web/app/create-app.d.ts +5 -0
  104. package/dist/web/app/create-app.js +15 -7
  105. package/dist/web/app/dashboard-body.js +17 -1
  106. package/dist/web/app/dashboard-panels.js +12 -2
  107. package/dist/web/app/history-leave-guard.d.ts +6 -0
  108. package/dist/web/app/history-leave-guard.js +115 -0
  109. package/dist/web/app/list-state-store-boot.d.ts +6 -0
  110. package/dist/web/app/list-state-store-boot.js +15 -0
  111. package/dist/web/app/nav.js +7 -54
  112. package/dist/web/app/session-list-state-store.d.ts +15 -0
  113. package/dist/web/app/session-list-state-store.js +117 -0
  114. package/dist/web/index.d.ts +2 -1
  115. package/dist/web/index.js +2 -1
  116. package/dist/web/layout/shell-header.js +2 -1
  117. package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
  118. package/dist/web/layout/uniform-bar-controls.js +4 -0
  119. package/dist/web/lib/export-xlsx-request.d.ts +11 -0
  120. package/dist/web/lib/export-xlsx-request.js +46 -0
  121. package/dist/web/lib/file-ref-download.js +4 -1
  122. package/dist/web/primitives/action-menu.js +1 -1
  123. package/dist/web/primitives/code-input.d.ts +6 -0
  124. package/dist/web/primitives/code-input.js +31 -0
  125. package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
  126. package/dist/web/primitives/data-table/actions-column.js +34 -0
  127. package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
  128. package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
  129. package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
  130. package/dist/web/primitives/data-table/card-sort-select.js +40 -0
  131. package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
  132. package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
  133. package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
  134. package/dist/web/primitives/data-table/column-picker-section.js +35 -0
  135. package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
  136. package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
  137. package/dist/web/primitives/data-table/density.d.ts +14 -0
  138. package/dist/web/primitives/data-table/density.js +20 -0
  139. package/dist/web/primitives/data-table/export-button.d.ts +5 -0
  140. package/dist/web/primitives/data-table/export-button.js +37 -0
  141. package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
  142. package/dist/web/primitives/data-table/facet-cluster.js +37 -0
  143. package/dist/web/primitives/data-table/group-band.d.ts +28 -0
  144. package/dist/web/primitives/data-table/group-band.js +53 -0
  145. package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
  146. package/dist/web/primitives/data-table/measure-value.js +28 -0
  147. package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
  148. package/dist/web/primitives/data-table/pin-section.js +20 -0
  149. package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
  150. package/dist/web/primitives/data-table/reset-all-button.js +8 -0
  151. package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
  152. package/dist/web/primitives/data-table/server-group-header.js +51 -0
  153. package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
  154. package/dist/web/primitives/data-table/sortable-header.js +41 -0
  155. package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
  156. package/dist/web/primitives/data-table/table-chrome.js +30 -0
  157. package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
  158. package/dist/web/primitives/data-table/table-footer.js +20 -0
  159. package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
  160. package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
  161. package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
  162. package/dist/web/primitives/data-table/totals-footer.js +48 -0
  163. package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
  164. package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
  165. package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
  166. package/dist/web/primitives/data-table/use-column-pins.js +117 -0
  167. package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
  168. package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
  169. package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
  170. package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
  171. package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
  172. package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
  173. package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
  174. package/dist/web/primitives/data-table/use-row-selection.js +48 -0
  175. package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
  176. package/dist/web/primitives/data-table/use-view-columns.js +17 -0
  177. package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
  178. package/dist/web/primitives/data-table/view-menu.js +45 -0
  179. package/dist/web/primitives/data-table-cell.d.ts +0 -22
  180. package/dist/web/primitives/data-table-cell.js +1 -58
  181. package/dist/web/primitives/data-table.d.ts +1 -1
  182. package/dist/web/primitives/data-table.js +125 -146
  183. package/dist/web/primitives/facet-filter.js +1 -1
  184. package/dist/web/primitives/file-thumbnail.d.ts +3 -0
  185. package/dist/web/primitives/file-thumbnail.js +5 -0
  186. package/dist/web/primitives/file-value-list.d.ts +1 -1
  187. package/dist/web/primitives/file-value-list.js +5 -5
  188. package/dist/web/primitives/input.js +6 -1
  189. package/dist/web/primitives/layout-elements.js +6 -4
  190. package/dist/web/primitives/number-range-facet.d.ts +7 -0
  191. package/dist/web/primitives/number-range-facet.js +33 -0
  192. package/dist/web/primitives/page-header.js +3 -1
  193. package/dist/web/primitives/row-actions.js +3 -3
  194. package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
  195. package/dist/web/primitives/use-focus-on-mount.js +9 -0
  196. package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
  197. package/dist/web/widgets/bounded-load-cache.js +49 -21
  198. package/dist/web/widgets/choice-cards.d.ts +17 -0
  199. package/dist/web/widgets/choice-cards.js +12 -0
  200. package/dist/web/widgets/collapsible-section.js +1 -1
  201. package/dist/web/widgets/detail-list.d.ts +5 -1
  202. package/dist/web/widgets/detail-list.js +5 -1
  203. package/dist/web/widgets/index.d.ts +1 -0
  204. package/dist/web/widgets/index.js +1 -0
  205. package/dist/web/widgets/pdf-preview.js +10 -5
  206. package/dist/web/widgets/plan-card.d.ts +2 -0
  207. package/dist/web/widgets/plan-card.js +1 -1
  208. package/dist/web/widgets/upload-zone.js +13 -5
  209. package/package.json +5 -5
  210. package/src/changes.json +158 -0
@@ -184,73 +184,98 @@ function unitDisplaySpec(spec) {
184
184
  const v = spec["unitDisplay"];
185
185
  return isOneOf(INTL_UNIT_DISPLAYS, v) ? v : "short";
186
186
  }
187
+ function isEmptyCellValue(value) {
188
+ return value === null || value === undefined || value === "";
189
+ }
187
190
  export function applyFormatSpec(spec, value, translate) {
188
- const isEmpty = value === null || value === undefined || value === "";
189
191
  // priority renders its emptyLabel for empty values — every other format
190
192
  // collapses empty to "".
191
- if (isEmpty && spec.format !== "priority")
193
+ if (isEmptyCellValue(value) && spec.format !== "priority")
192
194
  return "";
193
- switch (spec.format) {
194
- case "timestamp":
195
- case "date":
196
- return formatDateCell(value, spec.format, {
197
- locale: stringSpec(spec, "locale"),
198
- dateStyle: dateStyleSpec(spec, "dateStyle"),
199
- timeStyle: spec.format === "timestamp" ? dateStyleSpec(spec, "timeStyle") : undefined,
200
- timeZone: spec.format === "timestamp" ? stringSpec(spec, "timeZone") : undefined,
201
- });
202
- case "boolean": {
203
- if (value === true) {
204
- const trueLabel = stringSpec(spec, "trueLabel");
205
- return trueLabel === undefined ? "✓" : (translate?.(trueLabel) ?? trueLabel);
206
- }
207
- if (value === false) {
208
- const falseLabel = stringSpec(spec, "falseLabel");
209
- return falseLabel === undefined ? "" : (translate?.(falseLabel) ?? falseLabel);
210
- }
211
- return "";
212
- }
213
- case "currency": {
214
- const sym = stringSpec(spec, "symbol") ?? "";
215
- return sym.length > 0 ? `${value} ${sym}` : String(value);
216
- }
217
- case "number":
218
- case "decimal":
219
- case "bigInt":
220
- return formatNumberCell(value, stringSpec(spec, "locale"), booleanSpec(spec, "grouping") ?? true, spec.format === "bigInt" ? {} : fractionDigitsSpec(spec));
221
- case "unit":
222
- return formatUnitCell(value, stringSpec(spec, "unit"), stringSpec(spec, "locale"), unitDisplaySpec(spec));
223
- case "priority": {
224
- const emptyLabel = stringSpec(spec, "emptyLabel") ?? EMPTY_VALUE_PLACEHOLDER;
225
- const prefix = stringSpec(spec, "prefix") ?? "";
226
- if (isEmpty || value === 0)
227
- return emptyLabel;
228
- return `${prefix}${value}`;
229
- }
230
- case "enumOption": {
231
- const keyPrefix = stringSpec(spec, "keyPrefix");
232
- const labelFor = (item) => {
233
- const raw = typeof item === "string" ? item : String(item);
234
- if (keyPrefix === undefined || translate === undefined)
235
- return raw;
236
- const key = `${keyPrefix}${raw}`;
237
- const translated = translate(key);
238
- return translated === key ? raw : translated;
239
- };
240
- // multiSelect-style columns hold an array of option values
241
- return Array.isArray(value) ? value.map(labelFor).join(", ") : labelFor(value);
242
- }
243
- // Thumbnails and file links are drawn by the web list cell; this is the
244
- // text fallback for every other surface.
245
- case "image":
246
- case "file":
247
- return Array.isArray(value) ? value.join(", ") : String(value);
248
- case "json":
249
- return stringifyJsonOrString(value, typeof spec["indent"] === "number" ? spec["indent"] : 2);
250
- default:
251
- if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
252
- rendererWarn(`[kumiko] applyFormatSpec: unknown format key "${spec.format}" — registered via FieldFormatRegistry module augmentation?`);
253
- }
254
- return typeof value === "string" ? value : String(value);
195
+ if (!isBuiltInCellFormat(spec.format))
196
+ return formatUnknownCellFormat(spec, value);
197
+ return CELL_FORMATTERS[spec.format](spec, value, translate);
198
+ }
199
+ function formatTemporalCell(spec, value) {
200
+ const isTimestamp = spec.format === "timestamp";
201
+ return formatDateCell(value, spec.format, {
202
+ locale: stringSpec(spec, "locale"),
203
+ dateStyle: dateStyleSpec(spec, "dateStyle"),
204
+ timeStyle: isTimestamp ? dateStyleSpec(spec, "timeStyle") : undefined,
205
+ timeZone: isTimestamp ? stringSpec(spec, "timeZone") : undefined,
206
+ });
207
+ }
208
+ function formatBooleanCell(spec, value, translate) {
209
+ if (value === true) {
210
+ const trueLabel = stringSpec(spec, "trueLabel");
211
+ return trueLabel === undefined ? "✓" : (translate?.(trueLabel) ?? trueLabel);
212
+ }
213
+ if (value === false) {
214
+ const falseLabel = stringSpec(spec, "falseLabel");
215
+ return falseLabel === undefined ? "" : (translate?.(falseLabel) ?? falseLabel);
216
+ }
217
+ return "";
218
+ }
219
+ function formatCurrencyCell(spec, value) {
220
+ const sym = stringSpec(spec, "symbol") ?? "";
221
+ return sym.length > 0 ? `${value} ${sym}` : String(value);
222
+ }
223
+ function formatNumericCell(spec, value) {
224
+ return formatNumberCell(value, stringSpec(spec, "locale"), booleanSpec(spec, "grouping") ?? true, spec.format === "bigInt" ? {} : fractionDigitsSpec(spec));
225
+ }
226
+ function formatUnitSpecCell(spec, value) {
227
+ return formatUnitCell(value, stringSpec(spec, "unit"), stringSpec(spec, "locale"), unitDisplaySpec(spec));
228
+ }
229
+ function formatPriorityCell(spec, value) {
230
+ const emptyLabel = stringSpec(spec, "emptyLabel") ?? EMPTY_VALUE_PLACEHOLDER;
231
+ const prefix = stringSpec(spec, "prefix") ?? "";
232
+ if (isEmptyCellValue(value) || value === 0)
233
+ return emptyLabel;
234
+ return `${prefix}${value}`;
235
+ }
236
+ function formatEnumOptionCell(spec, value, translate) {
237
+ const keyPrefix = stringSpec(spec, "keyPrefix");
238
+ const labelFor = (item) => {
239
+ const raw = typeof item === "string" ? item : String(item);
240
+ if (keyPrefix === undefined || translate === undefined)
241
+ return raw;
242
+ const key = `${keyPrefix}${raw}`;
243
+ const translated = translate(key);
244
+ return translated === key ? raw : translated;
245
+ };
246
+ // multiSelect-style columns hold an array of option values
247
+ return Array.isArray(value) ? value.map(labelFor).join(", ") : labelFor(value);
248
+ }
249
+ // Thumbnails and file links are drawn by the web list cell; this is the
250
+ // text fallback for every other surface.
251
+ function formatFileCell(_spec, value) {
252
+ return Array.isArray(value) ? value.join(", ") : String(value);
253
+ }
254
+ function formatJsonCell(spec, value) {
255
+ return stringifyJsonOrString(value, typeof spec["indent"] === "number" ? spec["indent"] : 2);
256
+ }
257
+ const CELL_FORMATTERS = {
258
+ timestamp: formatTemporalCell,
259
+ date: formatTemporalCell,
260
+ boolean: formatBooleanCell,
261
+ currency: formatCurrencyCell,
262
+ number: formatNumericCell,
263
+ decimal: formatNumericCell,
264
+ bigInt: formatNumericCell,
265
+ unit: formatUnitSpecCell,
266
+ priority: formatPriorityCell,
267
+ enumOption: formatEnumOptionCell,
268
+ image: formatFileCell,
269
+ file: formatFileCell,
270
+ json: formatJsonCell,
271
+ };
272
+ // Own-key check: a format such as "toString" must not resolve to an inherited member.
273
+ function isBuiltInCellFormat(format) {
274
+ return Object.hasOwn(CELL_FORMATTERS, format);
275
+ }
276
+ function formatUnknownCellFormat(spec, value) {
277
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
278
+ rendererWarn(`[kumiko] applyFormatSpec: unknown format key "${spec.format}" — registered via FieldFormatRegistry module augmentation?`);
255
279
  }
280
+ return typeof value === "string" ? value : String(value);
256
281
  }
@@ -56,6 +56,7 @@ function wasEmptiedByWriteExclusion(declaredFieldSpecs, remainingFields) {
56
56
  export function computeRelatedListSectionViewModel(sectionSpec, translate) {
57
57
  return {
58
58
  kind: "relatedList",
59
+ ...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
59
60
  title: translate(sectionSpec.title),
60
61
  query: sectionSpec.query,
61
62
  ...(sectionSpec.parentParam !== undefined && { parentParam: sectionSpec.parentParam }),
@@ -67,13 +68,14 @@ export function computeRelatedListSectionViewModel(sectionSpec, translate) {
67
68
  }),
68
69
  ...(sectionSpec.itemNoun !== undefined && { itemNoun: sectionSpec.itemNoun }),
69
70
  ...(sectionSpec.pageSize !== undefined && { pageSize: sectionSpec.pageSize }),
71
+ ...(sectionSpec.table !== undefined && { table: sectionSpec.table }),
70
72
  ...(sectionSpec.defaultSort !== undefined && { defaultSort: sectionSpec.defaultSort }),
71
73
  ...(sectionSpec.searchable !== undefined && { searchable: sectionSpec.searchable }),
72
74
  ...(sectionSpec.facets !== undefined && { facets: sectionSpec.facets }),
73
75
  ...(sectionSpec.rowClick !== undefined && { rowClick: sectionSpec.rowClick }),
74
76
  ...(sectionSpec.rowActions !== undefined && { rowActions: sectionSpec.rowActions }),
75
77
  ...(sectionSpec.rowActionMode !== undefined && { rowActionMode: sectionSpec.rowActionMode }),
76
- ...(sectionSpec.groupBy !== undefined && { groupBy: sectionSpec.groupBy }),
78
+ ...(sectionSpec.bulkActions !== undefined && { bulkActions: sectionSpec.bulkActions }),
77
79
  ...(sectionSpec.rowTone !== undefined && { rowTone: sectionSpec.rowTone }),
78
80
  ...(sectionSpec.toolbarActions !== undefined && { toolbarActions: sectionSpec.toolbarActions }),
79
81
  ...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
@@ -469,6 +471,7 @@ export function computeEditViewModel(input) {
469
471
  .map((group) => ({
470
472
  title: translate(group.title),
471
473
  columns: group.columns ?? 2,
474
+ columnsDeclared: group.columns !== undefined,
472
475
  fields: group.fields.map((fieldSpec) => {
473
476
  const fieldName = normalizeEditField(fieldSpec).field;
474
477
  const fieldView = fields.find((f) => f.field === fieldName);
@@ -491,6 +494,7 @@ export function computeEditViewModel(input) {
491
494
  }),
492
495
  ...(sectionSpec.subtitle !== undefined && { subtitle: translate(sectionSpec.subtitle) }),
493
496
  columns: sectionSpec.columns ?? defaultColumns,
497
+ columnsDeclared: sectionSpec.columns !== undefined,
494
498
  fields,
495
499
  ...(groups !== undefined && { groups }),
496
500
  ...(sectionSpec.icon !== undefined && { icon: sectionSpec.icon }),
@@ -9,167 +9,202 @@ const WRITE_ONLY_SET_LABEL_KEY = "kumiko.field.writeOnly.set";
9
9
  // follow-up; this v1 assumes the caller passed a row-filter that drops any
10
10
  // field the user may not see.
11
11
  export function computeListViewModel(input) {
12
- const { screen, entity, rows, translate, featureName } = input;
13
- const columns = [];
14
- for (const spec of screen.columns) {
15
- const normalized = normalizeListColumn(spec);
16
- const storedFieldType = entity.fields[normalized.field]?.type;
17
- // A real non-text entity field keeps its own type (documented on
18
- // ListColumnSpec.refEntity); only the all-"text" pseudo-entities of the
19
- // projection shims and fields without a stored def take the declared path.
20
- if (normalized.refEntity !== undefined &&
21
- (storedFieldType === undefined || storedFieldType === "text")) {
22
- // Declared reference metadata (ListColumnSpec.refEntity) — for
23
- // projectionList/relatedList columns, which have no EntityDefinition
24
- // field to carry a real "reference" type. Checked before the
25
- // entity.fields lookup below so it also fires against the pseudo-
26
- // entities that hardcode every field as "text" (projection-list-shim,
27
- // related-list-section) — those never reach the reference branch
28
- // further down since fieldDef.type is always "text" there.
29
- const refTarget = parseRefTarget(normalized.refEntity, featureName);
30
- columns.push({
31
- field: normalized.field,
32
- label: translate(normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field)),
33
- type: "reference",
34
- // The declared value is the referenced id, not the resolved label —
35
- // there is no column to server-sort by, same as row-meta references.
36
- sortable: false,
37
- refEntity: refTarget.entityName,
38
- refFeature: refTarget.featureName,
39
- refLabelField: normalized.refLabelField ?? "id",
40
- ...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
41
- ...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
42
- });
43
- continue;
44
- }
45
- const fieldDef = entity.fields[normalized.field];
46
- if (!fieldDef) {
47
- // Not a stored field — may be a read-time derived field (value appended
48
- // by the list-query handler). Render it as its display valueType; derived
49
- // columns carry no reference/select metadata and never server-sort.
50
- const derivedDef = entity.derivedFields?.[normalized.field];
51
- if (!derivedDef) {
52
- // Row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) —
53
- // a real base-table column that's never a declared entity field, e.g.
54
- // a SystemAdmin cross-tenant list exposing tenantId. Unlike the
55
- // derivedFields branch above, these are actual DB columns, so
56
- // server-side ORDER BY works — sortable stays true, except for the
57
- // ID columns in LIST_ROW_META_REFERENCES (see their reference branch).
58
- const rowMetaType = LIST_ROW_META_COLUMNS[normalized.field];
59
- if (rowMetaType !== undefined) {
60
- const rowMetaRef = LIST_ROW_META_REFERENCES[normalized.field];
61
- columns.push({
62
- field: normalized.field,
63
- label: translate(normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field)),
64
- type: rowMetaRef !== undefined ? "reference" : rowMetaType,
65
- // The DB column holds the GUID, not the resolved label — server-side
66
- // ORDER BY would sort by GUID while the cell shows the name.
67
- sortable: rowMetaRef === undefined,
68
- ...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
69
- ...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
70
- ...(rowMetaRef !== undefined && {
71
- refEntity: rowMetaRef.refEntity,
72
- refFeature: rowMetaRef.refFeature,
73
- refLabelField: rowMetaRef.refLabelField,
74
- }),
75
- });
76
- continue;
77
- }
78
- // A virtual presentational column — drawn entirely by a columnRenderer
79
- // component from the row (e.g. tag chips); value is undefined and it
80
- // never server-sorts. `field` is just the column key. It needs BOTH a
81
- // label AND a renderer — renderer is what actually draws it; a label
82
- // alone with no renderer would push an empty, unrendered column into
83
- // the view model. Missing either is an author typo (or a stale
84
- // field-rename) → fail loud instead of silently drawing nothing.
85
- if (normalized.label !== undefined && normalized.renderer !== undefined) {
86
- columns.push({
87
- field: normalized.field,
88
- label: translate(normalized.label),
89
- type: "text",
90
- sortable: false,
91
- renderer: normalized.renderer,
92
- ...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
93
- });
94
- continue;
95
- }
96
- throw new Error(`computeListViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`);
97
- }
98
- columns.push({
99
- field: normalized.field,
100
- label: translate(normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field)),
101
- type: derivedDef.valueType,
102
- // Display-only: a header sort would round-trip to the server, which has
103
- // no column to sort by (see DerivedFieldDef). Never offer the affordance.
104
- sortable: false,
105
- ...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
106
- ...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
107
- });
108
- continue;
109
- }
110
- const label = translate(normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field));
111
- // Reference fields may target the same feature ("user") or another feature
112
- // ("users:user"). parseRefTarget returns the feature and entity names, which
113
- // the renderer uses to build the lookup query as
114
- // `<refFeature>:query:<refEntity>:list`.
115
- const refRaw = fieldDef.type === "reference" ? fieldDef.entity : undefined;
116
- const refTarget = refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined;
117
- const refEntity = refTarget?.entityName;
118
- const refFeature = refTarget?.featureName;
119
- const refLabelField = fieldDef.type === "reference" ? (fieldDef.labelField ?? "id") : undefined;
120
- // For select and multiSelect fields: bake in translated option labels.
121
- // Convention matches the form path → one translation source for list +
122
- // form. A missing key returns the key itself per convention; the
123
- // renderer then falls back to humanizeSlug.
124
- const optionLabels = fieldDef.type === "select" || fieldDef.type === "multiSelect"
125
- ? buildOptionLabels(translate, (value) => fieldOptionLabelKey(featureName, screen.entity, normalized.field, value), fieldDef.options)
126
- : undefined;
127
- const optionTones = fieldDef.type === "select" ? fieldDef.optionTones : undefined;
128
- const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
129
- const fileDisplay = deriveListFileDisplay(fieldDef, normalized.renderer);
130
- const column = {
131
- field: normalized.field,
132
- label,
133
- type: fieldDef.type,
134
- sortable: fieldIsSortable(fieldDef),
135
- ...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
136
- ...(optionLabels !== undefined && { optionLabels }),
137
- ...(optionTones !== undefined && { optionTones }),
138
- ...(refEntity !== undefined && { refEntity }),
139
- ...(refFeature !== undefined && { refFeature }),
140
- ...(refLabelField !== undefined && { refLabelField }),
141
- ...(grouping !== undefined && { grouping }),
142
- ...(fileDisplay !== undefined && { fileDisplay }),
143
- ...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
144
- };
145
- columns.push(column);
12
+ const { screen, rows } = input;
13
+ const columns = screen.columns.map((spec) => resolveListColumn(input, normalizeListColumn(spec)));
14
+ const listRows = rows.map(writeOnlyRowFormatter(input));
15
+ return {
16
+ screenId: screen.id,
17
+ entityName: screen.entity,
18
+ columns,
19
+ rows: listRows,
20
+ ...(screen.slots && { slots: screen.slots }),
21
+ isEmpty: listRows.length === 0,
22
+ };
23
+ }
24
+ // First match wins; the order encodes which metadata source a column name
25
+ // resolves against when several could claim it.
26
+ const LIST_COLUMN_RESOLVERS = [
27
+ declaredReferenceColumn,
28
+ storedFieldColumn,
29
+ derivedFieldColumn,
30
+ rowMetaColumn,
31
+ virtualRendererColumn,
32
+ ];
33
+ function resolveListColumn(input, column) {
34
+ for (const resolve of LIST_COLUMN_RESOLVERS) {
35
+ const resolved = resolve(input, column);
36
+ if (resolved)
37
+ return resolved;
146
38
  }
39
+ // No metadata source claims the name: an author typo or a stale field
40
+ // rename, so fail loud instead of silently drawing nothing.
41
+ throw new Error(`computeListViewModel: screen "${input.screen.id}" references unknown field "${column.field}" on entity "${input.screen.entity}"`);
42
+ }
43
+ function translatedColumnLabel(input, column) {
44
+ return input.translate(column.label ?? fieldLabelKey(input.featureName, input.screen.entity, column.field));
45
+ }
46
+ // A real non-text entity field keeps its own type (documented on
47
+ // ListColumnSpec.refEntity); only the all-"text" pseudo-entities of the
48
+ // projection shims and fields without a stored def take the declared path.
49
+ function declaredReferenceColumn(input, column) {
50
+ const storedFieldType = input.entity.fields[column.field]?.type;
51
+ // skip: no declared reference, or a real non-text field that keeps its own type
52
+ if (column.refEntity === undefined ||
53
+ (storedFieldType !== undefined && storedFieldType !== "text")) {
54
+ return undefined;
55
+ }
56
+ // Declared reference metadata (ListColumnSpec.refEntity) — for
57
+ // projectionList/relatedList columns, which have no EntityDefinition
58
+ // field to carry a real "reference" type. Checked before the
59
+ // entity.fields lookup so it also fires against the pseudo-entities
60
+ // that hardcode every field as "text" (projection-list-shim,
61
+ // related-list-section) — those never reach the stored reference branch
62
+ // since fieldDef.type is always "text" there.
63
+ const refTarget = parseRefTarget(column.refEntity, input.featureName);
64
+ return {
65
+ field: column.field,
66
+ label: translatedColumnLabel(input, column),
67
+ type: "reference",
68
+ // The declared value is the referenced id, not the resolved label —
69
+ // there is no column to server-sort by, same as row-meta references.
70
+ sortable: false,
71
+ refEntity: refTarget.entityName,
72
+ refFeature: refTarget.featureName,
73
+ refLabelField: column.refLabelField ?? "id",
74
+ ...(column.renderer !== undefined && { renderer: column.renderer }),
75
+ ...(column.hideOnNarrow === true && { hideOnNarrow: true }),
76
+ };
77
+ }
78
+ function storedFieldColumn(input, column) {
79
+ const fieldDef = input.entity.fields[column.field];
80
+ // skip: not a stored field, a later resolver decides
81
+ if (!fieldDef)
82
+ return undefined;
83
+ return {
84
+ field: column.field,
85
+ label: translatedColumnLabel(input, column),
86
+ type: fieldDef.type,
87
+ sortable: fieldIsSortable(fieldDef),
88
+ ...(column.renderer !== undefined && { renderer: column.renderer }),
89
+ ...storedFieldMetadata(input, column, fieldDef),
90
+ ...(column.hideOnNarrow === true && { hideOnNarrow: true }),
91
+ };
92
+ }
93
+ // Key order is part of the serialized view model, so it stays fixed here.
94
+ function storedFieldMetadata(input, column, fieldDef) {
95
+ // For select and multiSelect fields: bake in translated option labels.
96
+ // Convention matches the form path → one translation source for list +
97
+ // form. A missing key returns the key itself per convention; the
98
+ // renderer then falls back to humanizeSlug.
99
+ const optionLabels = fieldDef.type === "select" || fieldDef.type === "multiSelect"
100
+ ? buildOptionLabels(input.translate, (value) => fieldOptionLabelKey(input.featureName, input.screen.entity, column.field, value), fieldDef.options)
101
+ : undefined;
102
+ const optionTones = fieldDef.type === "select" ? fieldDef.optionTones : undefined;
103
+ const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
104
+ const fileDisplay = deriveListFileDisplay(fieldDef, column.renderer);
105
+ return {
106
+ ...(optionLabels !== undefined && { optionLabels }),
107
+ ...(optionTones !== undefined && { optionTones }),
108
+ ...storedReferenceMetadata(fieldDef, input.featureName),
109
+ ...(grouping !== undefined && { grouping }),
110
+ ...(fileDisplay !== undefined && { fileDisplay }),
111
+ };
112
+ }
113
+ // Reference fields may target the same feature ("user") or another feature
114
+ // ("users:user"). parseRefTarget returns the feature and entity names, which
115
+ // the renderer uses to build the lookup query as
116
+ // `<refFeature>:query:<refEntity>:list`.
117
+ function storedReferenceMetadata(fieldDef, featureName) {
118
+ if (fieldDef.type !== "reference")
119
+ return {};
120
+ const refTarget = parseRefTarget(fieldDef.entity, featureName);
121
+ return {
122
+ refEntity: refTarget.entityName,
123
+ refFeature: refTarget.featureName,
124
+ refLabelField: fieldDef.labelField ?? "id",
125
+ };
126
+ }
127
+ // Not a stored field — may be a read-time derived field (value appended
128
+ // by the list-query handler). Render it as its display valueType; derived
129
+ // columns carry no reference/select metadata and never server-sort.
130
+ function derivedFieldColumn(input, column) {
131
+ const derivedDef = input.entity.derivedFields?.[column.field];
132
+ // skip: not a derived field, a later resolver decides
133
+ if (!derivedDef)
134
+ return undefined;
135
+ return {
136
+ field: column.field,
137
+ label: translatedColumnLabel(input, column),
138
+ type: derivedDef.valueType,
139
+ // Display-only: a header sort would round-trip to the server, which has
140
+ // no column to sort by (see DerivedFieldDef). Never offer the affordance.
141
+ sortable: false,
142
+ ...(column.renderer !== undefined && { renderer: column.renderer }),
143
+ ...(column.hideOnNarrow === true && { hideOnNarrow: true }),
144
+ };
145
+ }
146
+ // Row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) —
147
+ // a real base-table column that's never a declared entity field, e.g.
148
+ // a SystemAdmin cross-tenant list exposing tenantId. Unlike derived
149
+ // fields, these are actual DB columns, so server-side ORDER BY works —
150
+ // sortable stays true, except for the ID columns in LIST_ROW_META_REFERENCES.
151
+ function rowMetaColumn(input, column) {
152
+ const rowMetaType = LIST_ROW_META_COLUMNS[column.field];
153
+ // skip: not a base-table column, a later resolver decides
154
+ if (rowMetaType === undefined)
155
+ return undefined;
156
+ const rowMetaRef = LIST_ROW_META_REFERENCES[column.field];
157
+ return {
158
+ field: column.field,
159
+ label: translatedColumnLabel(input, column),
160
+ type: rowMetaRef !== undefined ? "reference" : rowMetaType,
161
+ // The DB column holds the GUID, not the resolved label — server-side
162
+ // ORDER BY would sort by GUID while the cell shows the name.
163
+ sortable: rowMetaRef === undefined,
164
+ ...(column.renderer !== undefined && { renderer: column.renderer }),
165
+ ...(column.hideOnNarrow === true && { hideOnNarrow: true }),
166
+ ...(rowMetaRef !== undefined && {
167
+ refEntity: rowMetaRef.refEntity,
168
+ refFeature: rowMetaRef.refFeature,
169
+ refLabelField: rowMetaRef.refLabelField,
170
+ }),
171
+ };
172
+ }
173
+ // A virtual presentational column — drawn entirely by a columnRenderer
174
+ // component from the row (e.g. tag chips); value is undefined and it
175
+ // never server-sorts. `field` is just the column key. It needs BOTH a
176
+ // label AND a renderer — renderer is what actually draws it; a label
177
+ // alone with no renderer would push an empty, unrendered column into
178
+ // the view model.
179
+ function virtualRendererColumn(input, column) {
180
+ // skip: without both a label and a renderer the column is unknown
181
+ if (column.label === undefined || column.renderer === undefined)
182
+ return undefined;
183
+ return {
184
+ field: column.field,
185
+ label: input.translate(column.label),
186
+ type: "text",
187
+ sortable: false,
188
+ renderer: column.renderer,
189
+ ...(column.hideOnNarrow === true && { hideOnNarrow: true }),
190
+ };
191
+ }
192
+ // The wire carries `true` (set) / `null` (empty) for writeOnly fields; the cell shows a word, not "true".
193
+ function writeOnlyRowFormatter(input) {
194
+ const { screen, entity, translate } = input;
147
195
  const writeOnlyFieldNames = screen.columns
148
196
  .map((spec) => normalizeListColumn(spec).field)
149
197
  .filter((name) => {
150
198
  const fieldDef = entity.fields[name];
151
199
  return fieldDef?.type === "text" && fieldDef.writeOnly === true;
152
200
  });
153
- // The wire carries `true` (set) / `null` (empty) for writeOnly fields; the cell shows a word, not "true".
154
- const showWriteOnlyAsWord = (row) => {
201
+ return (row) => {
155
202
  const shown = { ...row };
156
203
  for (const name of writeOnlyFieldNames) {
157
204
  if (row[name] === true)
158
205
  shown[name] = translate(WRITE_ONLY_SET_LABEL_KEY);
159
206
  }
160
- return shown;
161
- };
162
- const listRows = rows.map((row) => ({
163
- id: String(row["id"] ?? ""),
164
- values: showWriteOnlyAsWord(row),
165
- }));
166
- return {
167
- screenId: screen.id,
168
- entityName: screen.entity,
169
- columns,
170
- rows: listRows,
171
- ...(screen.slots && { slots: screen.slots }),
172
- isEmpty: listRows.length === 0,
207
+ return { id: String(row["id"] ?? ""), values: shown };
173
208
  };
174
209
  }
175
210
  // Field-i18n-key convention matches what features register translations
@@ -1,4 +1,4 @@
1
- import type { EditFieldSpec, EditSectionSpec, FieldAffix, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListColumnSpec, ListFacetSpec, ListSortSpec, OptionsQueryPayload, PlatformComponent, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
1
+ import type { EditFieldSpec, EditSectionSpec, FieldAffix, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListBulkAction, ListColumnSpec, ListFacetSpec, ListSortSpec, ListTableOptions, OptionsQueryPayload, PlatformComponent, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
2
2
  export type RuntimeRenderer = (value: unknown, row?: Readonly<Record<string, unknown>>) => string;
3
3
  /** Where a cell's text links to. `href` comes from NavApi.hrefFor (new-tab and
4
4
  * copy-link work); `onPress` is the in-app navigation for a plain click. */
@@ -300,12 +300,15 @@ export type EditFieldsSectionViewModel = {
300
300
  /** Translated `EditFieldsSection.subtitle` (wizard step rail / up-next box). */
301
301
  readonly subtitle?: string;
302
302
  readonly columns: number;
303
+ /** True when the author declared `columns`; false means the tabs/stacked default. */
304
+ readonly columnsDeclared?: boolean;
303
305
  readonly fields: readonly EditFieldViewModel[];
304
306
  /** From `EditFieldsSection.groups` — each entry's `fields` are the same
305
307
  * view-model objects as `fields` above, re-grouped (not recomputed). */
306
308
  readonly groups?: readonly {
307
309
  readonly title: string;
308
310
  readonly columns: number;
311
+ readonly columnsDeclared?: boolean;
309
312
  readonly fields: readonly EditFieldViewModel[];
310
313
  }[];
311
314
  /** From `EditFieldsSection.icon` — closed IconKey vocabulary, renders
@@ -330,6 +333,7 @@ export type EditExtensionSectionViewModel = {
330
333
  };
331
334
  export type EditRelatedListSectionViewModel = {
332
335
  readonly kind: "relatedList";
336
+ readonly id?: string;
333
337
  readonly title: string;
334
338
  readonly query: string;
335
339
  readonly parentParam?: string;
@@ -342,6 +346,7 @@ export type EditRelatedListSectionViewModel = {
342
346
  /** Untranslated plural-forms key, resolved with the row count by the list footer. */
343
347
  readonly itemNoun?: string;
344
348
  readonly pageSize?: number;
349
+ readonly table?: ListTableOptions;
345
350
  readonly defaultSort?: ListSortSpec;
346
351
  readonly searchable?: boolean;
347
352
  readonly facets?: readonly ListFacetSpec[];
@@ -351,7 +356,7 @@ export type EditRelatedListSectionViewModel = {
351
356
  };
352
357
  readonly rowActions?: readonly RowAction[];
353
358
  readonly rowActionMode?: RowActionMode;
354
- readonly groupBy?: RelatedListGroupBy;
359
+ readonly bulkActions?: readonly ListBulkAction[];
355
360
  readonly rowTone?: RelatedListRowTone;
356
361
  readonly toolbarActions?: readonly RelatedListToolbarAction[];
357
362
  /** From `EditRelatedListSection.actions` — rendered in the
@@ -0,0 +1,19 @@
1
+ import type { DataTableColumnPin, DataTableColumnPins, DataTableColumnWidths } from "../data-table-view-types.ts";
2
+ export declare const DEFAULT_COLUMN_WIDTH_PX = 160;
3
+ export declare const DEFAULT_MIN_COLUMN_WIDTH_PX = 64;
4
+ /** Upper bound so a tampered stored width cannot blow the table up. */
5
+ export declare const MAX_COLUMN_WIDTH_PX = 2000;
6
+ type LayoutSchemaColumn = {
7
+ readonly field: string;
8
+ readonly pinned?: DataTableColumnPin;
9
+ readonly width?: number;
10
+ readonly minWidth?: number;
11
+ };
12
+ export declare function defaultPins(schema: readonly LayoutSchemaColumn[]): DataTableColumnPins;
13
+ export declare function minWidthsOf(schema: readonly LayoutSchemaColumn[]): DataTableColumnWidths;
14
+ export declare function clampWidth(width: number, min: number): number;
15
+ export declare function defaultWidths(schema: readonly LayoutSchemaColumn[], minWidths: DataTableColumnWidths): DataTableColumnWidths;
16
+ /** A saved width wins per field; a column the saved view has never seen keeps its default. */
17
+ export declare function mergeSavedWidths(defaults: DataTableColumnWidths, saved: Readonly<Record<string, number>>, minWidths: DataTableColumnWidths): DataTableColumnWidths;
18
+ export declare function sameRecord<T>(a: Readonly<Record<string, T>>, b: Readonly<Record<string, T>>): boolean;
19
+ export {};