@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.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 (230) hide show
  1. package/dist/app/projection-detail-body.js +59 -394
  2. package/dist/app/projection-detail-header-model.d.ts +11 -0
  3. package/dist/app/projection-detail-header-model.js +33 -0
  4. package/dist/app/projection-detail-header-region.d.ts +28 -0
  5. package/dist/app/projection-detail-header-region.js +81 -0
  6. package/dist/app/projection-detail-header.d.ts +29 -0
  7. package/dist/app/projection-detail-header.js +84 -0
  8. package/dist/app/projection-detail-navigation.d.ts +13 -0
  9. package/dist/app/projection-detail-navigation.js +91 -0
  10. package/dist/app/projection-detail-states.d.ts +22 -0
  11. package/dist/app/projection-detail-states.js +26 -0
  12. package/dist/app/projection-drawer-host.d.ts +12 -0
  13. package/dist/app/projection-drawer-host.js +10 -0
  14. package/dist/app/projection-list-body.d.ts +1 -1
  15. package/dist/app/projection-list-body.js +69 -189
  16. package/dist/app/projection-list-render-props.d.ts +56 -0
  17. package/dist/app/projection-list-render-props.js +47 -0
  18. package/dist/app/projection-list-render.d.ts +26 -0
  19. package/dist/app/projection-list-render.js +20 -0
  20. package/dist/app/projection-list-row-click.d.ts +12 -0
  21. package/dist/app/projection-list-row-click.js +14 -0
  22. package/dist/app/use-projection-detail-actions.d.ts +35 -0
  23. package/dist/app/use-projection-detail-actions.js +156 -0
  24. package/dist/app/use-projection-detail-data.d.ts +19 -0
  25. package/dist/app/use-projection-detail-data.js +47 -0
  26. package/dist/app/use-projection-detail-layout.d.ts +17 -0
  27. package/dist/app/use-projection-detail-layout.js +23 -0
  28. package/dist/app/use-projection-list-actions.d.ts +31 -0
  29. package/dist/app/use-projection-list-actions.js +73 -0
  30. package/dist/app/use-projection-list-data.d.ts +20 -0
  31. package/dist/app/use-projection-list-data.js +26 -0
  32. package/dist/app/use-projection-list-definitions.d.ts +11 -0
  33. package/dist/app/use-projection-list-definitions.js +20 -0
  34. package/dist/app/use-projection-list-query.d.ts +28 -0
  35. package/dist/app/use-projection-list-query.js +83 -0
  36. package/dist/components/render-edit-banners.d.ts +12 -0
  37. package/dist/components/render-edit-banners.js +90 -0
  38. package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
  39. package/dist/components/render-edit-draft-save-helpers.js +42 -0
  40. package/dist/components/render-edit-field-grid.d.ts +4 -0
  41. package/dist/components/render-edit-field-grid.js +40 -0
  42. package/dist/components/render-edit-fields-section.d.ts +4 -0
  43. package/dist/components/render-edit-fields-section.js +76 -0
  44. package/dist/components/render-edit-form-actions.d.ts +4 -0
  45. package/dist/components/render-edit-form-actions.js +66 -0
  46. package/dist/components/render-edit-header-actions.d.ts +11 -0
  47. package/dist/components/render-edit-header-actions.js +120 -0
  48. package/dist/components/render-edit-layout-flags.d.ts +29 -0
  49. package/dist/components/render-edit-layout-flags.js +106 -0
  50. package/dist/components/render-edit-layout.d.ts +4 -0
  51. package/dist/components/render-edit-layout.js +79 -0
  52. package/dist/components/render-edit-render-model.d.ts +63 -0
  53. package/dist/components/render-edit-render-model.js +14 -0
  54. package/dist/components/render-edit-section-kinds.d.ts +4 -0
  55. package/dist/components/render-edit-section-kinds.js +115 -0
  56. package/dist/components/render-edit-submit-result.d.ts +8 -0
  57. package/dist/components/render-edit-submit-result.js +77 -0
  58. package/dist/components/render-edit-submit.d.ts +7 -0
  59. package/dist/components/render-edit-submit.js +113 -0
  60. package/dist/components/render-edit-titles.d.ts +17 -0
  61. package/dist/components/render-edit-titles.js +99 -0
  62. package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
  63. package/dist/components/render-edit-wizard-navigation.js +115 -0
  64. package/dist/components/render-edit.d.ts +2 -2
  65. package/dist/components/render-edit.js +63 -1128
  66. package/dist/components/render-field-choice-inputs.d.ts +7 -0
  67. package/dist/components/render-field-choice-inputs.js +52 -0
  68. package/dist/components/render-field-control.d.ts +12 -0
  69. package/dist/components/render-field-control.js +93 -0
  70. package/dist/components/render-field-display-text.d.ts +4 -0
  71. package/dist/components/render-field-display-text.js +48 -0
  72. package/dist/components/render-field-fill-action.d.ts +13 -0
  73. package/dist/components/render-field-fill-action.js +63 -0
  74. package/dist/components/render-field-input-shared.d.ts +39 -0
  75. package/dist/components/render-field-input-shared.js +20 -0
  76. package/dist/components/render-field-input.d.ts +3 -0
  77. package/dist/components/render-field-input.js +34 -0
  78. package/dist/components/render-field-meta.d.ts +6 -0
  79. package/dist/components/render-field-meta.js +51 -0
  80. package/dist/components/render-field-read-only.d.ts +11 -0
  81. package/dist/components/render-field-read-only.js +75 -0
  82. package/dist/components/render-field-reference.d.ts +15 -0
  83. package/dist/components/render-field-reference.js +103 -0
  84. package/dist/components/render-field-scalar-inputs.d.ts +10 -0
  85. package/dist/components/render-field-scalar-inputs.js +35 -0
  86. package/dist/components/render-field-text-inputs.d.ts +3 -0
  87. package/dist/components/render-field-text-inputs.js +34 -0
  88. package/dist/components/render-field-values.d.ts +18 -0
  89. package/dist/components/render-field-values.js +111 -0
  90. package/dist/components/render-field-write-only.d.ts +9 -0
  91. package/dist/components/render-field-write-only.js +29 -0
  92. package/dist/components/render-field.d.ts +2 -2
  93. package/dist/components/render-field.js +39 -670
  94. package/dist/components/render-list-header-slot.d.ts +5 -0
  95. package/dist/components/render-list-header-slot.js +30 -0
  96. package/dist/components/render-list-reference-bridge.d.ts +3 -0
  97. package/dist/components/render-list-reference-bridge.js +25 -0
  98. package/dist/components/render-list-reference-columns.d.ts +25 -0
  99. package/dist/components/render-list-reference-columns.js +137 -0
  100. package/dist/components/render-list-table-props.d.ts +52 -0
  101. package/dist/components/render-list-table-props.js +53 -0
  102. package/dist/components/render-list-toolbar-model.d.ts +42 -0
  103. package/dist/components/render-list-toolbar-model.js +82 -0
  104. package/dist/components/render-list-toolbar.d.ts +38 -0
  105. package/dist/components/render-list-toolbar.js +40 -0
  106. package/dist/components/render-list-types.d.ts +113 -0
  107. package/dist/components/render-list-types.js +1 -0
  108. package/dist/components/render-list.d.ts +4 -113
  109. package/dist/components/render-list.js +91 -307
  110. package/dist/components/use-reference-input-state.d.ts +20 -0
  111. package/dist/components/use-reference-input-state.js +85 -0
  112. package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
  113. package/dist/components/use-render-edit-controlled-sync.js +26 -0
  114. package/dist/components/use-render-edit-draft-save.d.ts +29 -0
  115. package/dist/components/use-render-edit-draft-save.js +141 -0
  116. package/dist/components/use-render-edit-form.d.ts +10 -0
  117. package/dist/components/use-render-edit-form.js +101 -0
  118. package/dist/components/use-render-edit-sections.d.ts +15 -0
  119. package/dist/components/use-render-edit-sections.js +100 -0
  120. package/dist/components/use-render-edit-state.d.ts +65 -0
  121. package/dist/components/use-render-edit-state.js +158 -0
  122. package/dist/components/use-render-edit-steps.d.ts +13 -0
  123. package/dist/components/use-render-edit-steps.js +52 -0
  124. package/dist/components/use-render-list-columns.d.ts +18 -0
  125. package/dist/components/use-render-list-columns.js +41 -0
  126. package/dist/components/use-render-list-search.d.ts +4 -0
  127. package/dist/components/use-render-list-search.js +50 -0
  128. package/dist/primitives.d.ts +1 -1
  129. package/dist/web/app/dashboard-body.d.ts +1 -1
  130. package/dist/web/app/dashboard-body.js +42 -246
  131. package/dist/web/app/dashboard-header.d.ts +34 -0
  132. package/dist/web/app/dashboard-header.js +58 -0
  133. package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
  134. package/dist/web/app/dashboard-screen-controls.js +42 -0
  135. package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
  136. package/dist/web/app/dashboard-screen-tiles.js +42 -0
  137. package/dist/web/app/dashboard-visibility.d.ts +42 -0
  138. package/dist/web/app/dashboard-visibility.js +102 -0
  139. package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
  140. package/dist/web/app/use-dashboard-screen-state.js +69 -0
  141. package/dist/web/primitives/data-table/actions-column.js +6 -5
  142. package/dist/web/primitives/data-table/card-body.d.ts +6 -0
  143. package/dist/web/primitives/data-table/card-body.js +32 -0
  144. package/dist/web/primitives/data-table/card-row.d.ts +5 -0
  145. package/dist/web/primitives/data-table/card-row.js +31 -0
  146. package/dist/web/primitives/data-table/card-view.d.ts +3 -0
  147. package/dist/web/primitives/data-table/card-view.js +13 -0
  148. package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
  149. package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
  150. package/dist/web/primitives/data-table/data-cell-props.js +8 -0
  151. package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
  152. package/dist/web/primitives/data-table/row-render-parts.js +35 -0
  153. package/dist/web/primitives/data-table/table-body.d.ts +3 -0
  154. package/dist/web/primitives/data-table/table-body.js +32 -0
  155. package/dist/web/primitives/data-table/table-content.d.ts +3 -0
  156. package/dist/web/primitives/data-table/table-content.js +41 -0
  157. package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
  158. package/dist/web/primitives/data-table/table-data-cell.js +25 -0
  159. package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
  160. package/dist/web/primitives/data-table/table-header-row.js +21 -0
  161. package/dist/web/primitives/data-table/table-view.d.ts +3 -0
  162. package/dist/web/primitives/data-table/table-view.js +25 -0
  163. package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
  164. package/dist/web/primitives/data-table/toolbar.js +53 -0
  165. package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
  166. package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
  167. package/dist/web/primitives/data-table.d.ts +3 -4
  168. package/dist/web/primitives/data-table.js +32 -252
  169. package/dist/web/primitives/file-value-list.js +1 -1
  170. package/dist/web/primitives/form-footer-parts.d.ts +25 -0
  171. package/dist/web/primitives/form-footer-parts.js +55 -0
  172. package/dist/web/primitives/form.js +33 -43
  173. package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
  174. package/dist/web/primitives/input-choice-kinds.js +67 -0
  175. package/dist/web/primitives/input-file-kinds.d.ts +3 -0
  176. package/dist/web/primitives/input-file-kinds.js +6 -0
  177. package/dist/web/primitives/input-number-kinds.d.ts +4 -0
  178. package/dist/web/primitives/input-number-kinds.js +16 -0
  179. package/dist/web/primitives/input-optional-props.d.ts +20 -0
  180. package/dist/web/primitives/input-optional-props.js +24 -0
  181. package/dist/web/primitives/input-select-presentation.d.ts +13 -0
  182. package/dist/web/primitives/input-select-presentation.js +19 -0
  183. package/dist/web/primitives/input-shared.d.ts +22 -0
  184. package/dist/web/primitives/input-shared.js +10 -0
  185. package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
  186. package/dist/web/primitives/input-temporal-kinds.js +10 -0
  187. package/dist/web/primitives/input-text-kinds.d.ts +7 -0
  188. package/dist/web/primitives/input-text-kinds.js +33 -0
  189. package/dist/web/primitives/input.js +35 -161
  190. package/dist/web/widgets/chart-data.d.ts +35 -0
  191. package/dist/web/widgets/chart-data.js +37 -0
  192. package/dist/web/widgets/chart-markers.d.ts +26 -0
  193. package/dist/web/widgets/chart-markers.js +64 -0
  194. package/dist/web/widgets/chart-parts.d.ts +20 -0
  195. package/dist/web/widgets/chart-parts.js +15 -0
  196. package/dist/web/widgets/charts.d.ts +5 -78
  197. package/dist/web/widgets/charts.js +7 -485
  198. package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
  199. package/dist/web/widgets/drawer-panel-class.js +40 -0
  200. package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
  201. package/dist/web/widgets/drawer-resize-controls.js +12 -0
  202. package/dist/web/widgets/drawer-sizing.d.ts +19 -0
  203. package/dist/web/widgets/drawer-sizing.js +60 -0
  204. package/dist/web/widgets/drawer.js +17 -116
  205. package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
  206. package/dist/web/widgets/stacked-area-brush.js +91 -0
  207. package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
  208. package/dist/web/widgets/stacked-area-chart.js +45 -0
  209. package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
  210. package/dist/web/widgets/stacked-area-chrome.js +77 -0
  211. package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
  212. package/dist/web/widgets/stacked-area-geometry.js +83 -0
  213. package/dist/web/widgets/stacked-area-model.d.ts +11 -0
  214. package/dist/web/widgets/stacked-area-model.js +1 -0
  215. package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
  216. package/dist/web/widgets/stacked-area-plot.js +51 -0
  217. package/dist/web/widgets/stacked-area-props.d.ts +83 -0
  218. package/dist/web/widgets/stacked-area-props.js +16 -0
  219. package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
  220. package/dist/web/widgets/stacked-area-svg.js +38 -0
  221. package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
  222. package/dist/web/widgets/stacked-area-tooltip.js +86 -0
  223. package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
  224. package/dist/web/widgets/use-drawer-resize.js +52 -0
  225. package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
  226. package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
  227. package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
  228. package/dist/web/widgets/use-stacked-area-window.js +76 -0
  229. package/package.json +9 -9
  230. package/src/changes.json +54 -0
