@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
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ExtensionFormRegistryProvider } from "../app/extension-form-submit.js";
3
+ import { renderActionErrorBanner, renderAllRequiredHint, renderDeleteDialog, renderDraftPicker, renderExtensionErrorBanner, renderFormErrorBanner, renderRootIssueBanners, renderTabStrip, renderWizardProgress, } from "./render-edit-banners.js";
4
+ import { renderFormActions } from "./render-edit-form-actions.js";
5
+ import { buildHeaderActions } from "./render-edit-header-actions.js";
6
+ import { renderEditSections } from "./render-edit-section-kinds.js";
7
+ import { AllFieldsRequiredProvider } from "./render-field.js";
8
+ function renderPageHeader({ engine: { state }, layout, titles }, { headerMenuItems }) {
9
+ const { PageHeader, ActionOverflowMenu } = state.primitives;
10
+ const { pageHeaderCompact } = state;
11
+ return (layout.titleInShell &&
12
+ PageHeader !== undefined && (_jsx(PageHeader, { title: titles.formTitle, ...(titles.recordTitle !== undefined && { recordTitle: titles.recordTitle }), ...(headerMenuItems.length > 0 && pageHeaderCompact && { overflowItems: headerMenuItems }), ...(headerMenuItems.length > 0 &&
13
+ !pageHeaderCompact &&
14
+ ActionOverflowMenu !== undefined && {
15
+ actions: (_jsx(ActionOverflowMenu, { label: state.translate("kumiko.list.row-actions.more"), items: headerMenuItems, testId: "render-edit-header-menu" })),
16
+ }) })));
17
+ }
18
+ function renderWizardSideRail({ engine: { state, steps, sections, nav }, titles, }) {
19
+ const { StepBar } = state.primitives;
20
+ const { translate } = state;
21
+ const { upNextSection, formSubtitle } = titles;
22
+ if (StepBar === undefined)
23
+ return undefined;
24
+ return (_jsx(StepBar, { steps: sections.filteredSections.map((section) => section.title ?? ""), currentIndex: steps.currentStep, compactLabel: titles.compactStepLabel, onStepSelect: nav.handleWizardStepSelect, ...steps.updateModeStepBarProps, orientation: "vertical", heading: translate("kumiko.wizard.step", {
25
+ current: steps.currentStep + 1,
26
+ total: steps.lastStepIndex + 1,
27
+ }), ...(formSubtitle !== undefined && { description: formSubtitle }), subtitles: sections.filteredSections.map((section) => section.kind === "fields" ? section.subtitle : undefined), ...(upNextSection !== undefined && {
28
+ upNext: {
29
+ heading: translate("kumiko.wizard.up-next"),
30
+ title: upNextSection.title ?? "",
31
+ ...(upNextSection.kind === "fields" &&
32
+ upNextSection.subtitle !== undefined && { subtitle: upNextSection.subtitle }),
33
+ },
34
+ }), testId: "render-edit-wizard-steps", compactTestId: "render-edit-wizard-step-label" }));
35
+ }
36
+ function buildFormHeadingProps(model) {
37
+ const { props } = model.engine;
38
+ const { layout, footer, titles } = model;
39
+ const { unsavedCount, hasDirtyFooter } = footer;
40
+ return {
41
+ ...(props.hideSectionTitles !== true &&
42
+ !layout.titleInShell &&
43
+ !titles.titleInSettingsHeader &&
44
+ !titles.recordTitleInHeaderRegion && { title: titles.formTitle }),
45
+ ...(titles.formSubtitle !== undefined &&
46
+ !titles.stepRail &&
47
+ !titles.titleInSettingsHeader && { subtitle: titles.formSubtitle }),
48
+ ...(layout.isScreenForm && { screenForm: true }),
49
+ ...(unsavedCount !== undefined && (unsavedCount > 0 || hasDirtyFooter) && { unsavedCount }),
50
+ ...(titles.sectionNavItems.length > 0 && { sectionNav: titles.sectionNavItems }),
51
+ ...(titles.stepRail && { sideRail: renderWizardSideRail(model) }),
52
+ };
53
+ }
54
+ function buildFormActionProps(model, header) {
55
+ const { hideActions } = model.engine.props;
56
+ const { footer, layout } = model;
57
+ return {
58
+ ...(hideActions !== true && footer.hasFormActions && { actions: renderFormActions(model) }),
59
+ ...(hideActions !== true &&
60
+ layout.hasSecondaryFormActions && { secondaryActions: header.secondaryFormActions }),
61
+ };
62
+ }
63
+ function buildFormChromeProps({ engine: { props, sections } }, { formHeaderRegion, titleActionMount }) {
64
+ const { summary } = props;
65
+ return {
66
+ ...(props.screen.layout.width !== undefined && { width: props.screen.layout.width }),
67
+ ...(formHeaderRegion !== undefined && { headerRegion: formHeaderRegion }),
68
+ ...(titleActionMount !== undefined && { titleAction: titleActionMount }),
69
+ ...(sections.fillHeight && { fillHeight: sections.fillHeight }),
70
+ ...(props.hideSectionTitles === true && { chromeless: true }),
71
+ ...(summary !== undefined && { summary }),
72
+ };
73
+ }
74
+ export function renderEditLayout(model) {
75
+ const { props, state, sections, handleSubmit } = model.engine;
76
+ const header = buildHeaderActions(model);
77
+ const { Form } = state.primitives;
78
+ return (_jsxs(ExtensionFormRegistryProvider, { value: state.extensionFormRegistry, children: [state.menuActionConfirmDialog, renderPageHeader(model, header), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(state.focusFirstErrorKey > 0 && { focusFirstErrorKey: state.focusFirstErrorKey }), ...buildFormHeadingProps(model), ...buildFormActionProps(model, header), testId: "render-edit-form", stickyActions: state.isWizard || props.fillScreenHeight === true, ...buildFormChromeProps(model, header), children: _jsxs(AllFieldsRequiredProvider, { value: sections.showsAllRequiredHint, children: [props.leadContent, sections.showsAllRequiredHint && !state.insideDrawer && renderAllRequiredHint(model), renderDraftPicker(model), renderWizardProgress(model), state.isTabs && state.isStepped && renderTabStrip(model), renderEditSections(model), renderFormErrorBanner(model), renderRootIssueBanners(model), renderExtensionErrorBanner(model), renderActionErrorBanner(model), renderDeleteDialog(model)] }) })] }));
79
+ }
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from "react";
2
+ import type { EditSectionViewModel, EditViewModel, FormValues } from "../headless/index.ts";
3
+ import type { FooterFlags, LayoutFlags } from "./render-edit-layout-flags.ts";
4
+ import type { TitleModel } from "./render-edit-titles.ts";
5
+ import type { RenderEditProps } from "./render-edit-types.ts";
6
+ import type { WizardNavigation } from "./render-edit-wizard-navigation.ts";
7
+ import type { RenderEditDraft } from "./use-render-edit-draft-save.ts";
8
+ import type { RenderEditForm } from "./use-render-edit-form.ts";
9
+ import type { RenderEditSections } from "./use-render-edit-sections.ts";
10
+ import type { RenderEditState } from "./use-render-edit-state.ts";
11
+ import type { RenderEditSteps } from "./use-render-edit-steps.ts";
12
+ import type { buildWizardDraftActions } from "./use-wizard-form-draft.ts";
13
+ type ResolvedKeys = "payloadMode" | "markChangedFields" | "disabled" | "viewMode" | "valueDisplay" | "submitBlocked" | "i18nScreenId";
14
+ export type ResolvedRenderEditProps<TValues extends FormValues, TCtx = unknown> = Omit<RenderEditProps<TValues, TCtx>, ResolvedKeys> & {
15
+ readonly payloadMode: "values" | "changes";
16
+ readonly markChangedFields: boolean;
17
+ readonly disabled: boolean;
18
+ readonly viewMode: boolean;
19
+ readonly valueDisplay: "form" | "text";
20
+ readonly submitBlocked: boolean;
21
+ readonly i18nScreenId: string;
22
+ };
23
+ export declare function resolveRenderEditProps<TValues extends FormValues, TCtx>(props: RenderEditProps<TValues, TCtx>): ResolvedRenderEditProps<TValues, TCtx>;
24
+ export type RenderEditCore<TValues extends FormValues, TCtx = unknown> = {
25
+ readonly props: ResolvedRenderEditProps<TValues, TCtx>;
26
+ readonly state: RenderEditState;
27
+ readonly form: RenderEditForm<TValues>;
28
+ readonly draft: RenderEditDraft<TValues>;
29
+ };
30
+ export type RenderEditSectioned<TValues extends FormValues, TCtx = unknown> = RenderEditCore<TValues, TCtx> & {
31
+ readonly sections: RenderEditSections;
32
+ };
33
+ export type RenderEditSteppedCore<TValues extends FormValues, TCtx = unknown> = RenderEditSectioned<TValues, TCtx> & {
34
+ readonly steps: RenderEditSteps;
35
+ };
36
+ export type WizardDraftActions<TValues extends FormValues> = ReturnType<typeof buildWizardDraftActions<TValues>>;
37
+ export type RenderEditEngineBase<TValues extends FormValues, TCtx = unknown> = RenderEditSteppedCore<TValues, TCtx> & {
38
+ readonly draftActions: WizardDraftActions<TValues>;
39
+ readonly nav: WizardNavigation;
40
+ };
41
+ export type RenderEditEngine<TValues extends FormValues, TCtx = unknown> = RenderEditEngineBase<TValues, TCtx> & {
42
+ readonly handleSubmit: (options?: {
43
+ readonly saveNow?: boolean;
44
+ }) => Promise<void>;
45
+ };
46
+ export type { EditSectionViewModel, EditViewModel };
47
+ export type RenderEditModel<TValues extends FormValues, TCtx = unknown> = {
48
+ readonly engine: RenderEditEngine<TValues, TCtx>;
49
+ readonly layout: LayoutFlags;
50
+ readonly footer: FooterFlags;
51
+ readonly titles: TitleModel;
52
+ readonly setFieldUntyped: (k: string, v: unknown) => void;
53
+ readonly patchForExtension: (partial: Readonly<Record<string, unknown>>) => void;
54
+ };
55
+ export type SectionRenderContext<TValues extends FormValues, TCtx = unknown> = {
56
+ readonly model: RenderEditModel<TValues, TCtx>;
57
+ readonly sectionIndex: number;
58
+ readonly sectionActionsEl: ReactNode;
59
+ readonly wrapWizardStep: (key: string, el: ReactNode) => ReactNode;
60
+ };
61
+ export type FieldsSection = Extract<EditSectionViewModel, {
62
+ readonly kind: "fields";
63
+ }>;
@@ -0,0 +1,14 @@
1
+ // View mode is a locked form, so everything keyed on `disabled` follows.
2
+ export function resolveRenderEditProps(props) {
3
+ const viewMode = props.viewMode ?? false;
4
+ return {
5
+ ...props,
6
+ payloadMode: props.payloadMode ?? "values",
7
+ markChangedFields: props.markChangedFields ?? false,
8
+ viewMode,
9
+ disabled: (props.disabled ?? false) || viewMode,
10
+ valueDisplay: props.valueDisplay ?? "form",
11
+ submitBlocked: props.submitBlocked ?? false,
12
+ i18nScreenId: props.i18nScreenId ?? props.screen.id,
13
+ };
14
+ }
@@ -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 renderEditSections<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>): ReactNode;
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ExtensionSectionMount } from "./edit-mounts.js";
3
+ import { RelatedListSection } from "./related-list-section.js";
4
+ import { RenderEditActionButton } from "./render-edit-action-button.js";
5
+ import { renderFieldsSection } from "./render-edit-fields-section.js";
6
+ import { wizardStepKey } from "./render-edit-helpers.js";
7
+ import { resolveExtensionEntityId } from "./render-edit-logic.js";
8
+ import { WriteFormSection } from "./write-form-section.js";
9
+ function renderExtensionSection({ model, sectionIndex, sectionActionsEl, wrapWizardStep }, section) {
10
+ const { props, state, form, draft, sections } = model.engine;
11
+ const { hideSectionTitles } = props;
12
+ const { Card } = state.primitives;
13
+ const mount = (_jsx(ExtensionSectionMount, { section: section, entityName: sections.vm.entityName, entityId: resolveExtensionEntityId(props.entityId, sections.vm.id), initialValues: props.extensionInitialValues, values: form.snapshot.values, patch: model.patchForExtension, validate: draft.scopedValidate, markSaved: draft.markSaved, hideTitle: hideSectionTitles, ...(state.isWizard &&
14
+ !state.isCreateMode && {
15
+ stepKey: wizardStepKey(section, sectionIndex),
16
+ onReportStepComplete: state.reportExtensionStepComplete,
17
+ }), ...(hideSectionTitles !== true &&
18
+ sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title));
19
+ // Tabs mode: an unframed padded panel (the tab panel is the
20
+ // surface), so extension content neither sits flush at the shell
21
+ // edge nor touches the tab strip. testId stays only on the inner
22
+ // Section — giving the wrapper the same one would register two
23
+ // elements under one id.
24
+ const wrapped = hideSectionTitles !== true ? (mount) : (_jsx(Card, { options: { framed: false }, ...(sectionActionsEl !== undefined && {
25
+ slots: { headerActions: sectionActionsEl },
26
+ }), children: mount }, section.title));
27
+ return wrapWizardStep(section.title, wrapped);
28
+ }
29
+ function renderRelatedListSection({ model, sectionActionsEl }, section) {
30
+ const { props, form, sections } = model.engine;
31
+ const { hideSectionTitles, fillScreenHeight, refreshToken, onRelatedListDrawerAction } = props;
32
+ // parentId is the displayed record's id — without it there's no
33
+ // parent row whose related rows could be queried.
34
+ // Rejected at boot in wizard layouts, so no WizardStepGroup here.
35
+ const parentId = resolveExtensionEntityId(props.entityId, sections.vm.id);
36
+ if (parentId === null)
37
+ return null;
38
+ const mount = (_jsx(RelatedListSection, { section: section, parentId: parentId,
39
+ // @cast-boundary form-values: TValues is structurally a Record.
40
+ record: form.snapshot.values, featureName: props.featureName, translate: model.engine.state.translate, hideTitle: hideSectionTitles, ...(refreshToken !== undefined && { refreshToken }), ...(fillScreenHeight === true && { grow: true }), ...(hideSectionTitles !== true &&
41
+ sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(onRelatedListDrawerAction !== undefined && {
42
+ onOpenDrawer: onRelatedListDrawerAction,
43
+ }) }, section.title));
44
+ // Tabs mode: the table runs full-bleed under the tab strip (board
45
+ // layout); the toolbar buttons live in the list's own toolbar.
46
+ return hideSectionTitles === true ? (_jsx("div", { className: fillScreenHeight === true ? "flex min-h-0 flex-1 flex-col" : undefined, children: mount }, section.title)) : (mount);
47
+ }
48
+ function renderWriteFormSection({ model, sectionIndex, sectionActionsEl }, section) {
49
+ const { props, state, sections } = model.engine;
50
+ // Own submit button + dispatcher call, entirely independent of
51
+ // this screen's (nonexistent, on projectionDetail) form submit —
52
+ // rejected at boot in wizard layouts, so no WizardStepGroup here.
53
+ return (_jsx(WriteFormSection, { section: section, featureName: props.featureName, translate: state.translate, hideTitle: props.hideSectionTitles, onSubmitted: () => props.onReload?.(), ...(sections.isLoneWriteFormTab && { onFooterAction: sections.setWriteFormFooterAction }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title ?? `write-form-${sectionIndex}`));
54
+ }
55
+ function renderSectionByKind(rc, section) {
56
+ switch (section.kind) {
57
+ case "extension":
58
+ return renderExtensionSection(rc, section);
59
+ case "relatedList":
60
+ return renderRelatedListSection(rc, section);
61
+ case "writeForm":
62
+ return renderWriteFormSection(rc, section);
63
+ case "fields":
64
+ return renderFieldsSection(rc, section);
65
+ }
66
+ }
67
+ // turns this section's own `actions` (RowAction[]) into
68
+ // already-bound buttons via the caller-supplied builder, rendered
69
+ // through the same RenderEditActionButton the top-level `actions`
70
+ // prop uses (same style/confirm/icon/error handling).
71
+ function renderSectionActions({ engine: { props, state } }, section) {
72
+ const sectionActions = section.actions;
73
+ if (sectionActions === undefined || sectionActions.length === 0)
74
+ return undefined;
75
+ const { Button, Dialog } = state.primitives;
76
+ return props
77
+ .buildSectionActions?.(sectionActions)
78
+ ?.map((action) => (_jsx(RenderEditActionButton, { action: action, Button: Button, Dialog: Dialog, onError: state.setActionError }, action.id)));
79
+ }
80
+ // Wizard steps and tabs stay mounted while off-screen (native
81
+ // `hidden`, not unmounted) so an extension section's submit-registry
82
+ // entry (useExtensionFormSubmit → registry.remove on unmount)
83
+ // survives navigating past its step — otherwise Finish only ran the
84
+ // last mounted step's handler and silently dropped earlier steps'
85
+ // writes. For tabs this is also what makes ONE submit cover every
86
+ // tab, including tabs the user never opened.
87
+ // Off-screen wizard steps stay mounted (see comment above) but must
88
+ // not participate in native constraint validation, or the Next
89
+ // button's `type="submit"` triggers the browser's full-form check
90
+ // — including required fields on unvisited steps, which are not
91
+ // focusable while hidden, so the wizard silently stops navigating
92
+ // (only a console warning, no visible error). A plain `hidden`
93
+ // attribute does NOT bar descendants from constraint validation on
94
+ // web — see WizardStepGroupProps for what implementations must
95
+ // guarantee.
96
+ function createWizardStepWrapper({ engine: { props, state, steps } }, sectionIndex) {
97
+ const { WizardStepGroup } = state.primitives;
98
+ const stepHidden = state.isStepped && sectionIndex !== steps.currentStep;
99
+ return (key, el) => {
100
+ if (!state.isStepped)
101
+ return el;
102
+ if (WizardStepGroup === undefined) {
103
+ throw new Error(`RenderEdit: ${props.screen.layout.mode} layout requires primitives.WizardStepGroup, but none is registered.`);
104
+ }
105
+ return (_jsx(WizardStepGroup, { hidden: stepHidden, ...(props.hideSectionTitles === true && { inset: true }), children: el }, key));
106
+ };
107
+ }
108
+ export function renderEditSections(model) {
109
+ return model.engine.sections.filteredSections.map((section, sectionIndex) => renderSectionByKind({
110
+ model,
111
+ sectionIndex,
112
+ sectionActionsEl: renderSectionActions(model, section),
113
+ wrapWizardStep: createWizardStepWrapper(model, sectionIndex),
114
+ }, section));
115
+ }
@@ -0,0 +1,8 @@
1
+ import type { FormValues, SubmitResult } from "../headless/index.ts";
2
+ import type { RenderEditEngineBase } from "./render-edit-render-model.ts";
3
+ export declare function createSubmitResultHandlers<TValues extends FormValues, TCtx>({ props, state, form, draft, sections, draftActions, nav, }: RenderEditEngineBase<TValues, TCtx>): {
4
+ handleSubmitSuccess: (isNoOp: boolean) => Promise<boolean>;
5
+ applySubmitFailure: (result: Exclude<SubmitResult<unknown>, {
6
+ isSuccess: true;
7
+ }>) => void;
8
+ };
@@ -0,0 +1,77 @@
1
+ import { groupIssuesByPath } from "../headless/index.js";
2
+ import { hasIssueWithoutRenderedField } from "./render-edit-logic.js";
3
+ export function createSubmitResultHandlers({ props, state, form, draft, sections, draftActions, nav, }) {
4
+ const { controller } = form;
5
+ const { customSubmit } = props;
6
+ const { submittedRef } = draft.refs;
7
+ // `isNoOp: true` (payloadMode "changes", pre-filled form submitted
8
+ // untouched) means controller.submit() never called dispatcher.write
9
+ // — nothing to discard, and discarding here would delete a draft the
10
+ // form never persisted. Awaited, not fire-and-forget:
11
+ // `onSubmit` typically navigates away and unmounts this form, which
12
+ // would abort an in-flight discard and leave the draft behind after
13
+ // a successful submit.
14
+ // Resolves to whether the extension sections persisted (true when none ran).
15
+ async function handleSubmitSuccess(isNoOp) {
16
+ state.setFormError(null);
17
+ state.setExtraDeleteIssues([]);
18
+ if (!isNoOp) {
19
+ submittedRef.current = true;
20
+ await draftActions.discardDraft();
21
+ }
22
+ const extensionsPersisted = await nav.persistExtensions();
23
+ if (extensionsPersisted)
24
+ state.extensionDirtyRef.current = false;
25
+ return extensionsPersisted;
26
+ }
27
+ function jumpToFirstErroringStep(fieldPaths) {
28
+ // skip: unstepped layouts have no step to jump to
29
+ if (!state.isStepped)
30
+ return;
31
+ const step = nav.findFirstErroringStep(fieldPaths);
32
+ if (step !== undefined)
33
+ nav.jumpToStep(step);
34
+ }
35
+ // Root-level `.refine()`/cross-field issues from controller.validate()
36
+ // — same "jump to the erroring step" treatment as a server field
37
+ // error below, sourced from the freshly-validated snapshot.
38
+ function handleValidationBlocked() {
39
+ state.setFocusFirstErrorKey((key) => key + 1);
40
+ jumpToFirstErroringStep(Object.keys(controller.getSnapshot().errors));
41
+ }
42
+ function handleSubmitError(error) {
43
+ const fieldIssues = error.details?.fields ?? [];
44
+ const issuePaths = fieldIssues.map((i) => i.path);
45
+ if (fieldIssues.length > 0)
46
+ state.setFocusFirstErrorKey((key) => key + 1);
47
+ // controller.submit() writes server issues into the controller itself;
48
+ // customSubmit bypasses it, so push them here to reach the fields.
49
+ if (customSubmit !== undefined && fieldIssues.length > 0) {
50
+ controller.setErrors(groupIssuesByPath(fieldIssues));
51
+ }
52
+ // A server field error on a step the form isn't currently showing is
53
+ // otherwise invisible — jump to the first step that contains one of
54
+ // the errored fields. For tabs this is what keeps the submit from
55
+ // blocking in silence, the failure mode the boot-validator used to
56
+ // prevent by rejecting the layout outright. An issue no
57
+ // rendered field can show (`version`, `id`, a root-level refine, a
58
+ // hidden field) keeps the banner, jump or not.
59
+ jumpToFirstErroringStep(issuePaths);
60
+ state.setExtraDeleteIssues([]);
61
+ state.setFormErrorSource("submit");
62
+ state.setFormError(hasIssueWithoutRenderedField(issuePaths, sections.filteredSections, new Set(Object.keys(props.formDerivation?.derive ?? {})))
63
+ ? error
64
+ : null);
65
+ }
66
+ // Form-level errors (without field-level details) go to the banner.
67
+ // Field errors flow via snapshot.errors into the individual fields.
68
+ // Synchronous on purpose: awaiting here would defer the state updates by a
69
+ // microtask and let React render intermediate states.
70
+ function applySubmitFailure(result) {
71
+ if (result.validationBlocked)
72
+ handleValidationBlocked();
73
+ else
74
+ handleSubmitError(result.error);
75
+ }
76
+ return { handleSubmitSuccess, applySubmitFailure };
77
+ }
@@ -0,0 +1,7 @@
1
+ import type { FormValues } from "../headless/index.ts";
2
+ import type { RenderEditEngineBase } from "./render-edit-render-model.ts";
3
+ type SubmitOptions = {
4
+ readonly saveNow?: boolean;
5
+ };
6
+ export declare function createHandleSubmit<TValues extends FormValues, TCtx>(engine: RenderEditEngineBase<TValues, TCtx>): (options?: SubmitOptions) => Promise<void>;
7
+ export {};
@@ -0,0 +1,113 @@
1
+ import { shouldNotifyCaller } from "./render-edit-logic.js";
2
+ import { createSubmitResultHandlers } from "./render-edit-submit-result.js";
3
+ // Rebuilt every render on purpose: the closures read this render's snapshot
4
+ // and extension state, while handleSubmitRef publishes the current one.
5
+ export function createHandleSubmit(engine) {
6
+ const { props, state, form, draft, steps, draftActions, nav } = engine;
7
+ const { handleSubmitSuccess, applySubmitFailure } = createSubmitResultHandlers(engine);
8
+ const { controller, snapshot } = form;
9
+ const { customSubmit } = props;
10
+ const { submittedRef, isSubmittingRef } = draft.refs;
11
+ // Same discard as the main path: an extension-only save is still a
12
+ // successful submit, so the draft must not survive it.
13
+ async function submitExtensionsOnly() {
14
+ // skip: an extension section failed, its error banner is showing
15
+ if (!(await nav.persistExtensions()))
16
+ return;
17
+ state.extensionDirtyRef.current = false;
18
+ submittedRef.current = true;
19
+ await draftActions.discardDraft();
20
+ }
21
+ // customSubmit path (e.g. configEdit, which performs a separate write per
22
+ // field). Validate on the client first, then call the caller; on success,
23
+ // explicitly rebase the form state because controller.submit() normally
24
+ // does this itself, and the controller otherwise knows nothing about a
25
+ // successful custom submission (isUnchanged would remain false).
26
+ //
27
+ // Read the snapshot directly from the controller rather than React state.
28
+ // With rapid fill-then-click input, React batching may not have committed
29
+ // state updates when the submit click fires. handleSubmit's closure could otherwise
30
+ // see stale snapshot.changes={} and perform no writes, then return success
31
+ // and rebase the form, making it appear that the data was saved.
32
+ // controller.getSnapshot() is always current: the controller is the
33
+ // source of truth, while React state is only a rendering mirror.
34
+ function blockedCustomSubmitResult() {
35
+ if (controller.validate(form.scopeFieldNames))
36
+ return undefined;
37
+ // Field order matters: validationBlocked:true is its own
38
+ // variant in the SubmitResult union (NOT mixed with data/error);
39
+ // TS only narrows cleanly without a discriminator fight.
40
+ const blocked = {
41
+ validationBlocked: true,
42
+ isSuccess: false,
43
+ };
44
+ return blocked;
45
+ }
46
+ // Enter in the active step triggers the native form submit (Next is
47
+ // type="submit" for Enter support) — on intermediate steps that means
48
+ // "Next", not "Save". Checked BEFORE the `disabled` guard below:
49
+ // `disabled` means "no input/no write", not "no navigation" — a
50
+ // disabled wizard must still be steppable (handleWizardNext itself
51
+ // blocks validate()/saveDraft() while disabled). `saveNow` is the explicit
52
+ // "Save and close" button and skips the step advance.
53
+ function advancesWizardInsteadOfSaving(options) {
54
+ return state.isWizard && !steps.isLastWizardStep && options?.saveNow !== true;
55
+ }
56
+ // Locked state: the submit button is visibly disabled, but a
57
+ // native form submit (Enter key) reaches this handler regardless of the
58
+ // button's disabled attribute — block it here too, not just in the UI.
59
+ // Sync guard for the in-flight part — React state `isSubmitting` is too late
60
+ // for double-clicks in the same tick (customSubmit has no submitInFlight of its own).
61
+ function isSubmitBlocked() {
62
+ return props.disabled === true || props.submitBlocked === true || isSubmittingRef.current;
63
+ }
64
+ // Awaits only where the original did: a validation-blocked customSubmit
65
+ // must settle in the same tick so setIsSubmitting(true/false) batch.
66
+ return async function handleSubmit(options) {
67
+ if (advancesWizardInsteadOfSaving(options)) {
68
+ nav.handleWizardNext();
69
+ // skip: handled as a wizard step advance
70
+ return;
71
+ }
72
+ // skip: locked, blocked or a submit is already in flight
73
+ if (isSubmitBlocked())
74
+ return;
75
+ isSubmittingRef.current = true;
76
+ state.setIsSubmitting(true);
77
+ state.setExtensionErrorKey(null);
78
+ try {
79
+ // Extension-only: only a section is dirty and the main form is unchanged.
80
+ // Skip the entity write (it would send an empty changes payload) and only
81
+ // run the section handlers.
82
+ if (snapshot.isUnchanged && state.extensionDirty) {
83
+ await submitExtensionsOnly();
84
+ // skip: extension-only save needs no entity write
85
+ return;
86
+ }
87
+ let result = customSubmit !== undefined ? blockedCustomSubmitResult() : undefined;
88
+ if (result === undefined && customSubmit !== undefined) {
89
+ result = await customSubmit(controller.getSnapshot());
90
+ if (result.isSuccess)
91
+ controller.rebase();
92
+ }
93
+ else if (result === undefined) {
94
+ result = await controller.submit();
95
+ }
96
+ let extensionsPersisted = true;
97
+ if (result.isSuccess)
98
+ extensionsPersisted = await handleSubmitSuccess(result.isNoOp === true);
99
+ else
100
+ applySubmitFailure(result);
101
+ // On entity-success-but-extension-failure the extension-error
102
+ // banner is showing — don't notify (the caller navigates away on success
103
+ // and would unmount it before the user sees the failure).
104
+ if (shouldNotifyCaller(result, extensionsPersisted)) {
105
+ props.onSubmit?.(result, controller.getSnapshot().values);
106
+ }
107
+ }
108
+ finally {
109
+ isSubmittingRef.current = false;
110
+ state.setIsSubmitting(false);
111
+ }
112
+ };
113
+ }
@@ -0,0 +1,17 @@
1
+ import type { EditSectionViewModel, FormValues } from "../headless/index.ts";
2
+ import type { FormSectionNavItem } from "../primitives.tsx";
3
+ import type { FooterFlags, LayoutFlags } from "./render-edit-layout-flags.ts";
4
+ import type { RenderEditEngine } from "./render-edit-render-model.ts";
5
+ export type TitleModel = {
6
+ readonly recordTitle: string | undefined;
7
+ readonly formTitle: string;
8
+ readonly formSubtitle: string | undefined;
9
+ readonly titleInSettingsHeader: boolean;
10
+ readonly usesFieldFlow: boolean;
11
+ readonly recordTitleInHeaderRegion: boolean;
12
+ readonly stepRail: boolean;
13
+ readonly upNextSection: EditSectionViewModel | undefined;
14
+ readonly sectionNavItems: readonly FormSectionNavItem[];
15
+ readonly compactStepLabel: string;
16
+ };
17
+ export declare function deriveTitles<TValues extends FormValues, TCtx>(engine: RenderEditEngine<TValues, TCtx>, layout: LayoutFlags, footer: FooterFlags): TitleModel;
@@ -0,0 +1,99 @@
1
+ import { formSectionDomId, resolveRecordTitle, resolveTitleTemplate, } from "./render-edit-helpers.js";
2
+ // Title + Subtitle, create/edit-aware. i18n-Keys (mode = "create"|"edit"):
3
+ // screen:<id>.<mode>.title / .<mode>.subtitle (mode = "create"|"edit"|"view")
4
+ // Fallback chain: mode-specific → generic (screen:<id>.title/.subtitle).
5
+ // title falls back to screenId; subtitle falls back to screen.description,
6
+ // itself run through translate() so an i18n key (the established
7
+ // convention for description, e.g. bundled-features) resolves instead of
8
+ // showing the raw key — translate() returns the input unchanged for plain
9
+ // prose, same as it does for an unknown key. Same slot
10
+ // section.description already fills for a section. Falls back to
11
+ // undefined (no subtitle) when neither is set.
12
+ function createScreenTextResolver(translate, i18nScreenId, formMode) {
13
+ return (suffix) => {
14
+ for (const key of [
15
+ `screen:${i18nScreenId}.${formMode}.${suffix}`,
16
+ `screen:${i18nScreenId}.${suffix}`,
17
+ ]) {
18
+ const value = translate(key);
19
+ if (value !== key)
20
+ return value;
21
+ }
22
+ return undefined;
23
+ };
24
+ }
25
+ // In tabs mode (hideSectionTitles) screen.description is dropped — the head
26
+ // card carries it — but an explicit subtitle i18n override has no other
27
+ // source there, so it stays unless it merely resolves to the description.
28
+ function resolveFormSubtitle(hideSectionTitles, explicitSubtitle, descriptionSubtitle) {
29
+ if (hideSectionTitles !== true)
30
+ return explicitSubtitle ?? descriptionSubtitle;
31
+ return explicitSubtitle !== descriptionSubtitle ? explicitSubtitle : undefined;
32
+ }
33
+ function sectionNavItemsFor(section, index, formTitle) {
34
+ const isNavigable = section.kind === "fields" &&
35
+ section.visible &&
36
+ section.title !== undefined &&
37
+ section.title !== formTitle;
38
+ return isNavigable && section.title !== undefined
39
+ ? [{ id: formSectionDomId(index), title: section.title }]
40
+ : [];
41
+ }
42
+ function deriveSectionNavItems(filteredSections, formTitle) {
43
+ return filteredSections.flatMap((section, index) => sectionNavItemsFor(section, index, formTitle));
44
+ }
45
+ function deriveCompactStepLabel(translate, currentTitle, currentStep, lastStepIndex) {
46
+ return currentTitle !== undefined
47
+ ? translate("kumiko.wizard.step-with-title", {
48
+ current: currentStep + 1,
49
+ total: lastStepIndex + 1,
50
+ title: currentTitle,
51
+ })
52
+ : translate("kumiko.wizard.step", { current: currentStep + 1, total: lastStepIndex + 1 });
53
+ }
54
+ function deriveFormTitleTexts({ props, state, form }, isCreate) {
55
+ const { screen, hideSectionTitles } = props;
56
+ const { translate } = state;
57
+ const formMode = isCreate ? "create" : props.viewMode ? "view" : "edit";
58
+ const recordTitle = !isCreate && screen.recordTitleField !== undefined
59
+ ? resolveRecordTitle(props.initial[screen.recordTitleField])
60
+ : undefined;
61
+ const resolveScreenText = createScreenTextResolver(translate, props.i18nScreenId, formMode);
62
+ const plainFormTitle = resolveScreenText("title") ?? props.i18nScreenId;
63
+ const formTitle = resolveTitleTemplate(screen.titleTemplate, form.snapshot.values, translate, plainFormTitle);
64
+ const explicitSubtitle = resolveScreenText("subtitle");
65
+ const descriptionSubtitle = screen.description !== undefined ? translate(screen.description) : undefined;
66
+ const formSubtitle = resolveFormSubtitle(hideSectionTitles, explicitSubtitle, descriptionSubtitle);
67
+ return { recordTitle, formTitle, formSubtitle };
68
+ }
69
+ export function deriveTitles(engine, layout, footer) {
70
+ const { props, state, sections, steps } = engine;
71
+ const { screen, headerRegion } = props;
72
+ const { translate } = state;
73
+ const { filteredSections } = sections;
74
+ const { currentStep, lastStepIndex } = steps;
75
+ const { PageHeader, StepBar } = state.primitives;
76
+ const { recordTitle, formTitle, formSubtitle } = deriveFormTitleTexts(engine, footer.isCreate);
77
+ return {
78
+ recordTitle,
79
+ formTitle,
80
+ formSubtitle,
81
+ // Embedded settings-list forms (dashboard panels) have no shell header: the
82
+ // form title/subtitle move into the first section's header column instead of
83
+ // floating as a heading above the band.
84
+ titleInSettingsHeader: footer.isSettingsList && !layout.titleInShell,
85
+ usesFieldFlow: layout.isScreenForm || state.insideDrawer,
86
+ // The caller's headerRegion already shows the record title in the shell page
87
+ // header, so the type title ("Release") above the form would repeat it.
88
+ recordTitleInHeaderRegion: headerRegion !== undefined &&
89
+ recordTitle !== undefined &&
90
+ PageHeader !== undefined &&
91
+ state.pageHeaderSlotAvailable,
92
+ stepRail: state.isWizard && layout.isScreenForm && StepBar !== undefined,
93
+ upNextSection: screen.layout.wizard?.aside?.upNext === true ? filteredSections[currentStep + 1] : undefined,
94
+ sectionNavItems: layout.isScreenForm && !state.isStepped
95
+ ? deriveSectionNavItems(filteredSections, formTitle)
96
+ : [],
97
+ compactStepLabel: deriveCompactStepLabel(translate, filteredSections[currentStep]?.title, currentStep, lastStepIndex),
98
+ };
99
+ }
@@ -0,0 +1,11 @@
1
+ import type { FormValues } from "../headless/index.ts";
2
+ import type { RenderEditSteppedCore } from "./render-edit-render-model.ts";
3
+ export type WizardNavigation = {
4
+ readonly persistExtensions: () => Promise<boolean>;
5
+ readonly jumpToStep: (index: number) => void;
6
+ readonly handleWizardNext: () => void;
7
+ readonly handleWizardBack: () => void;
8
+ readonly handleWizardStepSelect: (targetStep: number) => void;
9
+ readonly findFirstErroringStep: (fieldPaths: readonly string[]) => number | undefined;
10
+ };
11
+ export declare function createWizardNavigation<TValues extends FormValues, TCtx>({ props, state, form, draft, sections, steps }: RenderEditSteppedCore<TValues, TCtx>, saveDraft: (stepIndex: number) => void): WizardNavigation;