@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.1

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 +10 -10
  230. package/src/changes.json +54 -0
@@ -0,0 +1,90 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ErrorReference } from "../app/error-reference.js";
3
+ import { formatWhen } from "../format-when.js";
4
+ import { tabIdAt } from "./render-edit-helpers.js";
5
+ import { isDispatcherRejection } from "./render-edit-logic.js";
6
+ export function renderAllRequiredHint({ engine: { state }, }) {
7
+ const { Text } = state.primitives;
8
+ return (_jsx(Text, { variant: "muted", testId: "render-edit-all-required-hint", children: state.translate("kumiko.form.all-required") }));
9
+ }
10
+ export function renderDraftPicker({ engine: { state, draftActions }, }) {
11
+ const { Banner, Button, Text } = state.primitives;
12
+ const { translate } = state;
13
+ const { draftCandidates, setDraftCandidates } = state.draftState;
14
+ return (draftCandidates !== null && (_jsx(Banner, { variant: "info", testId: "render-edit-draft-picker", actions: [
15
+ ...draftCandidates.map((candidate) => (_jsx(Button, { type: "button", variant: "link", onClick: () => draftActions.adoptDraft(candidate), testId: `render-edit-draft-pick-${candidate.id}`, children: formatWhen(candidate.savedAt) }, candidate.id))),
16
+ _jsx(Button, { type: "button", variant: "link", onClick: () => setDraftCandidates(null), testId: "render-edit-draft-start-new", children: translate("kumiko.form.draft.start-new") }, "start-new"),
17
+ ], children: _jsx(Text, { children: translate(draftCandidates.length === 1
18
+ ? "kumiko.form.draft.resume-single"
19
+ : "kumiko.form.draft.resume-multiple") }) })));
20
+ }
21
+ export function renderWizardProgress({ engine: { state, steps, sections, nav }, titles, }) {
22
+ const { Progress, StepBar, Text } = state.primitives;
23
+ const { currentStep, lastStepIndex, updateModeStepBarProps } = steps;
24
+ return (state.isWizard &&
25
+ !titles.stepRail && (_jsxs(_Fragment, { children: [Progress !== undefined && (_jsx(Progress, { value: (currentStep + 1) / (lastStepIndex + 1), testId: "render-edit-wizard-progress" })), StepBar === undefined ? (_jsx(Text, { variant: "small", testId: "render-edit-wizard-step-label", children: titles.compactStepLabel })) : (_jsx(StepBar, { steps: sections.filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: titles.compactStepLabel, onStepSelect: nav.handleWizardStepSelect, ...updateModeStepBarProps, testId: "render-edit-wizard-steps", compactTestId: "render-edit-wizard-step-label" }))] })));
26
+ }
27
+ export function renderTabStrip({ engine: { state, steps, sections, nav }, }) {
28
+ const { Tabs } = state.primitives;
29
+ const { filteredSections } = sections;
30
+ // No Tabs primitive registered → every section renders stacked
31
+ // (stepHidden below is gated on the same condition), which is the
32
+ // single-layout behaviour. Degrading to one long form beats
33
+ // hiding sections behind a strip that never drew.
34
+ if (Tabs === undefined)
35
+ return null;
36
+ return (_jsx(Tabs, { testId: "render-edit-tabs", items: filteredSections.map((section, index) => ({
37
+ id: tabIdAt(section, index),
38
+ label: section.title ?? "",
39
+ })), activeId: tabIdAt(filteredSections[steps.currentStep], steps.currentStep), onSelect: (id) => {
40
+ const index = filteredSections.findIndex((s, i) => tabIdAt(s, i) === id);
41
+ // skip: the strip reported an id no section owns.
42
+ if (index === -1)
43
+ return;
44
+ nav.jumpToStep(index);
45
+ } }));
46
+ }
47
+ export function renderFormErrorBanner({ engine: { props, state }, layout, }) {
48
+ const { Banner, Button, Text } = state.primitives;
49
+ const { translate, formError } = state;
50
+ const { onReload } = props;
51
+ return (formError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-form-error", actions: formError.code === "version_conflict" && onReload !== undefined ? (_jsx(Button, { variant: "secondary", onClick: () => {
52
+ onReload();
53
+ state.setFormError(null);
54
+ state.setExtraDeleteIssues([]);
55
+ }, testId: "render-edit-form-error-reload", children: translate("kumiko.actions.reload") })) : undefined, children: [_jsx(Text, { testId: "render-edit-form-error-key", children: translate(formError.i18nKey, formError.i18nParams) }), layout.rootIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-root", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`))), state.extraDeleteIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-extra", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`)))] })));
56
+ }
57
+ export function renderRootIssueBanners({ engine: { state }, layout, }) {
58
+ const { Banner, Text } = state.primitives;
59
+ return (state.formError === null &&
60
+ layout.rootIssues.map((issue) => (_jsx(Banner, { variant: "error", testId: "render-edit-root-issue", children: _jsx(Text, { children: state.translate(issue.i18nKey, issue.params) }) }, issue.i18nKey))));
61
+ }
62
+ export function renderExtensionErrorBanner({ engine: { state }, }) {
63
+ const { Banner, Text } = state.primitives;
64
+ const { extensionErrorKey } = state;
65
+ return (extensionErrorKey !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-extension-error", children: _jsx(Text, { testId: "render-edit-extension-error-key", children: state.translate(extensionErrorKey) }) })));
66
+ }
67
+ export function renderActionErrorBanner({ engine: { state }, }) {
68
+ const { Banner } = state.primitives;
69
+ const { actionError } = state;
70
+ return (actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })));
71
+ }
72
+ // A rejected delete (e.g. a preDelete hook) carries its reason in
73
+ // the field issues; the top-level key is only "validation failed".
74
+ function applyDeleteRejection(state, rejection) {
75
+ const [issue, ...otherIssues] = rejection.details?.fields ?? [];
76
+ state.setExtraDeleteIssues(otherIssues);
77
+ state.setFormErrorSource("delete");
78
+ state.setFormError(issue === undefined
79
+ ? rejection
80
+ : { ...rejection, i18nKey: issue.i18nKey, i18nParams: issue.params });
81
+ }
82
+ export function renderDeleteDialog({ engine: { props, state }, }) {
83
+ const { Dialog } = state.primitives;
84
+ const { onDelete } = props;
85
+ return (onDelete !== undefined && (_jsx(Dialog, { open: state.confirmDeleteOpen, onOpenChange: state.setConfirmDeleteOpen, title: state.translate("kumiko.actions.delete-confirm"), confirmLabel: state.translate("kumiko.actions.delete"), variant: "danger", onConfirm: async () => {
86
+ const rejection = await onDelete();
87
+ if (isDispatcherRejection(rejection))
88
+ applyDeleteRejection(state, rejection);
89
+ }, testId: "render-edit-delete-dialog" })));
90
+ }
@@ -0,0 +1,19 @@
1
+ import type { MutableRefObject } from "react";
2
+ import type { FormValues } from "../headless/index.ts";
3
+ import type { ResolvedRenderEditProps } from "./render-edit-render-model.ts";
4
+ import type { RenderEditForm } from "./use-render-edit-form.ts";
5
+ type ControlsOf<TValues extends FormValues> = Parameters<NonNullable<ResolvedRenderEditProps<TValues>["onControlsReady"]>>[0];
6
+ type ChangeStateOf<TValues extends FormValues> = Parameters<NonNullable<ResolvedRenderEditProps<TValues>["onChange"]>>[0];
7
+ export declare function flushPendingDraftSave(draftSaveTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>, saveDraftRef: MutableRefObject<(stepIndex: number) => void>, currentStepRef: MutableRefObject<number>): void;
8
+ export declare function buildControlsFor<TValues extends FormValues>({ controller, patch, validate, handleSubmitRef, wizardNextRef, wizardBackRef, }: {
9
+ readonly controller: RenderEditForm<TValues>["controller"];
10
+ readonly patch: ControlsOf<TValues>["patch"];
11
+ readonly validate: ControlsOf<TValues>["validate"];
12
+ readonly handleSubmitRef: MutableRefObject<(options?: {
13
+ readonly saveNow?: boolean;
14
+ }) => Promise<void>>;
15
+ readonly wizardNextRef: MutableRefObject<() => void>;
16
+ readonly wizardBackRef: MutableRefObject<() => void>;
17
+ }): ControlsOf<TValues>;
18
+ export declare function deriveChangeState<TValues extends FormValues>(snapshot: RenderEditForm<TValues>["snapshot"], schema: ResolvedRenderEditProps<TValues>["schema"], scopeFieldNames: readonly string[] | undefined, submitting: boolean): ChangeStateOf<TValues>;
19
+ export {};
@@ -0,0 +1,42 @@
1
+ import { dryRunFieldIssues } from "./render-edit-helpers.js";
2
+ export function flushPendingDraftSave(draftSaveTimerRef, saveDraftRef, currentStepRef) {
3
+ if (draftSaveTimerRef.current !== null) {
4
+ clearTimeout(draftSaveTimerRef.current);
5
+ // Flush the pending debounced save instead of dropping it — a
6
+ // navigation/unmount inside the debounce window would otherwise
7
+ // silently discard the last patch() (discardDraft nulls this ref
8
+ // first on the post-submit path, so this can't resurrect a
9
+ // just-submitted draft).
10
+ saveDraftRef.current(currentStepRef.current);
11
+ }
12
+ }
13
+ export function buildControlsFor({ controller, patch, validate, handleSubmitRef, wizardNextRef, wizardBackRef, }) {
14
+ return {
15
+ patch,
16
+ validate,
17
+ getValues: () => controller.getSnapshot().values,
18
+ // saveNow: a host-driven submit must save on a wizard's intermediate
19
+ // steps too instead of silently advancing; stepping is next()/back().
20
+ submit: () => handleSubmitRef.current({ saveNow: true }),
21
+ next: () => wizardNextRef.current(),
22
+ back: () => wizardBackRef.current(),
23
+ };
24
+ }
25
+ export function deriveChangeState(snapshot, schema, scopeFieldNames, submitting) {
26
+ // Dry-run parse against `schema` — NOT controller.validate(). Calling
27
+ // validate() here would write field-level errors into snapshot.errors
28
+ // on every keystroke, painting error messages while the user is still
29
+ // typing. `valid` can therefore legitimately diverge from what's
30
+ // currently rendered under the fields (the last *mutating* validate()
31
+ // call, e.g. from controls.validate() or submit()). The issues are
32
+ // filtered exactly like submit()'s validate: hidden fields and fields
33
+ // outside the `fields` scope never make the form invalid.
34
+ const valid = schema === undefined || dryRunFieldIssues(schema, snapshot, scopeFieldNames, true).length === 0;
35
+ return {
36
+ values: snapshot.values,
37
+ changes: snapshot.changes,
38
+ dirty: snapshot.isDirty,
39
+ valid,
40
+ submitting,
41
+ };
42
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from "react";
2
+ import type { EditFieldViewModel, FormValues } from "../headless/index.ts";
3
+ import type { FieldsSection, RenderEditModel } from "./render-edit-render-model.ts";
4
+ export declare function renderFieldGrid<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>, section: FieldsSection, fields: readonly EditFieldViewModel[], columns: number, gridKey: string, columnsDeclared: boolean | undefined): ReactNode;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { GridCellForField } from "./grid-cell-for-field.js";
3
+ function appendixCellProps(props, fieldName) {
4
+ const { labelAppendix, fieldAppendix, fieldAppendixPlacement, fieldDescription } = props;
5
+ return {
6
+ ...(labelAppendix !== undefined && { labelAppendix: labelAppendix(fieldName) }),
7
+ ...(fieldAppendix !== undefined && { fieldAppendix: fieldAppendix(fieldName) }),
8
+ ...(fieldAppendixPlacement !== undefined && { appendixPlacement: fieldAppendixPlacement }),
9
+ ...(fieldDescription !== undefined && { description: fieldDescription(fieldName) }),
10
+ };
11
+ }
12
+ function createFieldCellRenderer(model, section, columns) {
13
+ const { props, state, form } = model.engine;
14
+ const { snapshot } = form;
15
+ const { GridCell } = state.primitives;
16
+ const { isSettingsList, isCreate } = model.footer;
17
+ const { usesFieldFlow } = model.titles;
18
+ const { disabled, fieldAccent } = props;
19
+ const showsChangedMarker = !isCreate || props.markChangedFields;
20
+ const clearErrorsAndValidate = (fieldName) => {
21
+ form.clearErrorsOnInput(fieldName);
22
+ if (props.validateOnChange === true)
23
+ form.controller.validate([fieldName]);
24
+ };
25
+ return (field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(!isSettingsList && usesFieldFlow && { flow: true }), ...(isSettingsList && {
26
+ layout: "row",
27
+ accent: fieldAccent?.(field.field) === true,
28
+ }), issues: snapshot.errors[field.field], onChange: (v) => {
29
+ model.setFieldUntyped(field.field, v);
30
+ clearErrorsAndValidate(field.field);
31
+ }, GridCell: GridCell, featureName: props.featureName, ...appendixCellProps(props, field.field), allIssues: snapshot.errors, valueDisplay: props.valueDisplay, row: snapshot.values, ...(disabled && { fillDisabled: true }), ...(props.hideSectionTitles !== true &&
32
+ section.title !== undefined && { sectionTitle: section.title }), ...(showsChangedMarker && field.field in snapshot.changes && { changed: true }) }, field.field));
33
+ }
34
+ export function renderFieldGrid(model, section, fields, columns, gridKey, columnsDeclared) {
35
+ const { Grid } = model.engine.state.primitives;
36
+ const { isSettingsList } = model.footer;
37
+ const { usesFieldFlow } = model.titles;
38
+ const renderCell = createFieldCellRenderer(model, section, columns);
39
+ return (_jsx(Grid, { columns: usesFieldFlow && columnsDeclared !== true ? 1 : columns, ...(isSettingsList ? { list: true } : usesFieldFlow && { flow: true }), children: fields.map((field) => renderCell(field)) }, gridKey));
40
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from "react";
2
+ import type { FormValues } from "../headless/index.ts";
3
+ import type { FieldsSection, SectionRenderContext } from "./render-edit-render-model.ts";
4
+ export declare function renderFieldsSection<TValues extends FormValues, TCtx>(rc: SectionRenderContext<TValues, TCtx>, section: FieldsSection): ReactNode;
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { renderAllRequiredHint } from "./render-edit-banners.js";
3
+ import { renderFieldGrid } from "./render-edit-field-grid.js";
4
+ import { formSectionDomId } from "./render-edit-helpers.js";
5
+ function renderTabbedGroupsSection(rc, section, sectionKey) {
6
+ const { Grid, Card } = rc.model.engine.state.primitives;
7
+ const groupsGrid = (_jsx(Grid, { columns: 1, children: section.groups?.map((group, groupIndex) => (_jsx(Card, { slots: { title: group.title }, testId: `section-${sectionKey}-group-${groupIndex}`, children: renderFieldGrid(rc.model, section, group.fields, group.columns, `${sectionKey}-group-${groupIndex}-grid`, group.columnsDeclared) }, group.title))) }, `${sectionKey}-groups`));
8
+ // `actions` on a `groups` section has no group of its own to
9
+ // sit in and would need an extra title-less Card around the
10
+ // group cards (Card-in-Card) — the boot-validator rejects that
11
+ // combination in tabs mode, so `sectionActionsEl` is always
12
+ // undefined here at runtime.
13
+ return rc.wrapWizardStep(sectionKey, _jsx(Card, { options: { framed: false }, children: groupsGrid }, sectionKey));
14
+ }
15
+ function renderTabbedFieldsSection(rc, section, sectionKey) {
16
+ if (section.groups !== undefined)
17
+ return renderTabbedGroupsSection(rc, section, sectionKey);
18
+ const { Card } = rc.model.engine.state.primitives;
19
+ const { sectionActionsEl } = rc;
20
+ // No section title here — the Tab strip right above
21
+ // already names this panel, so a repeated Card title would just
22
+ // duplicate it. The header row only exists to carry the section
23
+ // description and actions, never the title text.
24
+ const cardEl = (_jsx(Card, { options: { framed: false }, ...((sectionActionsEl !== undefined || section.description !== undefined) && {
25
+ slots: {
26
+ ...(section.description !== undefined && { subtitle: section.description }),
27
+ ...(sectionActionsEl !== undefined && { headerActions: sectionActionsEl }),
28
+ },
29
+ }), testId: `section-${sectionKey}`, children: renderFieldGrid(rc.model, section, section.fields, section.columns, `${sectionKey}-grid`, section.columnsDeclared) }, sectionKey));
30
+ return rc.wrapWizardStep(sectionKey, cardEl);
31
+ }
32
+ // Suppress the section header when it would just repeat the form
33
+ // title verbatim (typical for single-section actionForms), or the
34
+ // tab label right above it. Without a Tabs primitive the sections
35
+ // render stacked, and then the titles are what tells them apart.
36
+ function resolveSectionTitle({ model, sectionIndex }, section) {
37
+ const { titles } = model;
38
+ const { state } = model.engine;
39
+ const repeatsTabLabel = state.isTabs && state.isStepped;
40
+ if (titles.titleInSettingsHeader && sectionIndex === 0)
41
+ return section.title ?? titles.formTitle;
42
+ return section.title === titles.formTitle || repeatsTabLabel ? undefined : section.title;
43
+ }
44
+ function resolveSectionSubtitle({ model, sectionIndex }, section) {
45
+ const headerFallback = model.titles.titleInSettingsHeader && sectionIndex === 0;
46
+ return section.description ?? (headerFallback ? model.titles.formSubtitle : undefined);
47
+ }
48
+ function renderDefaultFieldsSection(rc, section, sectionKey) {
49
+ const { model, sectionIndex, sectionActionsEl } = rc;
50
+ const { Section } = model.engine.state.primitives;
51
+ const sectionTitle = resolveSectionTitle(rc, section);
52
+ const sectionSubtitle = resolveSectionSubtitle(rc, section);
53
+ const sectionEl = (_jsxs(Section, { ...(model.titles.sectionNavItems.some((item) => item.id === formSectionDomId(sectionIndex)) && {
54
+ id: formSectionDomId(sectionIndex),
55
+ }), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(sectionSubtitle !== undefined && { subtitle: sectionSubtitle }), ...(section.icon !== undefined && { icon: section.icon }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(model.footer.isSettingsList && { layout: "settings-list" }), testId: `section-${sectionKey}`, children: [model.engine.sections.showsAllRequiredHint &&
56
+ model.engine.state.insideDrawer &&
57
+ sectionIndex === 0 &&
58
+ renderAllRequiredHint(model), renderFieldGrid(model, section, section.fields, section.columns, `${sectionKey}-grid`, section.columnsDeclared)] }, sectionKey));
59
+ return rc.wrapWizardStep(sectionKey, sectionEl);
60
+ }
61
+ export function renderFieldsSection(rc, section) {
62
+ if (!section.visible)
63
+ return null;
64
+ // Title-less sections would otherwise collide on key/testId, so use the index
65
+ // as a fallback.
66
+ const sectionKey = section.title ?? `section-${rc.sectionIndex}`;
67
+ // Tabs mode: the tab panel is the surface, so field sections render
68
+ // as unframed padded panels; group cards inside stay framed.
69
+ // Stays `Card`, not `Section` — Section always flattens to a
70
+ // borderless divider when rendered inside this component's own
71
+ // `<Form>` (InsideFormContext), even with `chromeless` set, so it
72
+ // cannot stand in for the tabs-mode card here.
73
+ return rc.model.engine.props.hideSectionTitles === true
74
+ ? renderTabbedFieldsSection(rc, section, sectionKey)
75
+ : renderDefaultFieldsSection(rc, section, sectionKey);
76
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from "react";
2
+ import type { FormValues } from "../headless/index.ts";
3
+ import type { RenderEditModel } from "./render-edit-render-model.ts";
4
+ export declare function renderFormActions<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>): ReactNode;
@@ -0,0 +1,66 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, STICKY_PRIMARY_ACTION_PROP, } from "../primitives.js";
3
+ import { EditSlotMount } from "./edit-mounts.js";
4
+ import { resolveExtensionEntityId } from "./render-edit-logic.js";
5
+ function renderWizardBackButton({ engine: { state, nav }, footer, }) {
6
+ const { Button } = state.primitives;
7
+ return (footer.showWizardBack && (_jsx(Button, { type: "button", variant: "secondary", icon: "chevron-left", onClick: nav.handleWizardBack, testId: "render-edit-wizard-back", [FOOTER_ACTION_ROLE_PROP]: "back", children: state.translate("kumiko.actions.back") })));
8
+ }
9
+ function renderFooterSlot({ engine: { props, state, form, draft, sections, steps }, footer, }) {
10
+ const footerSlot = props.screen.slots?.footer;
11
+ return (footerSlot !== undefined && (_jsx(EditSlotMount, { slot: footerSlot, slotName: "footer", screenId: props.screen.id, entityName: sections.vm.entityName, entityId: resolveExtensionEntityId(props.entityId, sections.vm.id), values: form.snapshot.values, markSaved: draft.markSaved, hasUnsavedChanges: footer.hasUnsavedInput, wizardStep: state.isWizard ? { index: steps.currentStep, isLast: steps.isLastWizardStep } : undefined, ...(props.screen.slots?.footerPrimary === true && { [STICKY_PRIMARY_ACTION_PROP]: true }) })));
12
+ }
13
+ function renderSaveAndCloseButton({ engine: { props, state, steps, handleSubmit }, footer, }) {
14
+ const { Button } = state.primitives;
15
+ return (state.isWizard &&
16
+ !steps.isLastWizardStep &&
17
+ footer.showsSubmit &&
18
+ !state.isCreateMode && (_jsx(Button, { type: "button", variant: "ghost", disabled: state.isSubmitting || props.disabled, onClick: () => void handleSubmit({ saveNow: true }), testId: "render-edit-wizard-save-close", [STICKY_PRIMARY_ACTION_PROP]: true, children: state.translate("kumiko.wizard.save-and-close") })));
19
+ }
20
+ function renderWizardNextButton({ engine: { state }, footer, }) {
21
+ const { Button } = state.primitives;
22
+ const { translate } = state;
23
+ return (footer.showWizardNext && (_jsx(Button, { type: "submit", variant: "primary", iconEnd: "chevron-right", testId: "render-edit-wizard-next", [FOOTER_ACTION_ROLE_PROP]: "primary",
24
+ [NARROW_LABEL_PROP]: translate("kumiko.actions.next"), children: footer.nextStepTitle !== undefined
25
+ ? translate("kumiko.wizard.next-with-title", { title: footer.nextStepTitle })
26
+ : translate("kumiko.actions.next") })));
27
+ }
28
+ function renderFooterActionButtons({ engine: { props, state, form, handleSubmit }, footer, }) {
29
+ const { Button } = state.primitives;
30
+ return (footer.showSubmit &&
31
+ props.footerActions?.map((action) => (_jsx(Button, { type: "button", variant: action.variant ?? "secondary", ...(action.icon !== undefined && { icon: action.icon }), disabled: state.isSubmitting || props.disabled, onClick: () => {
32
+ form.controller.setValues(action.patch); // @cast-boundary form-values
33
+ void handleSubmit();
34
+ }, testId: `render-edit-footer-action-${action.id}`, children: state.translate(action.label) }, action.id))));
35
+ }
36
+ function renderDiscardButton({ engine: { props, state, form }, footer, }) {
37
+ const { Button } = state.primitives;
38
+ const { unsavedCount } = footer;
39
+ return (footer.hasDirtyFooter &&
40
+ footer.showsSubmit &&
41
+ unsavedCount !== undefined &&
42
+ unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: state.isSubmitting || props.disabled, onClick: () => form.controller.reset(), testId: "render-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: state.translate("kumiko.form.discard") })));
43
+ }
44
+ function isSubmitDisabled({ engine: { props, state, form }, footer, }) {
45
+ const isPristine = form.snapshot.isUnchanged &&
46
+ !state.extensionDirty &&
47
+ !footer.isFieldlessSubmitForm &&
48
+ !state.insideDrawer &&
49
+ props.submitWithoutChanges !== true;
50
+ return isPristine || state.isSubmitting || props.disabled || props.submitBlocked;
51
+ }
52
+ function resolveSubmitLabelKey({ engine: { props, state }, footer, }) {
53
+ if (props.submitLabel !== undefined)
54
+ return props.submitLabel;
55
+ if (state.isWizard)
56
+ return "kumiko.actions.finish";
57
+ return footer.hasDirtyFooter ? "kumiko.form.saveChanges" : "kumiko.actions.save";
58
+ }
59
+ function renderSubmitButton(model) {
60
+ const { props, state } = model.engine;
61
+ const { Button } = state.primitives;
62
+ return (model.footer.showSubmit && (_jsx(Button, { type: "submit", disabled: isSubmitDisabled(model), loading: state.isSubmitting, variant: props.submitVariant ?? "primary", testId: "render-edit-submit", children: state.translate(resolveSubmitLabelKey(model)) })));
63
+ }
64
+ export function renderFormActions(model) {
65
+ return (_jsxs(_Fragment, { children: [renderWizardBackButton(model), renderFooterSlot(model), renderSaveAndCloseButton(model), renderWizardNextButton(model), renderFooterActionButtons(model), renderDiscardButton(model), renderSubmitButton(model), model.engine.sections.writeFormFooterAction] }));
66
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from "react";
2
+ import type { FormValues } from "../headless/index.ts";
3
+ import type { ActionMenuItemSpec } from "../primitives.tsx";
4
+ import type { RenderEditModel } from "./render-edit-render-model.ts";
5
+ export type HeaderActionsModel = {
6
+ readonly headerMenuItems: readonly ActionMenuItemSpec[];
7
+ readonly secondaryFormActions: ReactNode;
8
+ readonly formHeaderRegion: ReactNode;
9
+ readonly titleActionMount: ReactNode;
10
+ };
11
+ export declare function buildHeaderActions<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>): HeaderActionsModel;
@@ -0,0 +1,120 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { EditSlotMount } from "./edit-mounts.js";
3
+ import { RenderEditActionButton } from "./render-edit-action-button.js";
4
+ import { resolveExtensionEntityId } from "./render-edit-logic.js";
5
+ function copyLinkMenuItems({ engine: { props, state }, }) {
6
+ const { onCopyLink } = props;
7
+ if (onCopyLink === undefined)
8
+ return [];
9
+ return [
10
+ {
11
+ id: "copy-link",
12
+ label: state.translate(state.linkCopied ? "kumiko.actions.copyLinkCopied" : "kumiko.actions.copyLink"),
13
+ icon: "link",
14
+ onSelect: () => {
15
+ void Promise.resolve(onCopyLink()).then(() => state.setLinkCopied(true));
16
+ },
17
+ },
18
+ ];
19
+ }
20
+ function cancelMenuItems({ engine: { props, state }, layout, }) {
21
+ const { onCancel } = props;
22
+ if (!layout.cancelInHeaderMenu || onCancel === undefined)
23
+ return [];
24
+ return [
25
+ {
26
+ id: "cancel",
27
+ label: state.translate("kumiko.actions.cancel"),
28
+ icon: "x",
29
+ onSelect: () => onCancel(),
30
+ },
31
+ ];
32
+ }
33
+ function deleteMenuItems({ engine: { props, state }, }) {
34
+ if (props.onDelete === undefined)
35
+ return [];
36
+ return [
37
+ {
38
+ id: "delete",
39
+ label: state.translate("kumiko.actions.delete"),
40
+ icon: "trash",
41
+ variant: "danger",
42
+ onSelect: () => state.setConfirmDeleteOpen(true),
43
+ },
44
+ ];
45
+ }
46
+ function buildHeaderMenuItems(model) {
47
+ if (!model.layout.menuPageHeaderActions)
48
+ return [];
49
+ const { menuActions } = model.engine.props;
50
+ return [
51
+ ...(menuActions ?? []).map(model.engine.state.menuActionToItem),
52
+ ...copyLinkMenuItems(model),
53
+ ...cancelMenuItems(model),
54
+ ...deleteMenuItems(model),
55
+ ];
56
+ }
57
+ function renderDeleteButton({ engine: { props, state }, layout, }) {
58
+ const { Button } = state.primitives;
59
+ return (props.onDelete !== undefined &&
60
+ !layout.menuPageHeaderActions && (_jsx(Button, { type: "button", variant: "danger-ghost", icon: "trash", testId: "render-edit-delete", disabled: props.disabled, onClick: () => state.setConfirmDeleteOpen(true), children: state.translate("kumiko.actions.delete") })));
61
+ }
62
+ function renderCopyLinkButton({ engine: { props, state }, layout, }) {
63
+ const { Button } = state.primitives;
64
+ const { onCopyLink } = props;
65
+ const { iconOnlyMidActions } = layout;
66
+ const { translate, linkCopied } = state;
67
+ return (onCopyLink !== undefined &&
68
+ !layout.menuPageHeaderActions && (_jsx(Button, { type: "button", variant: iconOnlyMidActions ? "secondary" : "link", icon: linkCopied ? "check" : "link", testId: "render-edit-copy-link", ...(iconOnlyMidActions && {
69
+ size: "icon",
70
+ ariaLabel: translate(linkCopied ? "kumiko.actions.copyLinkCopied" : "kumiko.actions.copyLink"),
71
+ }), onClick: async () => {
72
+ await onCopyLink();
73
+ state.setLinkCopied(true);
74
+ }, children: iconOnlyMidActions
75
+ ? null
76
+ : translate(linkCopied ? "kumiko.actions.copyLinkCopied" : "kumiko.actions.copyLink") })));
77
+ }
78
+ function renderCustomActions({ engine: { props, state }, layout, }) {
79
+ const { Button, Dialog } = state.primitives;
80
+ return props.actions?.map((action) => (_jsx(RenderEditActionButton, { action: action, iconOnly: layout.iconOnlyMidActions, Button: Button, Dialog: Dialog, onError: state.setActionError }, action.id)));
81
+ }
82
+ function renderMenuActionButtons({ engine: { props, state }, layout, }) {
83
+ const { Button, Dialog } = state.primitives;
84
+ return (!layout.menuPageHeaderActions &&
85
+ props.menuActions?.map((action) => (_jsx(RenderEditActionButton, { action: action, Button: Button, Dialog: Dialog, onError: state.setActionError }, action.id))));
86
+ }
87
+ function renderCancelButton({ engine: { props, state }, layout, }) {
88
+ const { Button } = state.primitives;
89
+ const { onCancel } = props;
90
+ return (onCancel !== undefined &&
91
+ !layout.cancelInHeaderMenu && (_jsx(Button, { type: "button", variant: "secondary", onClick: () => onCancel(), testId: "render-edit-cancel", children: state.translate("kumiko.actions.cancel") })));
92
+ }
93
+ function renderSecondaryFormActions(model) {
94
+ return (_jsxs(_Fragment, { children: [renderDeleteButton(model), renderCopyLinkButton(model), renderCustomActions(model), renderMenuActionButtons(model), renderCancelButton(model)] }));
95
+ }
96
+ function renderEditSlot({ engine: { props, form, sections } }, slot, slotName) {
97
+ if (slot === undefined)
98
+ return undefined;
99
+ return (_jsx(EditSlotMount, { slot: slot, slotName: slotName, screenId: props.screen.id, entityName: sections.vm.entityName, entityId: resolveExtensionEntityId(props.entityId, sections.vm.id), values: form.snapshot.values }));
100
+ }
101
+ // A function headerRegion places the slot itself (projectionDetail puts it
102
+ // into its header card next to the actions); otherwise the slot renders
103
+ // above the caller's own headerRegion.
104
+ function placeHeaderSlot(headerRegion, headerSlotMount) {
105
+ if (typeof headerRegion === "function")
106
+ return headerRegion(headerSlotMount);
107
+ if (headerSlotMount === undefined)
108
+ return headerRegion;
109
+ return (_jsxs(_Fragment, { children: [headerSlotMount, headerRegion] }));
110
+ }
111
+ export function buildHeaderActions(model) {
112
+ const { props } = model.engine;
113
+ const headerSlotMount = renderEditSlot(model, props.screen.slots?.header, "header");
114
+ return {
115
+ headerMenuItems: buildHeaderMenuItems(model),
116
+ secondaryFormActions: renderSecondaryFormActions(model),
117
+ formHeaderRegion: placeHeaderSlot(props.headerRegion, headerSlotMount),
118
+ titleActionMount: renderEditSlot(model, props.screen.slots?.titleAction, "titleAction"),
119
+ };
120
+ }
@@ -0,0 +1,29 @@
1
+ import type { useExtensionSectionComponent } from "../app/extension-sections.tsx";
2
+ import type { FieldIssue, FormValues } from "../headless/index.ts";
3
+ import type { RenderEditEngine } from "./render-edit-render-model.ts";
4
+ export type LayoutFlags = {
5
+ readonly rootIssues: readonly FieldIssue[];
6
+ readonly isScreenForm: boolean;
7
+ readonly titleInShell: boolean;
8
+ readonly menuPageHeaderActions: boolean;
9
+ readonly cancelInHeaderMenu: boolean;
10
+ readonly hasSecondaryFormActions: boolean;
11
+ readonly iconOnlyMidActions: boolean;
12
+ };
13
+ export type FooterFlags = {
14
+ readonly isFieldlessSubmitForm: boolean;
15
+ readonly showsSubmit: boolean;
16
+ readonly showWizardBack: boolean;
17
+ readonly showWizardNext: boolean;
18
+ readonly showSubmit: boolean;
19
+ readonly hasFormActions: boolean;
20
+ readonly nextStepTitle: string | undefined;
21
+ readonly isCreate: boolean;
22
+ readonly hasDirtyFooter: boolean;
23
+ readonly unsavedCount: number | undefined;
24
+ readonly isSettingsList: boolean;
25
+ readonly hasUnsavedInput: boolean;
26
+ };
27
+ export type FooterSlotComponent = ReturnType<typeof useExtensionSectionComponent>;
28
+ export declare function deriveLayoutFlags<TValues extends FormValues, TCtx>({ props, state, form, }: RenderEditEngine<TValues, TCtx>): LayoutFlags;
29
+ export declare function deriveFooterFlags<TValues extends FormValues, TCtx>(engine: RenderEditEngine<TValues, TCtx>, layout: LayoutFlags, footerSlotComponent: FooterSlotComponent): FooterFlags;
@@ -0,0 +1,106 @@
1
+ import { PROJECTION_DETAIL_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { shouldRenderActionsIconOnly } from "../primitives.js";
3
+ import { ROOT_ISSUE_PATH, resolveExtensionEntityId } from "./render-edit-logic.js";
4
+ function hasSecondaryFormActions(props, menuPageHeaderActions, cancelInHeaderMenu) {
5
+ const { onDelete, onCopyLink, actions, menuActions, onCancel } = props;
6
+ const hasRecordActions = !menuPageHeaderActions && (onDelete !== undefined || onCopyLink !== undefined);
7
+ const hasCustomActions = actions !== undefined && actions.length > 0;
8
+ const hasMenuActions = !menuPageHeaderActions && menuActions !== undefined && menuActions.length > 0;
9
+ const hasCancel = onCancel !== undefined && !cancelInHeaderMenu;
10
+ return hasRecordActions || hasCustomActions || hasMenuActions || hasCancel;
11
+ }
12
+ export function deriveLayoutFlags({ props, state, form, }) {
13
+ const { fillScreenHeight, hideSectionTitles, headerRegion, actions, onCancel } = props;
14
+ const { PageHeader, ActionOverflowMenu } = state.primitives;
15
+ // Screen forms (entityEdit/actionForm filling the shell height) drop the card;
16
+ // the title then lives in the shell header when the shell offers a slot.
17
+ const rootIssues = form.snapshot.errors[ROOT_ISSUE_PATH] ?? [];
18
+ const isScreenForm = fillScreenHeight === true && hideSectionTitles !== true && !state.insideDrawer;
19
+ const titleInShell = PageHeader !== undefined &&
20
+ state.pageHeaderSlotAvailable &&
21
+ hideSectionTitles !== true &&
22
+ headerRegion === undefined;
23
+ const menuPageHeaderActions = titleInShell && isScreenForm && ActionOverflowMenu !== undefined;
24
+ const cancelInHeaderMenu = menuPageHeaderActions && state.isWizard && onCancel !== undefined;
25
+ return {
26
+ rootIssues,
27
+ isScreenForm,
28
+ titleInShell,
29
+ menuPageHeaderActions,
30
+ cancelInHeaderMenu,
31
+ // Two groups — wizard navigation on the right/top, record actions on the
32
+ // left/below; destructive action sits outermost, farthest from the target
33
+ // action.
34
+ hasSecondaryFormActions: hasSecondaryFormActions(props, menuPageHeaderActions, cancelInHeaderMenu),
35
+ // Teil-C collapse rule: driven by the custom `actions` group only — a
36
+ // resolved true also folds the Copy-Link button (same visual group in the
37
+ // footer), but never Delete/Cancel, which keep their text regardless.
38
+ iconOnlyMidActions: actions !== undefined && shouldRenderActionsIconOnly(actions),
39
+ };
40
+ }
41
+ function isCreateRecord(entityIdProp, vmId) {
42
+ const id = resolveExtensionEntityId(entityIdProp, vmId);
43
+ return id == null || id === "";
44
+ }
45
+ function resolveUnsavedCount(hasDirtyFooter, changedFieldCount, extensionDirty) {
46
+ if (!hasDirtyFooter)
47
+ return undefined;
48
+ if (changedFieldCount > 0)
49
+ return changedFieldCount;
50
+ return extensionDirty ? 1 : 0;
51
+ }
52
+ function showsSubmitButton({ props, state, sections }, isFieldlessSubmitForm) {
53
+ return (!props.viewMode &&
54
+ (sections.isFormEditable ||
55
+ state.hasExtensionRegistrations ||
56
+ isFieldlessSubmitForm ||
57
+ props.submitWithoutChanges !== undefined));
58
+ }
59
+ function deriveFormActionFlags({ state, sections, steps }, showsSubmit, footerSlotComponent) {
60
+ const { isWizard } = state;
61
+ // Mirrors every branch inside formActions below — without this guard
62
+ // DefaultForm renders an empty footer strip (border + padding, no content)
63
+ // on read-only detail screens, since `actions` would otherwise always be
64
+ // a defined (if empty) fragment.
65
+ const showWizardBack = isWizard && steps.currentStep > 0;
66
+ const showWizardNext = isWizard && !steps.isLastWizardStep;
67
+ const showSubmit = showsSubmit && (!isWizard || steps.isLastWizardStep);
68
+ const hasFormActions = showWizardBack ||
69
+ showWizardNext ||
70
+ showSubmit ||
71
+ footerSlotComponent !== undefined ||
72
+ sections.writeFormFooterAction !== undefined;
73
+ return { showWizardBack, showWizardNext, showSubmit, hasFormActions };
74
+ }
75
+ function deriveHasDirtyFooter({ props, state }, layout, isCreate) {
76
+ // Edit-mode screen forms always carry the dirty status (0 = quiet "No unsaved
77
+ // changes"); wizards keep the status-only-when-dirty behaviour.
78
+ return (!state.isWizard &&
79
+ !props.viewMode &&
80
+ (props.dirtyFooter === true ||
81
+ (layout.isScreenForm && !isCreate && props.headerRegion === undefined)));
82
+ }
83
+ export function deriveFooterFlags(engine, layout, footerSlotComponent) {
84
+ const { props, state, form, sections, steps } = engine;
85
+ // A projectionDetail's synthesized entity is empty when every section is an
86
+ // extension or relatedList; that is not a fieldless submit form, so it must not
87
+ // draw a no-op Save. Extension registrations still get their Save, since
88
+ // persistExtensions() runs on the customSubmit path too.
89
+ const isFieldlessSubmitForm = sections.isFieldless && props.screen.entity !== PROJECTION_DETAIL_ENTITY;
90
+ const showsSubmit = showsSubmitButton(engine, isFieldlessSubmitForm);
91
+ const isCreate = isCreateRecord(props.entityId, sections.vm.id);
92
+ const hasDirtyFooter = deriveHasDirtyFooter(engine, layout, isCreate);
93
+ return {
94
+ isFieldlessSubmitForm,
95
+ showsSubmit,
96
+ ...deriveFormActionFlags(engine, showsSubmit, footerSlotComponent),
97
+ nextStepTitle: state.isWizard
98
+ ? sections.filteredSections[steps.currentStep + 1]?.title
99
+ : undefined,
100
+ isCreate,
101
+ hasDirtyFooter,
102
+ unsavedCount: resolveUnsavedCount(hasDirtyFooter, Object.keys(form.snapshot.changes).length, state.extensionDirty),
103
+ isSettingsList: props.screen.layout.variant === "settings-list" && props.hideSectionTitles !== true,
104
+ hasUnsavedInput: form.snapshot.isDirty || state.extensionDirty,
105
+ };
106
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from "react";
2
+ import type { FormValues } from "../headless/index.ts";
3
+ import type { RenderEditModel } from "./render-edit-render-model.ts";
4
+ export declare function renderEditLayout<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>): ReactNode;