@@ -1,132 +1,24 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { isWriteFormEditSection } from "@cosmicdrift/kumiko-framework/ui-types";
3
- import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
4
- import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useState } from "react";
5
3
  import { RenderEdit } from "../components/render-edit.js";
6
- import { resolveRecordTitle } from "../components/render-edit-helpers.js";
7
4
  import { useOptionalDispatcher } from "../context/dispatcher-context.js";
8
5
  import { useUserRoles } from "../context/user-roles-context.js";
9
- import { rendererWarn } from "../dev-console.js";
10
- import { isSafeHref } from "../headless/index.js";
11
- import { useQuery } from "../hooks/use-query.js";
12
6
  import { useTranslation } from "../i18n.js";
13
- import { requestLeave } from "../leave-guard.js";
14
7
  import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header-slot.js";
15
- import { statusToneForOptionTone, statusToneForValue, usePrimitives, } from "../primitives.js";
8
+ import { usePrimitives } from "../primitives.js";
16
9
  import { screenFillsHeight } from "../screen-fills-height.js";
17
- import { DrawerHost, useDrawerAction } from "./action-form-body.js";
10
+ import { useDrawerAction } from "./action-form-body.js";
18
11
  import { useAppFeatures } from "./app-features-context.js";
19
12
  import { useClientActionRuntime } from "./client-actions.js";
