@cosmicdrift/kumiko-renderer 0.328.0 → 0.329.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 (259) hide show
  1. package/dist/app/action-form-shim.d.ts +14 -0
  2. package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
  3. package/dist/app/app-features-context.d.ts +8 -0
  4. package/dist/app/app-features-context.js +16 -0
  5. package/dist/app/column-renderers.d.ts +30 -0
  6. package/dist/app/column-renderers.js +34 -0
  7. package/dist/app/config-edit-shim.d.ts +11 -0
  8. package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
  9. package/dist/app/content-editors.d.ts +38 -0
  10. package/dist/app/content-editors.js +26 -0
  11. package/dist/app/content-preview.d.ts +11 -0
  12. package/dist/app/content-preview.js +30 -0
  13. package/dist/app/custom-screens.d.ts +11 -0
  14. package/dist/app/custom-screens.js +20 -0
  15. package/dist/app/dashboard-body.d.ts +16 -0
  16. package/dist/app/dashboard-body.js +11 -0
  17. package/dist/app/extension-form-submit.d.ts +32 -0
  18. package/dist/app/extension-form-submit.js +73 -0
  19. package/dist/app/extension-sections.d.ts +72 -0
  20. package/dist/app/extension-sections.js +34 -0
  21. package/dist/app/feature-schema.d.ts +4 -0
  22. package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
  23. package/dist/app/form-schema.d.ts +4 -0
  24. package/dist/app/form-schema.js +85 -0
  25. package/dist/app/kumiko-screen.d.ts +43 -0
  26. package/dist/app/kumiko-screen.js +2206 -0
  27. package/dist/app/layout-fields.d.ts +3 -0
  28. package/dist/app/layout-fields.js +20 -0
  29. package/dist/app/list-facets.d.ts +48 -0
  30. package/dist/app/list-facets.js +185 -0
  31. package/dist/app/nav.d.ts +61 -0
  32. package/dist/app/nav.js +139 -0
  33. package/dist/app/projection-detail-shim.d.ts +8 -0
  34. package/dist/app/projection-detail-shim.js +71 -0
  35. package/dist/app/projection-list-shim.d.ts +13 -0
  36. package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
  37. package/dist/app/qn.d.ts +7 -0
  38. package/{src/app/qn.ts → dist/app/qn.js} +9 -11
  39. package/dist/app/qualify-screen-id.d.ts +2 -0
  40. package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
  41. package/dist/app/reference-facet-bridge.d.ts +10 -0
  42. package/dist/app/reference-facet-bridge.js +22 -0
  43. package/dist/app/return-to.d.ts +31 -0
  44. package/dist/app/return-to.js +188 -0
  45. package/dist/app/row-actions.d.ts +77 -0
  46. package/dist/app/row-actions.js +416 -0
  47. package/dist/app/screen-access.d.ts +8 -0
  48. package/dist/app/screen-access.js +48 -0
  49. package/dist/app/secret-mint-body.d.ts +10 -0
  50. package/dist/app/secret-mint-body.js +140 -0
  51. package/dist/app/secrets-edit-body.d.ts +8 -0
  52. package/dist/app/secrets-edit-body.js +76 -0
  53. package/dist/app/use-embedded-screen.d.ts +6 -0
  54. package/dist/app/use-embedded-screen.js +19 -0
  55. package/dist/app/variable-chips.d.ts +7 -0
  56. package/dist/app/variable-chips.js +16 -0
  57. package/dist/app/write-failed-error.d.ts +6 -0
  58. package/dist/app/write-failed-error.js +21 -0
  59. package/dist/components/embedded-list-field.d.ts +10 -0
  60. package/dist/components/embedded-list-field.js +259 -0
  61. package/dist/components/grid-cell-for-field.d.ts +25 -0
  62. package/dist/components/grid-cell-for-field.js +31 -0
  63. package/dist/components/multi-select-checkboxes.d.ts +14 -0
  64. package/dist/components/multi-select-checkboxes.js +36 -0
  65. package/dist/components/reference-create-dialog.d.ts +13 -0
  66. package/dist/components/reference-create-dialog.js +46 -0
  67. package/dist/components/related-list-section.d.ts +25 -0
  68. package/dist/components/related-list-section.js +273 -0
  69. package/dist/components/render-edit-action-button.d.ts +22 -0
  70. package/dist/components/render-edit-action-button.js +43 -0
  71. package/dist/components/render-edit-logic.d.ts +6 -0
  72. package/dist/components/render-edit-logic.js +63 -0
  73. package/dist/components/render-edit-types.d.ts +209 -0
  74. package/dist/components/render-edit-types.js +1 -0
  75. package/dist/components/render-edit.d.ts +6 -0
  76. package/dist/components/render-edit.js +1169 -0
  77. package/dist/components/render-field.d.ts +38 -0
  78. package/dist/components/render-field.js +544 -0
  79. package/dist/components/render-list.d.ts +117 -0
  80. package/dist/components/render-list.js +232 -0
  81. package/dist/components/write-form-section.d.ts +17 -0
  82. package/dist/components/write-form-section.js +104 -0
  83. package/dist/context/dispatcher-context.d.ts +10 -0
  84. package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
  85. package/dist/context/draft-storage-context.d.ts +12 -0
  86. package/dist/context/draft-storage-context.js +18 -0
  87. package/dist/context/user-roles-context.d.ts +7 -0
  88. package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
  89. package/dist/format-when.d.ts +1 -0
  90. package/{src/format-when.ts → dist/format-when.js} +7 -7
  91. package/dist/hooks/reference-limits.d.ts +3 -0
  92. package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
  93. package/dist/hooks/use-ai-text.d.ts +52 -0
  94. package/dist/hooks/use-ai-text.js +90 -0
  95. package/dist/hooks/use-disclosure.d.ts +7 -0
  96. package/dist/hooks/use-disclosure.js +8 -0
  97. package/dist/hooks/use-form.d.ts +7 -0
  98. package/dist/hooks/use-form.js +36 -0
  99. package/dist/hooks/use-list-url-state.d.ts +43 -0
  100. package/dist/hooks/use-list-url-state.js +131 -0
  101. package/dist/hooks/use-mutation.d.ts +9 -0
  102. package/dist/hooks/use-mutation.js +37 -0
  103. package/dist/hooks/use-query.d.ts +14 -0
  104. package/dist/hooks/use-query.js +118 -0
  105. package/dist/hooks/use-reference-lookup.d.ts +14 -0
  106. package/dist/hooks/use-reference-lookup.js +63 -0
  107. package/dist/hooks/use-store.d.ts +3 -0
  108. package/dist/hooks/use-store.js +38 -0
  109. package/dist/hooks/use-stream-handler.d.ts +14 -0
  110. package/dist/hooks/use-stream-handler.js +93 -0
  111. package/dist/i18n-defaults.d.ts +2 -0
  112. package/dist/i18n-defaults.js +201 -0
  113. package/dist/i18n.d.ts +64 -0
  114. package/dist/i18n.js +166 -0
  115. package/dist/index.d.ts +77 -0
  116. package/dist/index.js +55 -0
  117. package/dist/inside-drawer.d.ts +2 -0
  118. package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
  121. package/dist/primitives.d.ts +1355 -0
  122. package/dist/primitives.js +73 -0
  123. package/dist/screen-fills-height.d.ts +3 -0
  124. package/dist/screen-fills-height.js +3 -0
  125. package/dist/share-links.d.ts +4 -0
  126. package/dist/share-links.js +11 -0
  127. package/dist/sort-by-accessor.d.ts +5 -0
  128. package/dist/sort-by-accessor.js +16 -0
  129. package/dist/sse/live-events.d.ts +23 -0
  130. package/dist/sse/live-events.js +28 -0
  131. package/dist/tokens.d.ts +83 -0
  132. package/dist/tokens.js +80 -0
  133. package/package.json +16 -9
  134. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  135. package/src/__tests__/format-when.test.ts +0 -12
  136. package/src/__tests__/i18n.test.tsx +0 -283
  137. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  138. package/src/__tests__/qn.test.ts +0 -83
  139. package/src/__tests__/share-links.test.ts +0 -26
  140. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  141. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  142. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  143. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  144. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  145. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  146. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  147. package/src/app/__tests__/content-editors.test.tsx +0 -128
  148. package/src/app/__tests__/content-preview.test.tsx +0 -184
  149. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  150. package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
  151. package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
  152. package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
  153. package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
  154. package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
  155. package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
  156. package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
  157. package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
  158. package/src/app/__tests__/feature-schema.test.ts +0 -40
  159. package/src/app/__tests__/form-schema.test.ts +0 -247
  160. package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
  161. package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
  162. package/src/app/__tests__/nav.test.ts +0 -198
  163. package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
  164. package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
  165. package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
  166. package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
  167. package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
  168. package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
  169. package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
  170. package/src/app/__tests__/qualified-names.test.ts +0 -37
  171. package/src/app/__tests__/return-to.test.ts +0 -396
  172. package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
  173. package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
  174. package/src/app/__tests__/screen-access-allows.test.ts +0 -34
  175. package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
  176. package/src/app/__tests__/variable-chips.test.tsx +0 -70
  177. package/src/app/app-features-context.tsx +0 -25
  178. package/src/app/column-renderers.tsx +0 -68
  179. package/src/app/content-editors.tsx +0 -85
  180. package/src/app/content-preview.tsx +0 -67
  181. package/src/app/custom-screens.tsx +0 -29
  182. package/src/app/dashboard-body.tsx +0 -33
  183. package/src/app/extension-form-submit.tsx +0 -123
  184. package/src/app/extension-sections.tsx +0 -111
  185. package/src/app/form-schema.ts +0 -91
  186. package/src/app/kumiko-screen.tsx +0 -3801
  187. package/src/app/layout-fields.ts +0 -31
  188. package/src/app/list-facets.ts +0 -259
  189. package/src/app/nav.tsx +0 -258
  190. package/src/app/projection-detail-shim.ts +0 -86
  191. package/src/app/reference-facet-bridge.tsx +0 -58
  192. package/src/app/return-to.tsx +0 -234
  193. package/src/app/row-actions.ts +0 -640
  194. package/src/app/screen-access.ts +0 -63
  195. package/src/app/secret-mint-body.tsx +0 -259
  196. package/src/app/secrets-edit-body.tsx +0 -194
  197. package/src/app/use-embedded-screen.ts +0 -27
  198. package/src/app/variable-chips.tsx +0 -39
  199. package/src/app/write-failed-error.ts +0 -27
  200. package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
  201. package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
  202. package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
  203. package/src/components/__tests__/related-list-section.test.tsx +0 -1733
  204. package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
  205. package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
  206. package/src/components/__tests__/render-edit-logic.test.ts +0 -314
  207. package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
  208. package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
  209. package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
  210. package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
  211. package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
  212. package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
  213. package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
  214. package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
  215. package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
  216. package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
  217. package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
  218. package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
  219. package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
  220. package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
  221. package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
  222. package/src/components/__tests__/write-form-section.test.tsx +0 -375
  223. package/src/components/embedded-list-field.tsx +0 -354
  224. package/src/components/grid-cell-for-field.tsx +0 -88
  225. package/src/components/multi-select-checkboxes.tsx +0 -93
  226. package/src/components/reference-create-dialog.tsx +0 -91
  227. package/src/components/related-list-section.tsx +0 -491
  228. package/src/components/render-edit-action-button.tsx +0 -109
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -226
  231. package/src/components/render-edit.tsx +0 -1871
  232. package/src/components/render-field.tsx +0 -951
  233. package/src/components/render-list.tsx +0 -593
  234. package/src/components/write-form-section.tsx +0 -198
  235. package/src/context/draft-storage-context.tsx +0 -39
  236. package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
  237. package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
  238. package/src/hooks/__tests__/use-form.test.tsx +0 -65
  239. package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
  240. package/src/hooks/__tests__/use-query.test.tsx +0 -149
  241. package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
  242. package/src/hooks/use-ai-text.ts +0 -172
  243. package/src/hooks/use-disclosure.ts +0 -20
  244. package/src/hooks/use-form.ts +0 -78
  245. package/src/hooks/use-list-url-state.ts +0 -207
  246. package/src/hooks/use-mutation.ts +0 -61
  247. package/src/hooks/use-query.ts +0 -166
  248. package/src/hooks/use-reference-lookup.ts +0 -76
  249. package/src/hooks/use-store.ts +0 -48
  250. package/src/hooks/use-stream-handler.ts +0 -134
  251. package/src/i18n-defaults.ts +0 -240
  252. package/src/i18n.tsx +0 -286
  253. package/src/index.ts +0 -279
  254. package/src/primitives.tsx +0 -1567
  255. package/src/screen-fills-height.ts +0 -3
  256. package/src/share-links.ts +0 -12
  257. package/src/sort-by-accessor.ts +0 -20
  258. package/src/sse/live-events.tsx +0 -56
  259. package/src/tokens.tsx +0 -170