20
- import { ErrorReference } from "./error-reference.js";
21
- import { HeaderActionsBar } from "./header-actions-bar.js";
22
13
  import { useNav } from "./nav.js";
23
- import { synthesizeProjectionDetailEntity, synthesizeProjectionDetailScreen, } from "./projection-detail-shim.js";
24
- import { lastSegment } from "./qn.js";
25
- import { QueryErrorBanner, QueryLoadingBanner } from "./query-state-banners.js";
26
- import { navigateToReturnOr, navigateWithReturnTo, useReturnHost, useReturnTarget, } from "./return-to.js";
27
- import { buildRecordActions, refetchAfterWrite, runProjectionRowNavigate } from "./row-actions.js";
28
- import { findEditScreenFor, navigateTargetAllows } from "./screen-access.js";
29
- import { findEntityListScreen, navigateAfterRedirect, RecordNotFoundState, } from "./screen-shared.js";
30
- const HEADER_CARD_DEFAULT_WIDTH = "4xl";
31
- // Reuses runProjectionRowNavigate (row-actions.ts) instead of re-deriving
32
- // navigate execution — a metric click has the same entity/screen/params
33
- // shape as a RowActionNavigate, minus the fields (id, label, style) that
34
- // function never reads.
35
- function runMetricNavigate(nav, navigate, record, host) {
36
- // tab alone (no screen/entity) stays on the current record and just
37
- // activates that tab — no route change, so runProjectionRowNavigate
38
- // (which always navigates) doesn't apply here.
39
- if (navigate.screen === undefined && navigate.entity === undefined) {
40
- const { tab } = navigate;
41
- if (tab !== undefined)
42
- requestLeave(() => nav.setSearchParams({ tab }));
43
- return;
44
- }
45
- const base = { kind: "navigate", id: "metric-navigate", label: "" };
46
- const action = navigate.entity !== undefined
47
- ? {
48
- ...base,
49
- entity: navigate.entity,
50
- ...(navigate.entityId !== undefined && { entityId: navigate.entityId }),
51
- ...(navigate.params !== undefined && { params: navigate.params }),
52
- ...(navigate.tab !== undefined && { tab: navigate.tab }),
53
- }
54
- : navigate.screen !== undefined
55
- ? {
56
- ...base,
57
- screen: navigate.screen,
58
- ...(navigate.entityId !== undefined && { entityId: navigate.entityId }),
59
- ...(navigate.params !== undefined && { params: navigate.params }),
60
- ...(navigate.tab !== undefined && { tab: navigate.tab }),
61
- }
62
- : undefined;
63
- if (action === undefined)
64
- return;
65
- runProjectionRowNavigate(nav, action, { id: "", values: record }, host);
66
- }
67
- // Absolute http(s) check for RecordHeaderSpec.subtitleHref — deliberately
68
- // stricter than isSafeHref (which also allows relative paths and mailto:),
69
- // since a relative/mailto value here should render as plain text, not a
70
- // "_blank" external link.
71
- function isAbsoluteHttpUrl(value) {
72
- return /^https?:\/\//i.test(value) && isSafeHref(value);
73
- }
74
- // hrefFor throws for an entity without a detailFor screen; outside
75
- // createKumikoApp the feature list is empty, so navigateTargetAllows can't
76
- // rule that out beforehand. The NavApi owns target resolution, so asking it
77
- // and falling back to plain text covers both cases.
78
- function hrefForNavigate(nav, navigate, record) {
79
- const entityId = navigate.entityId !== undefined ? String(record[navigate.entityId] ?? "") : undefined;
80
- try {
81
- if (navigate.entity !== undefined) {
82
- return entityId === undefined || entityId === ""
83
- ? undefined
84
- : nav.hrefFor({ entity: navigate.entity, id: entityId });
85
- }
86
- if (navigate.screen !== undefined) {
87
- return nav.hrefFor({
88
- screenId: navigate.screen,
89
- ...(entityId !== undefined && entityId !== "" && { entityId }),
90
- });
91
- }
92
- }
93
- catch {
94
- return undefined;
95
- }
96
- return undefined;
97
- }
98
- function buildSubtitleParts(parts, record, nav, appFeatures, userRoles, host) {
99
- return parts.flatMap((part) => {
100
- const field = typeof part === "string" ? part : part.field;
101
- const text = String(record[field] ?? "");
102
- if (text === "")
103
- return [];
104
- const navigate = typeof part === "string" ? undefined : part.navigate;
105
- if (navigate === undefined || !navigateTargetAllows(navigate, appFeatures, userRoles)) {
106
- return [{ field, text }];
107
- }
108
- const href = hrefForNavigate(nav, navigate, record);
109
- if (href === undefined)
110
- return [{ field, text }];
111
- return [{ field, text, href, onPress: () => runMetricNavigate(nav, navigate, record, host) }];
112
- });
113
- }
114
- function headerStatusTone(header, record) {
115
- if (header.status === undefined)
116
- return undefined;
117
- const value = String(record[header.statusToneField ?? header.status] ?? "");
118
- const declared = header.statusTones !== undefined && Object.hasOwn(header.statusTones, value)
119
- ? header.statusTones[value]
120
- : undefined;
121
- const declaredTone = declared !== undefined ? statusToneForOptionTone(declared) : undefined;
122
- return declaredTone ?? statusToneForValue(value);
123
- }
124
- function resolveSubtitleHref(header, record) {
125
- if (header?.subtitleHref === undefined)
126
- return undefined;
127
- const value = record[header.subtitleHref];
128
- return typeof value === "string" && isAbsoluteHttpUrl(value) ? value : undefined;
129
- }
14
+ import { deriveDetailRegionPieces, renderDetailHeaderContent, } from "./projection-detail-header-region.js";
15
+ import { renderDetailQueryError, renderMissingIdBanner, renderRecordNotFound, } from "./projection-detail-states.js";
16
+ import { renderProjectionDrawerHost } from "./projection-drawer-host.js";
17
+ import { QueryLoadingBanner } from "./query-state-banners.js";
18
+ import { useReturnHost, useReturnTarget } from "./return-to.js";
19
+ import { useProjectionDetailActions } from "./use-projection-detail-actions.js";
20
+ import { useProjectionDetailData } from "./use-projection-detail-data.js";
21
+ import { useProjectionDetailLayout } from "./use-projection-detail-layout.js";
130
22
  // Projection detail body: read-only single-row inspector over an explicit
131
23
  // query instead of an entity (see projection-detail-shim.ts for the debt
132
24
  // doc). Fetches itself via `screen.query` + `idParam` (like
@@ -139,67 +31,23 @@ function resolveSubtitleHref(header, record) {
139
31
  // customSubmit; the no-op makes that path harmless instead of leaving it to
140
32
  // chance.
141
33
  export function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
142
- const { Banner, Button, Dialog, Text, Grid, GridCell, Card, Tabs, StatusBadge, Metric, MetricBand, PageHeader, Link, ActionOverflowMenu, } = usePrimitives();
34
+ const primitives = usePrimitives();
143
35
  const t = useTranslation();
144
36
  const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
145
37
  const pageHeaderCompactContext = usePageHeaderCompact();
146
38
  const effectiveTranslate = translate ?? t;
147
39
  const nav = useNav();
148
- const idParam = screen.idParam ?? "id";
149
40
  // Singleton screens ignore whatever id the path happens to carry — the
150
41
  // server picks the row from session/context, so forwarding a path id
151
42
  // (even a stray/spoofed one) into the query or into extension sections'
152
43
  // entityId fallback (resolveExtensionEntityId in render-edit.tsx) would
153
44
  // let a client-controlled value leak where none is meant to reach.
154
45
  const effectiveEntityId = screen.singleton === true ? undefined : entityId;
155
- const isTabsMode = screen.layout.mode === "tabs";
156
- const activeSection = useMemo(() => {
157
- if (!isTabsMode || Tabs === undefined)
158
- return undefined;
159
- const tabParam = nav.searchParams["tab"];
160
- return (screen.layout.sections.find((section) => section.id === tabParam) ?? screen.layout.sections[0]);
161
- }, [isTabsMode, Tabs, screen.layout.sections, nav.searchParams]);
162
- const hasTabs = isTabsMode && Tabs !== undefined && activeSection !== undefined;
163
- // Tabs is an optional Core-Primitive: without it the screen falls back to
164
- // the stacked all-sections layout instead of silently truncating to section 1.
165
- useEffect(() => {
166
- if (isTabsMode && Tabs === undefined) {
167
- rendererWarn(`[kumiko] screen "${screen.id}" uses layout.mode: "tabs", but no <Tabs> primitive is registered — the tab strip will not render. createKumikoApp() from kumiko-renderer-web wires it automatically.`);
168
- }
169
- }, [isTabsMode, Tabs, screen.id]);
170
- const entity = useMemo(() => synthesizeProjectionDetailEntity(screen.layout), [screen.layout]);
171
- const detailScreen = useMemo(() => {
172
- const source = activeSection !== undefined
173
- ? { ...screen, layout: { ...screen.layout, sections: [activeSection] } }
174
- : screen;
175
- return synthesizeProjectionDetailScreen(source);
176
- }, [screen, activeSection]);
177
- const detailQuery = useQuery(screen.query, effectiveEntityId !== undefined ? { [idParam]: effectiveEntityId } : {});
178
- // A writeForm section's handler creates a new record (see EditWriteFormSection's
179
- // doc) — both the outer record (header/metrics/other sections) and a sibling
180
- // relatedList section's own independently-fetched rows must reflect it. A
181
- // query refetch alone only refreshes `record` below; bumping this as
182
- // <RenderEdit>'s refreshToken makes RelatedListSection's own useQuery
183
- // re-run too (it has no `live` subscription of its own). Refetch instead of
184
- // remount (a `key`) so unsaved input in the other writeForm sections and
185
- // the related lists' local state survive.
186
- const [reloadNonce, setReloadNonce] = useState(0);
187
- const reloadDetail = useCallback(async () => {
188
- await refetchAfterWrite(detailQuery.refetch);
189
- setReloadNonce((n) => n + 1);
190
- }, [detailQuery.refetch]);
46
+ const { activeSection, hasTabs, entity, detailScreen, detailQuery, reloadNonce, reloadDetail } = useProjectionDetailData({ screen, Tabs: primitives.Tabs, nav, effectiveEntityId });
191
47
  // Header actions render directly via RenderEditActionButton
192
48
  // headerActions moved out of RenderEdit's footer — this component owns
193
49
  // the failure-surfacing state RenderEdit used to own for them.
194
50
  const [actionError, setActionError] = useState(null);
195
- // Default edit action: resolved cross-feature over ALL mounted
196
- // features, not just this feature's own schema — detailFor itself is
197
- // resolved cross-feature by the boot-validator (detail-screens.ts), and
198
- // the motivating case is a projectionDetail whose query belongs to one
199
- // feature while the entity's entityEdit screen lives in another. Unlike
200
- // useNavigateToCreateFor this must NOT filter on allowCreate/singleton —
201
- // those gate create-targets, we're resolving an update-target by a
202
- // known id.
203
51
  const appFeatures = useAppFeatures();
204
52
  const userRoles = useUserRoles();
205
53
  const dispatcher = useOptionalDispatcher();
@@ -208,257 +56,74 @@ export function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
208
56
  const returnTargetParam = useReturnTarget(screen.id);
209
57
  // A singleton has no record route to come back to — same as entityEdit.
210
58
  const returnTarget = screen.singleton === true ? undefined : returnTargetParam;
211
- const { drawerAction, drawerScreen, drawerInitialValues, openDrawer, closeDrawer } = useDrawerAction(schema);
212
- const handleRecordLeft = useCallback((action, resultData, deletedShownRecord) => {
213
- const record = detailQuery.data ?? {};
214
- // Never return onto the just-deleted record.
215
- const target = deletedShownRecord && returnTarget?.entityId === effectiveEntityId
216
- ? undefined
217
- : returnTarget;
218
- if (action.redirect !== undefined) {
219
- navigateAfterRedirect({
220
- nav,
221
- schema,
222
- appFeatures,
223
- returnTarget: target,
224
- redirect: action.redirect,
225
- resultData,
226
- record,
227
- });
228
- return;
229
- }
230
- navigateToReturnOr(nav, target, () => {
231
- const listScreen = screen.listScreenId ??
232
- (screen.detailFor !== undefined
233
- ? findEntityListScreen(schema, screen.detailFor)?.id
234
- : undefined);
235
- if (listScreen !== undefined)
236
- nav.navigate({ screenId: lastSegment(listScreen) });
237
- else
238
- void detailQuery.refetch();
239
- });
240
- }, [
241
- detailQuery.data,
242
- detailQuery.refetch,
243
- nav,
59
+ const drawer = useDrawerAction(schema);
60
+ const { openDrawer } = drawer;
61
+ const { buildSectionActions, headerActions } = useProjectionDetailActions({
244
62
  schema,
245
- appFeatures,
246
- returnTarget,
247
- effectiveEntityId,
248
- screen.listScreenId,
249
- screen.detailFor,
250
- ]);
251
- const editScreen = useMemo(() => {
252
- const detailFor = screen.detailFor;
253
- if (detailFor === undefined)
254
- return undefined;
255
- return findEditScreenFor(detailFor, appFeatures, userRoles);
256
- }, [appFeatures, screen.detailFor, userRoles]);
257
- const defaultEditAction = useMemo(() => {
258
- if (editScreen === undefined)
259
- return undefined;
260
- // editScreen.id is registry-qualified ("feature:screen:contact-edit");
261
- // nav.navigate expects the short form (see useNavigateToCreateFor above).
262
- const targetScreenId = lastSegment(editScreen.id);
263
- return {
264
- id: "edit",
265
- label: effectiveTranslate("kumiko.actions.edit"),
266
- icon: resolveActionIcon("edit"),
267
- onPress: () => {
268
- const target = {
269
- screenId: targetScreenId,
270
- ...(effectiveEntityId !== undefined && { entityId: effectiveEntityId }),
271
- };
272
- navigateWithReturnTo(nav, target, host);
273
- },
274
- };
275
- }, [editScreen, effectiveTranslate, nav, effectiveEntityId, host]);
276
- // a navigate-screen action targeting an entityEdit screen for the
277
- // SAME entity this projectionDetail shows (screen.detailFor) defaults its
278
- // entityId to the shown record's own id — without this fallback the target
279
- // opens an empty create-form instead of the shown record, silently.
280
- // Searched cross-feature, consistent with editScreen above.
281
- const sameEntityScreenId = useCallback((targetScreen) => {
282
- const record = detailQuery.data ?? {};
283
- const targetIsEntityEditSameEntity = screen.detailFor !== undefined &&
284
- appFeatures.some((feature) => feature.screens.some((s) => s.type === "entityEdit" &&
285
- s.entity === screen.detailFor &&
286
- lastSegment(s.id) === targetScreen));
287
- return targetIsEntityEditSameEntity ? String(record["id"] ?? "") : undefined;
288
- }, [detailQuery.data, screen.detailFor, appFeatures]);
289
- const buildSectionActions = useCallback((actions) => buildRecordActions({
290
- actions,
291
- record: detailQuery.data ?? {},
292
- translate: effectiveTranslate,
293
- nav,
294
- host,
295
- dispatcher,
296
- openDrawer,
297
- onWriteSuccess: reloadDetail,
298
- onRecordLeft: handleRecordLeft,
299
- sameEntityScreenId,
300
- clientActions,
301
- }), [
302
- detailQuery.data,
303
- reloadDetail,
63
+ screen,
304
64
  effectiveTranslate,
65
+ effectiveEntityId,
305
66
  nav,
306
67
  host,
307
68
  dispatcher,
308
69
  clientActions,
70
+ appFeatures,
71
+ userRoles,
72
+ returnTarget,
309
73
  openDrawer,
310
- handleRecordLeft,
311
- sameEntityScreenId,
312
- ]);
313
- const headerActions = useMemo(() => {
314
- const declaredHasEdit = screen.actions?.some((a) => a.id === "edit") === true;
315
- const resolved = buildRecordActions({
316
- actions: screen.actions ?? [],
317
- record: detailQuery.data ?? {},
318
- translate: effectiveTranslate,
319
- nav,
320
- host,
321
- dispatcher,
322
- openDrawer,
323
- onWriteSuccess: reloadDetail,
324
- onRecordLeft: handleRecordLeft,
325
- sameEntityScreenId,
326
- clientActions,
327
- });
328
- if (defaultEditAction === undefined || declaredHasEdit)
329
- return resolved;
330
- return [defaultEditAction, ...(resolved ?? [])];
331
- }, [
332
- screen.actions,
333
- defaultEditAction,
334
- effectiveTranslate,
335
- nav,
336
- host,
337
- dispatcher,
338
- clientActions,
339
- detailQuery.data,
74
+ detailQuery,
340
75
  reloadDetail,
341
- openDrawer,
342
- handleRecordLeft,
343
- sameEntityScreenId,
344
- ]);
345
- const hasHeader = screen.header !== undefined;
346
- const hasMetrics = screen.metrics !== undefined && screen.metrics.length > 0;
347
- // ?? [] rather than threading `headerActions !== undefined` through every
348
- // use below — an empty array is a safe no-op for .map/.length.
349
- const headerActionsList = headerActions ?? [];
350
- const hasHeaderActions = headerActionsList.length > 0;
351
- const hasHeaderCard = hasHeader || hasMetrics || hasHeaderActions;
352
- // A header card (metric band, status, actions) needs more room than the
353
- // 640px form column; screens with only a record title keep the narrow one.
354
- const widensScreenFormColumn = hasHeaderCard &&
355
- !hasTabs &&
356
- screenFillsHeight(screen) &&
357
- detailScreen.layout.width === undefined;
358
- const renderedDetailScreen = useMemo(() => widensScreenFormColumn
359
- ? { ...detailScreen, layout: { ...detailScreen.layout, width: HEADER_CARD_DEFAULT_WIDTH } }
360
- : detailScreen, [detailScreen, widensScreenFormColumn]);
76
+ });
77
+ const { hasMetrics, headerActionsList, hasHeaderActions, hasHeaderCard, renderedDetailScreen } = useProjectionDetailLayout({ screen, headerActions, hasTabs, detailScreen });
361
78
  if (effectiveEntityId === undefined && screen.singleton !== true) {
362
- return (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-screen-projection-detail-missing-id", children: ["Screen ", _jsx(Text, { variant: "code", children: screen.id }), " (projectionDetail) needs a row id in the path \u2014 open it from a row action."] }));
79
+ return renderMissingIdBanner(primitives, screen);
363
80
  }
364
81
  if (detailQuery.loading && detailQuery.data === null) {
365
82
  return _jsx(QueryLoadingBanner, {});
366
83
  }
367
84
  if (detailQuery.error && detailQuery.data === null) {
368
- return (_jsx(QueryErrorBanner, { error: detailQuery.error, translate: effectiveTranslate, onRetry: detailQuery.refetch }));
85
+ return renderDetailQueryError(detailQuery.error, effectiveTranslate, detailQuery.refetch);
369
86
  }
370
87
  // A failed refetch keeps the last good record on screen.
371
- const refetchErrorBanner = detailQuery.error ? (_jsx(QueryErrorBanner, { error: detailQuery.error, translate: effectiveTranslate, onRetry: detailQuery.refetch })) : null;
88
+ const refetchErrorBanner = detailQuery.error
89
+ ? renderDetailQueryError(detailQuery.error, effectiveTranslate, detailQuery.refetch)
90
+ : null;
372
91
  const record = detailQuery.data;
373
92
  if (!record) {
374
- const backListScreen = screen.listScreenId ??
375
- (screen.detailFor !== undefined
376
- ? findEntityListScreen(schema, screen.detailFor)?.id
377
- : undefined);
378
- return (_jsx(RecordNotFoundState, { translate: effectiveTranslate, ...((returnTarget !== undefined || backListScreen !== undefined) && {
379
- onBack: () => navigateToReturnOr(nav, returnTarget, () => {
380
- if (backListScreen !== undefined) {
381
- nav.navigate({ screenId: lastSegment(backListScreen) });
382
- }
383
- }),
384
- }) }));
93
+ return renderRecordNotFound({ schema, screen, nav, returnTarget, effectiveTranslate });
385
94
  }
386
- // Grouped into the head Card alongside title/status/metrics:
387
- // these are actions on the record the head shows, not on whichever tab is
388
- // open, so they must stay in place across tab switches instead of
389
- // trailing the active tab's content in the card footer.
390
- const headerActionsContent = hasHeaderActions && (_jsx(Grid, { columns: "auto", testId: "kumiko-screen-projection-detail-actions", children: _jsx(HeaderActionsBar, { actions: headerActionsList, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError, ...(screen.primaryAction !== undefined && { preferredPrimaryId: screen.primaryAction }) }) }));
391
- // Rendered as RenderEdit's headerRegion (not as JSX siblings before it) so
392
- // this shares the same page padding/width as the record's edit card below
393
- // it, instead of sitting flush against the screen edge (fw record-screen
394
- // header polish).
395
- const recordTitle = screen.recordTitleField !== undefined
396
- ? resolveRecordTitle(record[screen.recordTitleField])
397
- : undefined;
398
- const header = screen.header;
399
- const subtitleHref = resolveSubtitleHref(header, record);
400
- // Plain Text, not Heading — the Card title slot already renders as its own
401
- // heading element (h3); nesting another heading (h1) inside it is invalid HTML.
402
- const headerTitleSlot = header !== undefined ? (header.status !== undefined ? (_jsxs(Grid, { columns: "auto", children: [_jsx(Text, { testId: "kumiko-screen-projection-detail-title", children: String(record[header.title] ?? "") }), StatusBadge !== undefined ? (_jsx(StatusBadge, { value: String(record[header.status] ?? ""), tone: headerStatusTone(header, record), testId: "kumiko-screen-projection-detail-status" })) : (_jsx(Text, { testId: "kumiko-screen-projection-detail-status", children: String(record[header.status] ?? "") }))] })) : (_jsx(Text, { testId: "kumiko-screen-projection-detail-title", children: String(record[header.title] ?? "") }))) : undefined;
403
- const subtitleParts = header !== undefined && typeof header.subtitle === "object"
404
- ? buildSubtitleParts(header.subtitle, record, nav, appFeatures, userRoles, host)
405
- : undefined;
406
- const headerSubtitleSlot = subtitleParts !== undefined ? (subtitleParts.length > 0 ? (_jsx(Text, { testId: "kumiko-screen-projection-detail-subtitle", children: subtitleParts.map((part, index) => (_jsxs(Fragment, { children: [index > 0 && (_jsx(Text, { variant: "muted", decorative: true, children: " · " })), part.href !== undefined ? (_jsx(Link, { href: part.href, ...(part.onPress !== undefined && { onPress: part.onPress }), className: "underline", testId: `kumiko-screen-projection-detail-subtitle-${part.field}`, children: part.text })) : (_jsx(Text, { testId: `kumiko-screen-projection-detail-subtitle-${part.field}`, children: part.text }))] }, part.field))) })) : undefined) : typeof header?.subtitle === "string" ? (subtitleHref !== undefined ? (_jsx(Link, { href: subtitleHref, target: "_blank", testId: "kumiko-screen-projection-detail-subtitle", children: String(record[header.subtitle] ?? "") })) : (_jsx(Text, { variant: "muted", testId: "kumiko-screen-projection-detail-subtitle", children: String(record[header.subtitle] ?? "") }))) : undefined;
407
- // slots.header shares the header card with the actions (one card, actions
408
- // top right) instead of rendering as an unframed block above it.
409
- const metricItems = screen.metrics?.map((metric) => {
410
- const field = metric.field;
411
- const label = effectiveTranslate(metric.label);
412
- const value = String(record[field] ?? "");
413
- const testId = `kumiko-screen-projection-detail-metric-${field}`;
414
- const navigate = metric.navigate;
415
- const onPress = navigate !== undefined && navigateTargetAllows(navigate, appFeatures, userRoles)
416
- ? () => runMetricNavigate(nav, navigate, record, host)
417
- : undefined;
418
- return Metric !== undefined ? (_jsx(Metric, { label: label, value: value, testId: testId, ...(onPress !== undefined && { onPress }) }, field)) : (_jsxs(GridCell, { children: [_jsx(Text, { variant: "small", testId: `${testId}-label`, children: label }), _jsx(Text, { testId: `${testId}-value`, children: value })] }, field));
419
- });
420
- // Metrics are a dl band when the primitive exists; the old Grid stays as the fallback.
421
- const metricsBlock = (subtitle, lead) => MetricBand !== undefined ? (_jsx(MetricBand, { testId: "kumiko-screen-projection-detail-metrics", subtitle: subtitle, ...(lead !== undefined && { lead }), children: metricItems })) : (_jsx(Grid, { columns: screen.metrics?.length ?? 1, testId: "kumiko-screen-projection-detail-metrics", children: metricItems }));
422
- const usesPageHeaderSlot = pageHeaderSlotAvailable && PageHeader !== undefined;
423
- const pageHeaderCompact = pageHeaderCompactContext && usesPageHeaderSlot;
424
- // The phone shell header clips a long status mid-word, so on phones the badge
425
- // sits in the body instead of the shell header slot.
426
- const statusBadge = header?.status !== undefined && StatusBadge !== undefined ? (_jsx(StatusBadge, { value: String(record[header.status] ?? ""), tone: headerStatusTone(header, record), testId: "kumiko-screen-projection-detail-status" })) : undefined;
427
- // Lives in the band's subtitle row so it shares the metrics' horizontal inset.
428
- const bandSubtitle = pageHeaderCompact && statusBadge !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Grid, { columns: "auto", testId: "kumiko-screen-projection-detail-status-row", children: statusBadge }), headerSubtitleSlot] })) : (headerSubtitleSlot);
429
- const actionErrorBanner = actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] }));
430
- const renderHeaderContent = (headerSlot) => (_jsxs(_Fragment, { children: [usesPageHeaderSlot ? (_jsxs(_Fragment, { children: [_jsx(PageHeader, { ...(header !== undefined && { title: String(record[header.title] ?? "") }), ...(header === undefined && recordTitle !== undefined && { title: recordTitle }), ...(!pageHeaderCompact && statusBadge !== undefined && { status: statusBadge }), ...(hasHeaderActions && {
431
- actions: (_jsx(Grid, { columns: "auto", testId: "kumiko-screen-projection-detail-actions", children: _jsx(HeaderActionsBar, { actions: headerActionsList, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError, collapseAfterPrimary: true, compact: pageHeaderCompact, PageHeader: PageHeader, ...(screen.primaryAction !== undefined && {
432
- preferredPrimaryId: screen.primaryAction,
433
- }) }) })),
434
- }) }), MetricBand === undefined && bandSubtitle, MetricBand === undefined && headerSlot, (hasMetrics || bandSubtitle !== undefined || headerSlot !== undefined) &&
435
- (MetricBand !== undefined || hasMetrics) &&
436
- metricsBlock(bandSubtitle, MetricBand !== undefined ? headerSlot : undefined), actionErrorBanner] })) : ((hasHeaderCard || headerSlot !== undefined || recordTitle !== undefined) && (_jsxs(Card, { slots: {
437
- ...(headerTitleSlot !== undefined && { title: headerTitleSlot }),
438
- ...(headerTitleSlot === undefined &&
439
- recordTitle !== undefined && { title: recordTitle }),
440
- ...(headerSubtitleSlot !== undefined && { subtitle: headerSubtitleSlot }),
441
- ...(headerSlot !== undefined && { headerContent: headerSlot }),
442
- ...(hasHeaderActions && { headerActions: headerActionsContent }),
443
- }, children: [hasMetrics && metricsBlock(), actionErrorBanner] }))), hasTabs && activeSection !== undefined && (_jsx(Tabs, { testId: "kumiko-screen-projection-detail-tabs", items: screen.layout.sections.map((section) => {
444
- const countField = isWriteFormEditSection(section) ? undefined : section.countField;
445
- const countValue = countField !== undefined ? record[countField] : undefined;
446
- const count = typeof countValue === "number" && Number.isFinite(countValue)
447
- ? countValue
448
- : undefined;
449
- return {
450
- id: section.id ?? "",
451
- label: effectiveTranslate(section.title ?? section.id ?? ""),
452
- ...(count !== undefined && { count }),
453
- };
454
- }), activeId: activeSection.id ?? "", onSelect: (id) => requestLeave(() => nav.setSearchParams({ tab: id })) }))] }));
95
+ const regionModel = {
96
+ screen,
97
+ record,
98
+ primitives,
99
+ effectiveTranslate,
100
+ nav,
101
+ appFeatures,
102
+ userRoles,
103
+ host,
104
+ headerActionsList,
105
+ setActionError,
106
+ actionError,
107
+ pageHeaderSlotAvailable,
108
+ pageHeaderCompactContext,
109
+ hasHeaderActions,
110
+ hasMetrics,
111
+ hasHeaderCard,
112
+ hasTabs,
113
+ activeSection,
114
+ };
115
+ const regionPieces = deriveDetailRegionPieces(regionModel);
116
+ const renderHeaderContent = (headerSlot) => renderDetailHeaderContent(regionModel, regionPieces, headerSlot);
455
117
  return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(RenderEdit, { refreshToken: reloadNonce, screen: renderedDetailScreen, entity: entity, featureName: schema.featureName, initial: record, entityId: effectiveEntityId, customSubmit: async () => ({ isSuccess: true, validationBlocked: false, data: undefined }), onReload: reloadDetail, onRelatedListDrawerAction: openDrawer, ...(translate !== undefined && { translate }), ...(hasTabs && { hideSectionTitles: true }), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...((hasHeaderCard ||
456
118
  hasTabs ||
457
119
  screen.slots?.header !== undefined ||
458
120
  screen.recordTitleField !== undefined) && {
459
121
  headerRegion: renderHeaderContent,
460
- }), buildSectionActions: buildSectionActions, valueDisplay: screen.valueDisplay ?? "text" }, entityId), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
461
- closeDrawer();
462
- void reloadDetail();
463
- } })] }));
122
+ }), buildSectionActions: buildSectionActions, valueDisplay: screen.valueDisplay ?? "text" }, entityId), renderProjectionDrawerHost({
123
+ schema,
124
+ translate,
125
+ userRoles,
126
+ drawer,
127
+ refreshRows: reloadDetail,
128
+ })] }));
464
129
  }