@@ -0,0 +1,3 @@
1
+ import type { EditFieldSpec, EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ export declare function layoutEditFields(screen: EntityEditScreenDefinition): readonly Exclude<EditFieldSpec, string>[];
3
+ export declare function layoutFieldNames(screen: EntityEditScreenDefinition): ReadonlySet<string>;
@@ -0,0 +1,20 @@
1
+ import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ // Normalized field specs actually rendered by the screen's layout, extension
3
+ // (and relatedList) sections skipped. Both this and `layoutFieldNames` key
4
+ // off "rendered by the layout" for the same reason: a field the user never
5
+ // sees gets no chance to review/correct a value nor to fix a presence error
6
+ // (search-param merge, #1708; presence schema in form-schema.ts).
7
+ export function layoutEditFields(screen) {
8
+ const specs = [];
9
+ for (const section of screen.layout.sections) {
10
+ if (!isFieldsEditSection(section))
11
+ continue;
12
+ for (const spec of sectionFieldSpecs(section)) {
13
+ specs.push(normalizeEditField(spec));
14
+ }
15
+ }
16
+ return specs;
17
+ }
18
+ export function layoutFieldNames(screen) {
19
+ return new Set(layoutEditFields(screen).map((spec) => spec.field));
20
+ }
@@ -0,0 +1,48 @@
1
+ import type { ListFacetSpec } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { Translate } from "@cosmicdrift/kumiko-headless";
3
+ import type { DataTableFacet } from "../primitives.js";
4
+ export type ResolvedFacetSpec = {
5
+ readonly field: string;
6
+ readonly type: "select" | "boolean" | "reference";
7
+ readonly label: string;
8
+ /** Empty for a "reference" facet until `ReferenceFacetBridges` resolves it
9
+ * at render time, unlike select/boolean's author-declared options. */
10
+ readonly options: readonly {
11
+ readonly value: string;
12
+ readonly label: string;
13
+ }[];
14
+ /** Present only for a "reference" facet — the resolved lookup target
15
+ * `ReferenceFacetBridges` needs to fetch its options. */
16
+ readonly reference?: {
17
+ readonly refEntity: string;
18
+ readonly refFeature: string;
19
+ readonly labelField: string;
20
+ };
21
+ };
22
+ export declare function mergeReferenceFacetOptions(specs: readonly ResolvedFacetSpec[], optionsByField: Readonly<Record<string, readonly {
23
+ readonly value: string;
24
+ readonly label: string;
25
+ }[]>>): ResolvedFacetSpec[];
26
+ export declare function buildFilterFacets(specs: readonly ResolvedFacetSpec[]): DataTableFacet[];
27
+ export declare function buildFilterPayload(urlFilters: Readonly<Record<string, readonly string[]>>, typeOf: (field: string) => string | undefined): {
28
+ field: string;
29
+ op: "in";
30
+ value: unknown;
31
+ }[];
32
+ export declare function resolveProjectionFacetSpecs(facets: readonly ListFacetSpec[] | undefined, translate: Translate, featureName: string): ResolvedFacetSpec[];
33
+ export type ResolvedDateRangeFacet = {
34
+ readonly field: string;
35
+ readonly label: string;
36
+ readonly fromParam: string;
37
+ readonly toParam: string;
38
+ };
39
+ export type DateRangeBound = "from" | "to";
40
+ export type DateRangeValue = {
41
+ readonly from: string;
42
+ readonly to: string;
43
+ };
44
+ export declare function resolveDateRangeFacets(facets: readonly ListFacetSpec[] | undefined, translate: Translate): ResolvedDateRangeFacet[];
45
+ export declare function dateRangeUrlField(field: string, bound: DateRangeBound): string;
46
+ export declare function readDateRange(urlFilters: Readonly<Record<string, readonly string[]>>, field: string): DateRangeValue;
47
+ export declare function clampDateRange(current: DateRangeValue, bound: DateRangeBound, value: string): DateRangeValue;
48
+ export declare function buildDateRangePayload(specs: readonly ResolvedDateRangeFacet[], urlFilters: Readonly<Record<string, readonly string[]>>, timeZone: string): Record<string, string>;
@@ -0,0 +1,185 @@
1
+ // Split from kumiko-screen.tsx: related-list-section.tsx renders through
2
+ // kumiko-screen.tsx, so importing this back from there would be a require cycle.
3
+ import { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
4
+ import { Temporal } from "temporal-polyfill";
5
+ // Fills in a "reference" spec's initially-empty `options` from the
6
+ // field-keyed options ReferenceFacetBridges resolved, shared by every caller.
7
+ export function mergeReferenceFacetOptions(specs, optionsByField) {
8
+ return specs.map((spec) => spec.reference !== undefined
9
+ ? { ...spec, options: optionsByField[spec.field] ?? spec.options }
10
+ : spec);
11
+ }
12
+ export function buildFilterFacets(specs) {
13
+ return specs.map((spec) => ({ field: spec.field, label: spec.label, options: spec.options }));
14
+ }
15
+ // User-selected faceted filters from URL-state → payload.filters. Boolean
16
+ // fields coerce "true"/"false" strings to real booleans (DB column is
17
+ // boolean); everything else stays string[] under op:"in" (multi-select
18
+ // semantics). `typeOf` resolves a field to its known type string —
19
+ // undefined means "unknown field", so it's dropped (typo-safe: a stale/
20
+ // hand-crafted URL param for an undeclared field never reaches the
21
+ // server). Deliberately NOT gated on the field being a *facet* — entityList
22
+ // passes through any field present in entity.fields, matching its
23
+ // pre-fw#2224 behavior; only the boolean-coercion branch cares about type.
24
+ function isFacetI18nKey(label) {
25
+ return !/\s/.test(label) && (label.includes(".") || label.includes(":"));
26
+ }
27
+ export function buildFilterPayload(urlFilters, typeOf) {
28
+ const out = [];
29
+ for (const [field, values] of Object.entries(urlFilters)) {
30
+ if (values.length === 0)
31
+ continue;
32
+ // `id` is a base column (not a declared facet), allowed as an id-set
33
+ // filter so a header-slot control — e.g. the tags TagFilter — can narrow
34
+ // ANY list to a resolved set of row ids without the host declaring a facet.
35
+ if (field === "id") {
36
+ out.push({ field, op: "in", value: values });
37
+ continue;
38
+ }
39
+ const type = typeOf(field);
40
+ if (type === undefined)
41
+ continue;
42
+ const value = type === "boolean" ? values.map((v) => v === "true") : values;
43
+ out.push({ field, op: "in", value });
44
+ }
45
+ return out;
46
+ }
47
+ // projectionList/relatedList adapter — neither has an entity/i18n convention
48
+ // to derive labels from, so ListFacetSpec carries every label explicitly
49
+ // (fw#2224). Labels may be raw display strings or i18n keys; run them
50
+ // through translate like entity facets (passthrough when the key is missing).
51
+ // `featureName` resolves a reference facet's `entity` target the same way
52
+ // ListColumnSpec.refEntity does (same-feature short name, or "feature:entity").
53
+ export function resolveProjectionFacetSpecs(facets, translate, featureName) {
54
+ if (facets === undefined)
55
+ return [];
56
+ const tr = (label) => (isFacetI18nKey(label) ? translate(label) : label);
57
+ // dateRange carries no option list and no `filters` entry — it resolves
58
+ // through resolveDateRangeFacets into its own toolbar control instead.
59
+ return facets.flatMap((facet) => {
60
+ if (facet.type === "dateRange")
61
+ return [];
62
+ if (facet.type === "select") {
63
+ return [
64
+ {
65
+ field: facet.field,
66
+ type: "select",
67
+ label: tr(facet.label),
68
+ options: facet.options.map((opt) => ({
69
+ value: opt.value,
70
+ label: tr(opt.label),
71
+ })),
72
+ },
73
+ ];
74
+ }
75
+ if (facet.type === "boolean") {
76
+ return [
77
+ {
78
+ field: facet.field,
79
+ type: "boolean",
80
+ label: tr(facet.label),
81
+ options: [
82
+ { value: "true", label: tr(facet.trueLabel) },
83
+ { value: "false", label: tr(facet.falseLabel) },
84
+ ],
85
+ },
86
+ ];
87
+ }
88
+ const target = parseRefTarget(facet.entity, featureName);
89
+ return [
90
+ {
91
+ field: facet.field,
92
+ type: "reference",
93
+ label: tr(facet.label),
94
+ options: [],
95
+ reference: {
96
+ refEntity: target.entityName,
97
+ refFeature: target.featureName,
98
+ labelField: facet.labelField ?? "id",
99
+ },
100
+ },
101
+ ];
102
+ });
103
+ }
104
+ export function resolveDateRangeFacets(facets, translate) {
105
+ if (facets === undefined)
106
+ return [];
107
+ return facets.flatMap((facet) => facet.type === "dateRange"
108
+ ? [
109
+ {
110
+ field: facet.field,
111
+ label: isFacetI18nKey(facet.label) ? translate(facet.label) : facet.label,
112
+ fromParam: facet.params.from,
113
+ toParam: facet.params.to,
114
+ },
115
+ ]
116
+ : []);
117
+ }
118
+ // The bounds live in the same `<screenId>.f.<key>` URL namespace the option
119
+ // facets use, so setFilter's page-reset and clearFilters cover them for free.
120
+ // buildFilterPayload drops them again: a dotted key resolves to no facet
121
+ // type, which is its "unknown field" case.
122
+ export function dateRangeUrlField(field, bound) {
123
+ return `${field}.${bound}`;
124
+ }
125
+ export function readDateRange(urlFilters, field) {
126
+ return {
127
+ from: urlFilters[dateRangeUrlField(field, "from")]?.[0] ?? "",
128
+ to: urlFilters[dateRangeUrlField(field, "to")]?.[0] ?? "",
129
+ };
130
+ }
131
+ // Keeps from <= to by pushing the opposite bound along, so an inverted range
132
+ // can't reach the server and trip a handler's `from <= to` refine (a 422 the
133
+ // user would see as a broken list, not as a correction).
134
+ export function clampDateRange(current, bound, value) {
135
+ if (bound === "from") {
136
+ const to = current.to !== "" && value !== "" && value > current.to ? value : current.to;
137
+ return { from: value, to };
138
+ }
139
+ const from = current.from !== "" && value !== "" && value < current.from ? value : current.from;
140
+ return { from, to: value };
141
+ }
142
+ // A calendar date covers a whole day in the viewer's zone: "from the 14th"
143
+ // starts at that day's first instant, "to the 14th" includes everything up to
144
+ // its last. Going through the next day's start keeps both ends right across a
145
+ // DST boundary, where the day is not 24h long.
146
+ function dayBoundInstant(date, bound, timeZone) {
147
+ let plainDate;
148
+ try {
149
+ plainDate = Temporal.PlainDate.from(date);
150
+ }
151
+ catch {
152
+ // Half-typed or hand-crafted URL value — drop the bound rather than send
153
+ // something the handler's ISO-datetime schema would reject.
154
+ return null;
155
+ }
156
+ const start = plainDate.toZonedDateTime(timeZone).toInstant();
157
+ return bound === "from"
158
+ ? start.toString()
159
+ : plainDate
160
+ .add({ days: 1 })
161
+ .toZonedDateTime(timeZone)
162
+ .toInstant()
163
+ .subtract({
164
+ nanoseconds: 1,
165
+ })
166
+ .toString();
167
+ }
168
+ export function buildDateRangePayload(specs, urlFilters, timeZone) {
169
+ const out = {};
170
+ for (const spec of specs) {
171
+ const range = readDateRange(urlFilters, spec.field);
172
+ for (const [bound, param] of [
173
+ ["from", spec.fromParam],
174
+ ["to", spec.toParam],
175
+ ]) {
176
+ const value = range[bound];
177
+ if (value === "")
178
+ continue;
179
+ const instant = dayBoundInstant(value, bound, timeZone);
180
+ if (instant !== null)
181
+ out[param] = instant;
182
+ }
183
+ }
184
+ return out;
185
+ }
@@ -0,0 +1,61 @@
1
+ import { type ReactNode } from "react";
2
+ import type { FeatureSchema } from "./feature-schema.js";
3
+ export type NavRoute = {
4
+ readonly workspaceId?: string;
5
+ readonly screenId: string;
6
+ readonly entityId?: string;
7
+ };
8
+ export type ScreenTarget = {
9
+ readonly workspaceId?: string;
10
+ readonly screenId: string;
11
+ readonly entityId?: string;
12
+ };
13
+ export type ObjectTarget = {
14
+ readonly workspaceId?: string;
15
+ readonly entity: string;
16
+ readonly id: string;
17
+ };
18
+ export type NavTarget = ScreenTarget | ObjectTarget;
19
+ export type NavApi = {
20
+ /** Current route — `undefined` when the URL is at the root / there's
21
+ * no route selected. Caller's initial fallback kicks in then. */
22
+ readonly route: NavRoute | undefined;
23
+ /** Push a new route. Platform-specific Impl writes to
24
+ * history/stack and notifies subscribers. */
25
+ readonly navigate: (target: NavTarget) => void;
26
+ /** Replace the current route in place. Same effect as navigate from
27
+ * the user's perspective, but doesn't add a history entry — used for
28
+ * mount-time URL fills (e.g. WorkspaceShell defaulting to `/admin/x`
29
+ * when the user typed `/`). Browser Back must take the user out of
30
+ * the app, not back to the original empty path. */
31
+ readonly replace: (target: NavTarget) => void;
32
+ /** Build the href a click on {target} would produce. Used by
33
+ * platform-specific Link-Komponenten (Web: `<a href>`; Native
34
+ * typically doesn't need this). */
35
+ readonly hrefFor: (target: NavTarget) => string;
36
+ /** Lese-Snapshot der aktuellen Search-Params (Browser: ?key=value-
37
+ * Pairs nach dem Pfad). Native-Impls die kein URL-Konzept haben
38
+ * liefern ein leeres Object. Wert ist ein Plain-Record (kein Map)
39
+ * damit React-Subscribers shallow-compare können. */
40
+ readonly searchParams: Readonly<Record<string, string>>;
41
+ /** Mergt Updates in die aktuellen Search-Params. Wert `null` löscht
42
+ * den Key. Ändert NICHT den Pfad. Plattform-Impls nutzen
43
+ * history.replaceState (kein Push) — Sort/Filter-Toggles sollen
44
+ * nicht die Back-Navigation fluten. Native-Impls können no-op'en
45
+ * wenn das Konzept nicht existiert. */
46
+ readonly setSearchParams: (updates: Readonly<Record<string, string | null>>) => void;
47
+ };
48
+ export declare function parsePath(pathname: string, hasWorkspaces?: boolean): NavRoute | undefined;
49
+ export declare function formatPath(target: ScreenTarget): string;
50
+ /** Non-throwing existence check for findDetailForScreen — lets a caller pick
51
+ * a different default (fw#2164's row-click fallback) instead of resolving. */
52
+ export declare function hasDetailScreen(features: readonly FeatureSchema[], entity: string): boolean;
53
+ export declare function resolveTarget(features: readonly FeatureSchema[], target: NavTarget): ScreenTarget;
54
+ export type NavProviderProps = {
55
+ readonly children: ReactNode;
56
+ readonly value: NavApi;
57
+ };
58
+ export declare function NavProvider({ children, value }: NavProviderProps): ReactNode;
59
+ export declare function useNavigateWithInitialValues(): (target: ScreenTarget, initialValues: Readonly<Record<string, unknown>>) => void;
60
+ export declare function useInitialValuesHandoff(screenId: string): Readonly<Record<string, unknown>> | undefined;
61
+ export declare function useNav(): NavApi;
@@ -0,0 +1,139 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useState } from "react";
3
+ import { lastSegment } from "./qn.js";
4
+ // Pfad-Format:
5
+ // ohne workspaces: `/task-list`, `/task-edit/abc-123`, `/` → undefined
6
+ // mit workspaces: `/admin/task-list`, `/admin/task-edit/abc`,
7
+ // `/admin` → workspace-only (no screen yet),
8
+ // `/` → undefined.
9
+ // Alles nach den ersten 2/3 Segmenten wird ignoriert — kumikos
10
+ // Navigation-Grammatik nested nicht weiter. Nested-Routes wäre eine
11
+ // Spec-Änderung, nicht ein URL-Shape-Unfall.
12
+ export function parsePath(pathname, hasWorkspaces) {
13
+ const parts = pathname.split("/").filter((p) => p !== "");
14
+ if (hasWorkspaces === true) {
15
+ const [workspaceId, screenId, entityId] = parts;
16
+ if (workspaceId === undefined || workspaceId === "")
17
+ return undefined;
18
+ if (screenId === undefined || screenId === "") {
19
+ // Workspace-only URL ("/admin") — caller resolves the default screen
20
+ // for that workspace. We carry workspaceId WITHOUT a screen so the
21
+ // shell can branch instead of making something up.
22
+ return { workspaceId, screenId: "" };
23
+ }
24
+ return {
25
+ workspaceId,
26
+ screenId,
27
+ ...(entityId !== undefined && { entityId }),
28
+ };
29
+ }
30
+ const [screenId, entityId] = parts;
31
+ if (screenId === undefined || screenId === "")
32
+ return undefined;
33
+ return { screenId, ...(entityId !== undefined && { entityId }) };
34
+ }
35
+ export function formatPath(target) {
36
+ // Workspace-Mode: prefix the workspace short id. Order matters —
37
+ // workspace before screen mirrors parsePath's segment order.
38
+ const segments = [];
39
+ if (target.workspaceId !== undefined)
40
+ segments.push(target.workspaceId);
41
+ segments.push(target.screenId);
42
+ if (target.entityId !== undefined)
43
+ segments.push(target.entityId);
44
+ return `/${segments.join("/")}`;
45
+ }
46
+ // Shared entity→detailFor-screen lookup — resolveTarget's ObjectTarget
47
+ // branch and hasDetailScreen (create-app's row-click default, fw#2164) both
48
+ // need "is there a detail screen for this entity", one to resolve it, the
49
+ // other to just check before falling back to a different default.
50
+ function findDetailForScreen(features, entity) {
51
+ for (const feature of features) {
52
+ for (const screen of feature.screens) {
53
+ if (screen.detailFor === entity)
54
+ return { featureName: feature.featureName, screenId: screen.id };
55
+ }
56
+ }
57
+ return undefined;
58
+ }
59
+ /** Non-throwing existence check for findDetailForScreen — lets a caller pick
60
+ * a different default (fw#2164's row-click fallback) instead of resolving. */
61
+ export function hasDetailScreen(features, entity) {
62
+ return findDetailForScreen(features, entity) !== undefined;
63
+ }
64
+ // Resolves a NavTarget to the ScreenTarget form navigate/replace/hrefFor
65
+ // operate on. Pure — no context, no I/O — so callers can use it outside
66
+ // React too (see resolve-at-NavApi-build alternative in renderer-web).
67
+ export function resolveTarget(features, target) {
68
+ if ("screenId" in target)
69
+ return target;
70
+ const found = findDetailForScreen(features, target.entity);
71
+ if (found !== undefined) {
72
+ // Short form (globally unique per validateScreenShortIdCollisions) —
73
+ // ScreenTarget.screenId round-trips through formatPath/parsePath as a
74
+ // short id; qualifying it here double-qualifies at RoutedScreen.
75
+ return {
76
+ ...(target.workspaceId !== undefined && { workspaceId: target.workspaceId }),
77
+ screenId: found.screenId,
78
+ entityId: target.id,
79
+ };
80
+ }
81
+ throw new Error(`resolveTarget: no detail screen for entity "${target.entity}". Add detailFor: "${target.entity}" to the screen that shows it.`);
82
+ }
83
+ // Context + Hook. Default ist `undefined` damit fehlender Provider
84
+ // laut kracht statt ein silent-no-op NavApi mit toten navigate()
85
+ // Aufrufen anzubieten.
86
+ const NavContext = createContext(undefined);
87
+ const InitialValuesHandoffContext = createContext(undefined);
88
+ export function NavProvider({ children, value }) {
89
+ const [handoffSlot] = useState(() => ({ current: undefined }));
90
+ const routeScreenId = value.route?.screenId;
91
+ // An offer whose form never mounted (e.g. already on that screen) must not
92
+ // resurface on a later, unrelated visit — drop it once the route moves elsewhere.
93
+ useEffect(() => {
94
+ const pending = handoffSlot.current;
95
+ if (pending === undefined || routeScreenId === undefined)
96
+ return;
97
+ if (lastSegment(routeScreenId) !== pending.screenId)
98
+ handoffSlot.current = undefined;
99
+ }, [handoffSlot, routeScreenId]);
100
+ return (_jsx(NavContext.Provider, { value: value, children: _jsx(InitialValuesHandoffContext.Provider, { value: handoffSlot, children: children }) }));
101
+ }
102
+ // Navigates to a form screen (actionForm, secretMint, entityEdit-create) with
103
+ // initial values carried in memory — never in the URL, so a link cannot
104
+ // forge them and they are not limited to the screen's urlPrefillFields.
105
+ // sensitive and password fields are still never prefilled.
106
+ export function useNavigateWithInitialValues() {
107
+ const nav = useNav();
108
+ const handoffSlot = useContext(InitialValuesHandoffContext);
109
+ return useCallback((target, initialValues) => {
110
+ if (handoffSlot !== undefined) {
111
+ handoffSlot.current = { screenId: lastSegment(target.screenId), values: initialValues };
112
+ }
113
+ nav.navigate(target);
114
+ }, [nav, handoffSlot]);
115
+ }
116
+ export function useInitialValuesHandoff(screenId) {
117
+ const handoffSlot = useContext(InitialValuesHandoffContext);
118
+ const [values] = useState(() => {
119
+ const pending = handoffSlot?.current;
120
+ return pending !== undefined && pending.screenId === lastSegment(screenId)
121
+ ? pending.values
122
+ : undefined;
123
+ });
124
+ useEffect(() => {
125
+ if (handoffSlot !== undefined &&
126
+ values !== undefined &&
127
+ handoffSlot.current?.values === values) {
128
+ handoffSlot.current = undefined;
129
+ }
130
+ }, [handoffSlot, values]);
131
+ return values;
132
+ }
133
+ export function useNav() {
134
+ const api = useContext(NavContext);
135
+ if (api === undefined) {
136
+ throw new Error("useNav: no <NavProvider> mounted above this component. Plattform-Packages (z.B. @cosmicdrift/kumiko-renderer-web) liefern eine Default-Impl über createKumikoApp.");
137
+ }
138
+ return api;
139
+ }
@@ -0,0 +1,8 @@
1
+ import type { EditLayout, EntityDefinition, EntityEditScreenDefinition, ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ /** Minimale EntityDefinition aus den Layout-Feldern: jedes Feld ein Text-
3
+ * Feld — computeEditViewModel liest nur `fields[<f>].type`, Text reicht für
4
+ * eine reine Anzeige (kein Select/Number-spezifisches Rendering nötig). */
5
+ export declare function synthesizeProjectionDetailEntity(layout: EditLayout): EntityDefinition;
6
+ /** Wandelt ein ProjectionDetailScreenDefinition in die EntityEditScreen-Shape
7
+ * die RenderEdit erwartet — mit jedem Feld hart auf readOnly:true erzwungen. */
8
+ export declare function synthesizeProjectionDetailScreen(screen: ProjectionDetailScreenDefinition): EntityEditScreenDefinition;
@@ -0,0 +1,71 @@
1
+ // Shim für ProjectionDetail-Renderer (analog zu projection-list-shim.ts /
2
+ // config-edit-shim.ts).
3
+ //
4
+ // RenderEdit verlangt ein `entity: EntityDefinition` + ein
5
+ // `screen: EntityEditScreenDefinition`-Pair, nutzt davon aber nur
6
+ // `entity.fields` (Feld-Typ für den Input-Renderer) und `screen.layout`
7
+ // (welche Felder in welchen Sections). Ein projectionDetail-Screen ist
8
+ // query-getrieben und hat KEINE Entity — die zwei Helper hier shapen den
9
+ // Input ad-hoc um, damit RenderEdit reused werden kann.
10
+ //
11
+ // Der strukturelle Read-Only-Beweis liegt in synthesizeProjectionDetailScreen:
12
+ // JEDES Feld wird hart auf readOnly:true gesetzt (nicht nur was der Author im
13
+ // Layout gesetzt hat) — hasEditableSection() liest genau dieses Flag und
14
+ // blendet den Save-Button aus, wenn kein Feld editierbar ist. Der Author kann
15
+ // diese Garantie nicht versehentlich umgehen.
16
+ //
17
+ // Selbe Schulden-Reservation wie bei den anderen Shims: greift RenderEdit
18
+ // künftig auf weitere EntityDefinition-Felder (transitions, idType) zu, oder
19
+ // cross-referenziert ein Boot-Validator die schema.entities-Map, brechen die
20
+ // Type-Lies hier silent — dann ist Zeit für eine echte RenderProjectionDetail-
21
+ // Komponente.
22
+ import { isFieldsEditSection, normalizeEditField, PROJECTION_DETAIL_ENTITY as PROJECTION_DETAIL_PSEUDO_ENTITY, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
23
+ /** Minimale EntityDefinition aus den Layout-Feldern: jedes Feld ein Text-
24
+ * Feld — computeEditViewModel liest nur `fields[<f>].type`, Text reicht für
25
+ * eine reine Anzeige (kein Select/Number-spezifisches Rendering nötig). */
26
+ export function synthesizeProjectionDetailEntity(layout) {
27
+ const fields = {};
28
+ for (const section of layout.sections) {
29
+ // relatedList and extension sections carry no `fields` to synthesize.
30
+ if (!isFieldsEditSection(section))
31
+ continue;
32
+ for (const spec of sectionFieldSpecs(section)) {
33
+ fields[normalizeEditField(spec).field] = { type: "text" };
34
+ }
35
+ }
36
+ return { fields };
37
+ }
38
+ /** Wandelt ein ProjectionDetailScreenDefinition in die EntityEditScreen-Shape
39
+ * die RenderEdit erwartet — mit jedem Feld hart auf readOnly:true erzwungen. */
40
+ export function synthesizeProjectionDetailScreen(screen) {
41
+ const forceReadOnly = (spec) => ({ ...normalizeEditField(spec), readOnly: true });
42
+ const sections = screen.layout.sections.map((section) => {
43
+ // relatedList and extension sections pass through unchanged.
44
+ if (!isFieldsEditSection(section))
45
+ return section;
46
+ return {
47
+ ...section,
48
+ // kumiko-lint-ignore section-fields-raw rewrites both sources in place — the groups branch below does the same for groups[].fields
49
+ fields: section.fields.map(forceReadOnly),
50
+ ...(section.groups !== undefined && {
51
+ groups: section.groups.map((group) => ({
52
+ ...group,
53
+ fields: group.fields.map(forceReadOnly),
54
+ })),
55
+ }),
56
+ };
57
+ });
58
+ return {
59
+ id: screen.id,
60
+ type: "entityEdit",
61
+ entity: PROJECTION_DETAIL_PSEUDO_ENTITY,
62
+ layout: { sections, ...(screen.layout.width !== undefined && { width: screen.layout.width }) },
63
+ allowCreate: false,
64
+ allowDelete: false,
65
+ ...(screen.description !== undefined && { description: screen.description }),
66
+ ...(screen.fieldLabels !== undefined && { fieldLabels: screen.fieldLabels }),
67
+ ...(screen.slots !== undefined && { slots: screen.slots }),
68
+ ...(screen.access !== undefined && { access: screen.access }),
69
+ ...(screen.fillHeight !== undefined && { fillHeight: screen.fillHeight }),
70
+ };
71
+ }
@@ -0,0 +1,13 @@
1
+ import type { EntityDefinition, EntityListScreenDefinition, ListColumnSpec, ProjectionListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ /** Minimal EntityDefinition from the column list: every field is a text
3
+ * field. `sortable` applies uniformly to all columns — buildAppSchema
4
+ * derives it from the query's Zod schema (`screen.sortable`, fw#2165); the
5
+ * query itself has no per-field server-sort guarantee.
6
+ * computeListViewModel only reads `fields[<col>].type` — text is enough,
7
+ * presentation comes from the column renderer + explicit label. */
8
+ export declare function synthesizeProjectionEntity(columns: readonly ListColumnSpec[], sortable: boolean): EntityDefinition;
9
+ /** Wandelt ein ProjectionListScreenDefinition in die EntityListScreen-Shape die
10
+ * RenderList erwartet. `type` = "entityList" + Pseudo-Entity halten den Type-
11
+ * Constraint; RenderList branched nicht auf `type` und liest `entity` nur für
12
+ * Fehlermeldungen. Die List-Felder werden 1:1 durchgereicht. */
13
+ export declare function synthesizeProjectionScreen(screen: ProjectionListScreenDefinition): EntityListScreenDefinition;
@@ -13,58 +13,43 @@
13
13
  // derivedFields) oder cross-referenziert ein Boot-Validator die schema.entities-
14
14
  // Map, brechen die Type-Lies hier silent — dann ist Zeit für eine echte
15
15
  // query-native ListView-Komponente.
16
-
17
- import type {
18
- EntityDefinition,
19
- EntityListScreenDefinition,
20
- ListColumnSpec,
21
- ProjectionListScreenDefinition,
22
- } from "@cosmicdrift/kumiko-framework/ui-types";
23
16
  import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
24
-
25
17
  const PROJECTION_PSEUDO_ENTITY = "__projection__";
26
-
27
18
  /** Minimal EntityDefinition from the column list: every field is a text
28
19
  * field. `sortable` applies uniformly to all columns — buildAppSchema
29
20
  * derives it from the query's Zod schema (`screen.sortable`, fw#2165); the
30
21
  * query itself has no per-field server-sort guarantee.
31
22
  * computeListViewModel only reads `fields[<col>].type` — text is enough,
32
23
  * presentation comes from the column renderer + explicit label. */
33
- export function synthesizeProjectionEntity(
34
- columns: readonly ListColumnSpec[],
35
- sortable: boolean,
36
- ): EntityDefinition {
37
- const fields: Record<string, { type: "text"; sortable: boolean }> = {};
38
- for (const col of columns) {
39
- fields[normalizeListColumn(col).field] = { type: "text", sortable };
40
- }
41
- return { fields } as unknown as EntityDefinition;
24
+ export function synthesizeProjectionEntity(columns, sortable) {
25
+ const fields = {};
26
+ for (const col of columns) {
27
+ fields[normalizeListColumn(col).field] = { type: "text", sortable };
28
+ }
29
+ return { fields };
42
30
  }
43
-
44
31
  /** Wandelt ein ProjectionListScreenDefinition in die EntityListScreen-Shape die
45
32
  * RenderList erwartet. `type` = "entityList" + Pseudo-Entity halten den Type-
46
33
  * Constraint; RenderList branched nicht auf `type` und liest `entity` nur für
47
34
  * Fehlermeldungen. Die List-Felder werden 1:1 durchgereicht. */
48
- export function synthesizeProjectionScreen(
49
- screen: ProjectionListScreenDefinition,
50
- ): EntityListScreenDefinition {
51
- return {
52
- id: screen.id,
53
- type: "entityList",
54
- entity: PROJECTION_PSEUDO_ENTITY,
55
- columns: screen.columns,
56
- ...(screen.rowRenderer !== undefined && { rowRenderer: screen.rowRenderer }),
57
- ...(screen.cardRenderer !== undefined && { cardRenderer: screen.cardRenderer }),
58
- ...(screen.rowActions !== undefined && { rowActions: screen.rowActions }),
59
- ...(screen.toolbarActions !== undefined && { toolbarActions: screen.toolbarActions }),
60
- ...(screen.pagination !== undefined && { pagination: screen.pagination }),
61
- ...(screen.pageSize !== undefined && { pageSize: screen.pageSize }),
62
- ...(screen.defaultSort !== undefined && { defaultSort: screen.defaultSort }),
63
- ...(screen.searchable !== undefined && { searchable: screen.searchable }),
64
- ...(screen.createLabel !== undefined && { createLabel: screen.createLabel }),
65
- ...(screen.searchPlaceholder !== undefined && { searchPlaceholder: screen.searchPlaceholder }),
66
- ...(screen.slots !== undefined && { slots: screen.slots }),
67
- ...(screen.access !== undefined && { access: screen.access }),
68
- ...(screen.fillHeight !== undefined && { fillHeight: screen.fillHeight }),
69
- };
35
+ export function synthesizeProjectionScreen(screen) {
36
+ return {
37
+ id: screen.id,
38
+ type: "entityList",
39
+ entity: PROJECTION_PSEUDO_ENTITY,
40
+ columns: screen.columns,
41
+ ...(screen.rowRenderer !== undefined && { rowRenderer: screen.rowRenderer }),
42
+ ...(screen.cardRenderer !== undefined && { cardRenderer: screen.cardRenderer }),
43
+ ...(screen.rowActions !== undefined && { rowActions: screen.rowActions }),
44
+ ...(screen.toolbarActions !== undefined && { toolbarActions: screen.toolbarActions }),
45
+ ...(screen.pagination !== undefined && { pagination: screen.pagination }),
46
+ ...(screen.pageSize !== undefined && { pageSize: screen.pageSize }),
47
+ ...(screen.defaultSort !== undefined && { defaultSort: screen.defaultSort }),
48
+ ...(screen.searchable !== undefined && { searchable: screen.searchable }),
49
+ ...(screen.createLabel !== undefined && { createLabel: screen.createLabel }),
50
+ ...(screen.searchPlaceholder !== undefined && { searchPlaceholder: screen.searchPlaceholder }),
51
+ ...(screen.slots !== undefined && { slots: screen.slots }),
52
+ ...(screen.access !== undefined && { access: screen.access }),
53
+ ...(screen.fillHeight !== undefined && { fillHeight: screen.fillHeight }),
54
+ };
70
55
  }
@@ -0,0 +1,7 @@
1
+ export declare function lastSegment(qn: string): string;
2
+ /**
3
+ * Client-safe copy of framework `toKebab` (engine/qualified-name.ts).
4
+ * Must stay in sync — do NOT import from `@cosmicdrift/kumiko-framework/engine`
5
+ * here: that barrel pulls server deps into the browser bundle.
6
+ */
7
+ export declare function toKebab(input: string): string;