@@ -0,0 +1,11 @@
1
+ import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { Translate } from "../headless/index.ts";
3
+ import { type StatusTone } from "../primitives.tsx";
4
+ export declare function headerStatusTone(header: NonNullable<ProjectionDetailScreenDefinition["header"]>, record: Readonly<Record<string, unknown>>): StatusTone | undefined;
5
+ export declare function resolveSubtitleHref(header: ProjectionDetailScreenDefinition["header"], record: Readonly<Record<string, unknown>>): string | undefined;
6
+ export type DetailTabItem = {
7
+ readonly id: string;
8
+ readonly label: string;
9
+ readonly count?: number;
10
+ };
11
+ export declare function buildDetailTabItems(sections: ProjectionDetailScreenDefinition["layout"]["sections"], record: Readonly<Record<string, unknown>>, translate: Translate): DetailTabItem[];
@@ -0,0 +1,33 @@
1
+ import { isWriteFormEditSection } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { statusToneForOptionTone, statusToneForValue } from "../primitives.js";
3
+ import { isAbsoluteHttpUrl } from "./projection-detail-navigation.js";
4
+ export function headerStatusTone(header, record) {
5
+ // skip: a header without status has no tone
6
+ if (header.status === undefined)
7
+ return undefined;
8
+ const value = String(record[header.statusToneField ?? header.status] ?? "");
9
+ const declared = header.statusTones !== undefined && Object.hasOwn(header.statusTones, value)
10
+ ? header.statusTones[value]
11
+ : undefined;
12
+ const declaredTone = declared !== undefined ? statusToneForOptionTone(declared) : undefined;
13
+ return declaredTone ?? statusToneForValue(value);
14
+ }
15
+ export function resolveSubtitleHref(header, record) {
16
+ // skip: no subtitleHref declared
17
+ if (header?.subtitleHref === undefined)
18
+ return undefined;
19
+ const value = record[header.subtitleHref];
20
+ return typeof value === "string" && isAbsoluteHttpUrl(value) ? value : undefined;
21
+ }
22
+ export function buildDetailTabItems(sections, record, translate) {
23
+ return sections.map((section) => {
24
+ const countField = isWriteFormEditSection(section) ? undefined : section.countField;
25
+ const countValue = countField !== undefined ? record[countField] : undefined;
26
+ const count = typeof countValue === "number" && Number.isFinite(countValue) ? countValue : undefined;
27
+ return {
28
+ id: section.id ?? "",
29
+ label: translate(section.title ?? section.id ?? ""),
30
+ ...(count !== undefined && { count }),
31
+ };
32
+ });
33
+ }
@@ -0,0 +1,28 @@
1
+ import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { ReactNode } from "react";
3
+ import type { ErrorNotice } from "./error-reference.tsx";
4
+ import { type DetailHeaderModel } from "./projection-detail-header.tsx";
5
+ export type DetailRegionModel = DetailHeaderModel & {
6
+ readonly actionError: ErrorNotice | null;
7
+ readonly pageHeaderSlotAvailable: boolean;
8
+ readonly pageHeaderCompactContext: boolean;
9
+ readonly hasHeaderActions: boolean;
10
+ readonly hasMetrics: boolean;
11
+ readonly hasHeaderCard: boolean;
12
+ readonly hasTabs: boolean;
13
+ readonly activeSection: ProjectionDetailScreenDefinition["layout"]["sections"][number] | undefined;
14
+ };
15
+ export type DetailRegionPieces = {
16
+ readonly PageHeader: DetailHeaderModel["primitives"]["PageHeader"];
17
+ readonly pageHeaderCompact: boolean;
18
+ readonly recordTitle: string | undefined;
19
+ readonly headerTitleSlot: ReactNode;
20
+ readonly headerSubtitleSlot: ReactNode;
21
+ readonly headerActionsContent: ReactNode;
22
+ readonly statusBadge: ReactNode;
23
+ readonly bandSubtitle: ReactNode;
24
+ readonly metricItems: ReactNode[] | undefined;
25
+ readonly actionErrorBanner: ReactNode;
26
+ };
27
+ export declare function deriveDetailRegionPieces(model: DetailRegionModel): DetailRegionPieces;
28
+ export declare function renderDetailHeaderContent(model: DetailRegionModel, pieces: DetailRegionPieces, headerSlot: ReactNode | undefined): ReactNode;