@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,141 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { buildControlsFor, deriveChangeState, flushPendingDraftSave, } from "./render-edit-draft-save-helpers.js";
3
+ import { PATCH_DRAFT_SAVE_DEBOUNCE_MS } from "./use-wizard-form-draft.js";
4
+ export function useRenderEditDraftSave(props, state, form) {
5
+ const { controller, snapshot } = form;
6
+ const { disabled, schema } = props;
7
+ const { isSubmitting, draftEnabled, dispatcher } = state;
8
+ // Controlled mode. Both callbacks live in refs so a
9
+ // re-rendering caller (identity-unstable `onChange`/`onControlsReady`
10
+ // closures, e.g. inline arrows) doesn't retrigger these effects — only a
11
+ // real snapshot mutation (typing, patch()) does. Without this, a caller
12
+ // whose onChange calls patch() to fill other fields (the VIN-decode use
13
+ // case) would loop: patch → new snapshot → effect
14
+ // re-subscribes with a "new" onChange → refires.
15
+ const onChangeRef = useRef(props.onChange);
16
+ onChangeRef.current = props.onChange;
17
+ // Guards against redelivering to the SAME callback identity on every
18
+ // render (an inline-arrow onControlsReady would otherwise refire the
19
+ // effect below on every render since the callback itself is now a dep).
20
+ // Tracks the actual prop, not a ref snapshot, so a caller that swaps in a
21
+ // real handler after mount (e.g. `onControlsReady={ready ? cb : undefined}`)
22
+ // gets delivered to for THIS mount instead of never.
23
+ const deliveredControlsToRef = useRef(undefined);
24
+ const onControlsReadyRef = useRef(props.onControlsReady);
25
+ onControlsReadyRef.current = props.onControlsReady;
26
+ const hasNavigatedRef = useRef(false);
27
+ const submittedRef = useRef(false);
28
+ const isSubmittingRef = useRef(false);
29
+ const scopeFieldNamesRef = useRef(form.scopeFieldNames);
30
+ scopeFieldNamesRef.current = form.scopeFieldNames;
31
+ const scopedValidate = useCallback(() => controller.validate(scopeFieldNamesRef.current), [controller]);
32
+ // The extension dirty flag aggregates every section; each section clears its
33
+ // own part, so a host-side markSaved must not reset it.
34
+ const markSaved = useCallback((fields) => {
35
+ if (fields === undefined)
36
+ controller.rebase();
37
+ else
38
+ controller.rebaseFields(fields);
39
+ }, [controller]);
40
+ // `saveDraft` (defined below) is a fresh closure every render, over that
41
+ // render's `draftKey`/`draftId` — the ref lets the debounce timer below
42
+ // always call the CURRENT one even though the timer was scheduled by an
43
+ // earlier render's patch() call. `currentStepRef` gives it the current
44
+ // wizard step without depending on `currentStep` (computed further down,
45
+ // from `filteredSections`) at the point patchAndScheduleDraftSave itself
46
+ // is defined — kept fresh where `currentStep` is computed below.
47
+ const saveDraftRef = useRef(() => { });
48
+ // `handleSubmit` (defined below) is a fresh closure over this render's
49
+ // snapshot/extensionDirty, while the onControlsReady effect fires once per
50
+ // mount — the ref lets controls.submit() always reach the current one.
51
+ const handleSubmitRef = useRef(async () => { });
52
+ const wizardNextRef = useRef(() => { });
53
+ const wizardBackRef = useRef(() => { });
54
+ const currentStepRef = useRef(0);
55
+ const draftSaveTimerRef = useRef(null);
56
+ const refs = {
57
+ onChangeRef,
58
+ deliveredControlsToRef,
59
+ onControlsReadyRef,
60
+ hasNavigatedRef,
61
+ submittedRef,
62
+ isSubmittingRef,
63
+ scopeFieldNamesRef,
64
+ saveDraftRef,
65
+ handleSubmitRef,
66
+ wizardNextRef,
67
+ wizardBackRef,
68
+ currentStepRef,
69
+ draftSaveTimerRef,
70
+ };
71
+ useEffect(() => {
72
+ return () => flushPendingDraftSave(draftSaveTimerRef, saveDraftRef, currentStepRef);
73
+ }, []);
74
+ // patch() (controlled mode, extension sections) applies immediately —
75
+ // only the resulting draft save is debounced, so a patch() burst
76
+ // (VIN-decode filling several fields, or onChange fanning a keystroke out
77
+ // through patch(), see the VIN-decode test above) collapses into one save.
78
+ const patchAndScheduleDraftSave = useCallback((partial) => {
79
+ controller.setValues(partial);
80
+ // skip: no draft screen, locked, or already submitted
81
+ if (!draftEnabled || dispatcher === undefined || disabled || submittedRef.current)
82
+ return;
83
+ if (draftSaveTimerRef.current !== null)
84
+ clearTimeout(draftSaveTimerRef.current);
85
+ draftSaveTimerRef.current = setTimeout(() => {
86
+ draftSaveTimerRef.current = null;
87
+ // skip: locked or submitted since the timer was set
88
+ if (disabled || submittedRef.current)
89
+ return;
90
+ saveDraftRef.current(currentStepRef.current);
91
+ }, PATCH_DRAFT_SAVE_DEBOUNCE_MS);
92
+ }, [controller, draftEnabled, dispatcher, disabled]);
93
+ // Runs before the onChange effect below (declaration order = React
94
+ // execution order) so that an onChange fired on mount always sees
95
+ // `controls !== undefined` — both effects depend only on mount-stable
96
+ // values (controller/scopedValidate/patchAndScheduleDraftSave), so the
97
+ // swap is otherwise a no-op. See the VIN-decode test: an initial
98
+ // value that should derive dependent fields on mount needs controls.patch
99
+ // available on the very first onChange call, not just from the second
100
+ // keystroke onward.
101
+ const hasControlsReady = props.onControlsReady !== undefined;
102
+ useEffect(() => {
103
+ // skip: caller registered no onControlsReady
104
+ if (!hasControlsReady)
105
+ return;
106
+ const ready = onControlsReadyRef.current;
107
+ // skip: callback vanished before this effect ran
108
+ if (ready === undefined)
109
+ return;
110
+ // skip: this callback already received the controls
111
+ if (deliveredControlsToRef.current === ready)
112
+ return;
113
+ deliveredControlsToRef.current = ready;
114
+ ready(buildControlsFor({
115
+ controller,
116
+ patch: patchAndScheduleDraftSave,
117
+ validate: scopedValidate,
118
+ handleSubmitRef,
119
+ wizardNextRef,
120
+ wizardBackRef,
121
+ }));
122
+ // Dep on boolean presence only — inline-arrow identity must not redeliver.
123
+ }, [hasControlsReady, controller, scopedValidate, patchAndScheduleDraftSave]);
124
+ // `schema` is typically an inline caller prop (`schema={z.object({...})}`)
125
+ // with unstable identity across renders — as an effect dep it would refire
126
+ // on every parent render, not just on a snapshot change, risking a loop if
127
+ // the caller's onChange triggers a parent re-render. Held in a ref like
128
+ // onChangeRef so only a real snapshot mutation retriggers this effect.
129
+ const schemaRef = useRef(schema);
130
+ schemaRef.current = schema;
131
+ // Controls are guaranteed ready by the time this fires (see the
132
+ // onControlsReady effect above, which is declared first on purpose).
133
+ useEffect(() => {
134
+ const cb = onChangeRef.current;
135
+ // skip: caller registered no onChange
136
+ if (cb === undefined)
137
+ return;
138
+ cb(deriveChangeState(snapshot, schemaRef.current, scopeFieldNamesRef.current, isSubmitting));
139
+ }, [snapshot, isSubmitting]);
140
+ return { refs, scopedValidate, markSaved, patchAndScheduleDraftSave };
141
+ }
@@ -0,0 +1,10 @@
1
+ import type { FormValues } from "../headless/index.ts";
2
+ import { type UseFormResult } from "../hooks/use-form.ts";
3
+ import type { ResolvedRenderEditProps } from "./render-edit-render-model.ts";
4
+ import type { RenderEditState } from "./use-render-edit-state.ts";
5
+ export type RenderEditForm<TValues extends FormValues> = UseFormResult<TValues> & {
6
+ readonly scopeFieldNames: readonly string[] | undefined;
7
+ readonly writeVerb: "create" | "update";
8
+ readonly clearErrorsOnInput: (changedField: string) => void;
9
+ };
10
+ export declare function useRenderEditForm<TValues extends FormValues, TCtx>(props: ResolvedRenderEditProps<TValues, TCtx>, state: RenderEditState): RenderEditForm<TValues>;
@@ -0,0 +1,101 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import { layoutEditFields } from "../app/layout-fields.js";
3
+ import { useForm } from "../hooks/use-form.js";
4
+ import { changedDerivedValues } from "./form-derivation-patch.js";
5
+ import { deriveFormFields } from "./render-edit-helpers.js";
6
+ import { issuesAfterEdit } from "./render-edit-logic.js";
7
+ // Must be computed before submitConfig/useForm bakes it in (the controller
8
+ // freezes it on first render). This is the unfiltered field-name set from
9
+ // `fieldsFilter` ∩ `fields` — not the value-dependent `filteredSections`
10
+ // (which also drops `section.visible === false`). Hidden fields still fall
11
+ // out via form-controller's `hiddenFields` path. undefined = unscoped, since
12
+ // scoping would silently drop root-level .refine() issues.
13
+ // A `fieldsFilter` that matches nothing in `fields` (typo, renamed field)
14
+ // must fall back to unscoped too — an empty scope array would filter out
15
+ // ALL validation issues, letting submit() through
16
+ // unvalidated on a form that also renders no sections.
17
+ function useScopeFieldNames(fieldsFilter, fields) {
18
+ return useMemo(() => {
19
+ if (fieldsFilter === undefined)
20
+ return undefined;
21
+ const matched = fieldsFilter.filter((f) => Object.hasOwn(fields, f));
22
+ return matched.length === 0 ? undefined : matched;
23
+ }, [fieldsFilter, fields]);
24
+ }
25
+ // A write-excluded field must never reach the payload: the handler schema is
26
+ // z.never() for it, so even an unchanged read-only value would be rejected.
27
+ function useOmitFields(screen, writeVerb) {
28
+ return useMemo(() => [
29
+ ...layoutEditFields(screen)
30
+ .filter((spec) => spec.submit === false)
31
+ .map((spec) => spec.field),
32
+ ...(screen.writeExcludedFields?.[writeVerb] ?? []),
33
+ ], [screen, writeVerb]);
34
+ }
35
+ function buildSubmitConfig(props, omitFields, scopeFieldNames) {
36
+ const { writeCommand, payloadMode, buildPayload, serverFieldPathPrefix } = props;
37
+ return writeCommand !== undefined
38
+ ? {
39
+ type: writeCommand,
40
+ payloadMode,
41
+ ...(buildPayload !== undefined && { buildPayload }),
42
+ ...(omitFields.length > 0 && { omitFields }),
43
+ ...(serverFieldPathPrefix !== undefined && { serverFieldPathPrefix }),
44
+ ...(scopeFieldNames !== undefined && { validateScope: scopeFieldNames }),
45
+ }
46
+ : undefined;
47
+ }
48
+ export function useRenderEditForm(props, state) {
49
+ const { screen, initial, schema, ctx, formDerivation, refreshToken } = props;
50
+ const fields = useMemo(() => deriveFormFields(screen), [screen]);
51
+ const scopeFieldNames = useScopeFieldNames(props.fields, fields);
52
+ // Configure submission only when the caller provides a writeCommand; with
53
+ // customSubmit, the form controller has no submit wiring because
54
+ // controller.submit() is not called.
55
+ const writeVerb = state.isCreateMode ? "create" : "update";
56
+ const omitFields = useOmitFields(screen, writeVerb);
57
+ const submitConfig = buildSubmitConfig(props, omitFields, scopeFieldNames);
58
+ const { controller, snapshot } = useForm({
59
+ initial,
60
+ fields,
61
+ ...(schema !== undefined && { schema }),
62
+ ...(ctx !== undefined && { ctx }),
63
+ ...(submitConfig !== undefined && { submit: submitConfig }),
64
+ });
65
+ // Derived fields are written into form state (not only displayed) so required
66
+ // validation and the submit payload carry them; only a real change is set, which
67
+ // keeps this from looping.
68
+ useEffect(() => {
69
+ // skip: screen has no derived fields
70
+ if (formDerivation === undefined)
71
+ return;
72
+ const patch = changedDerivedValues(snapshot.values, formDerivation);
73
+ if (patch !== undefined)
74
+ controller.setValues(patch); // @cast-boundary form-values
75
+ }, [snapshot.values, formDerivation, controller]);
76
+ // Wired at the user-input boundary, not in controller.setField: computed
77
+ // fields, prefill, draft restore and action patches also call setValues
78
+ // after a failed submit and must not wipe the errors.
79
+ const clearErrorsOnInput = (changedField) => {
80
+ const errors = controller.getSnapshot().errors;
81
+ const remaining = issuesAfterEdit(errors, changedField);
82
+ if (remaining !== errors)
83
+ controller.setErrors(remaining);
84
+ if (state.formErrorSource === "submit" && state.formError?.code !== "version_conflict") {
85
+ state.setFormError(null);
86
+ }
87
+ };
88
+ const lastRefreshTokenRef = useRef(refreshToken);
89
+ useEffect(() => {
90
+ // skip: refreshToken unchanged
91
+ if (lastRefreshTokenRef.current === refreshToken)
92
+ return;
93
+ lastRefreshTokenRef.current = refreshToken;
94
+ // skip: never overwrite user input, a dirty form keeps its values.
95
+ if (controller.getSnapshot().isDirty)
96
+ return;
97
+ controller.setValues(initial);
98
+ controller.rebase();
99
+ }, [refreshToken, controller, initial]);
100
+ return { controller, snapshot, scopeFieldNames, writeVerb, clearErrorsOnInput };
101
+ }
@@ -0,0 +1,15 @@
1
+ import { type Dispatch, type ReactNode, type SetStateAction } from "react";
2
+ import type { EditSectionViewModel, EditViewModel, FormValues } from "../headless/index.ts";
3
+ import type { RenderEditCore } from "./render-edit-render-model.ts";
4
+ export type RenderEditSections = {
5
+ readonly vm: EditViewModel;
6
+ readonly filteredSections: readonly EditSectionViewModel[];
7
+ readonly showsAllRequiredHint: boolean;
8
+ readonly isFormEditable: boolean;
9
+ readonly isFieldless: boolean;
10
+ readonly fillHeight: boolean;
11
+ readonly isLoneWriteFormTab: boolean;
12
+ readonly writeFormFooterAction: ReactNode;
13
+ readonly setWriteFormFooterAction: Dispatch<SetStateAction<ReactNode>>;
14
+ };
15
+ export declare function useRenderEditSections<TValues extends FormValues, TCtx>(core: RenderEditCore<TValues, TCtx>): RenderEditSections;
@@ -0,0 +1,100 @@
1
+ import { useMemo, useState } from "react";
2
+ import { computeEditViewModel } from "../headless/index.js";
3
+ import { filterEditSections, hasEditableSection } from "./render-edit-logic.js";
4
+ function useEditViewModel({ props, state, form, }) {
5
+ const { screen, entity, featureName, formDerivation, actionFormScreenQn } = props;
6
+ const entityIdProp = props.entityId;
7
+ const { translate, isCreateMode } = state;
8
+ const { writeVerb } = form;
9
+ const values = form.snapshot.values;
10
+ return useMemo(() => computeEditViewModel({
11
+ screen,
12
+ entity,
13
+ values,
14
+ translate,
15
+ featureName,
16
+ mode: writeVerb,
17
+ ...(!isCreateMode && entityIdProp != null && { entityId: entityIdProp }),
18
+ ...(formDerivation !== undefined && { derivation: formDerivation }),
19
+ ...(actionFormScreenQn !== undefined && { actionFormScreenQn }),
20
+ }), [
21
+ formDerivation,
22
+ actionFormScreenQn,
23
+ screen,
24
+ entity,
25
+ values,
26
+ translate,
27
+ featureName,
28
+ writeVerb,
29
+ isCreateMode,
30
+ entityIdProp,
31
+ ]);
32
+ }
33
+ function useFilteredSections(vm, fieldsFilter) {
34
+ return useMemo(
35
+ // A fully-hidden "fields" section (every field in it currently
36
+ // condition-hidden) must not occupy a wizard step; it would render
37
+ // empty and block Back/Next on nothing. Extension, relatedList and
38
+ // writeForm sections carry no `visible` (they own their own lifecycle /
39
+ // run their own query / own submit), so they always pass through. A
40
+ // section with no fields at all (e.g. a review-only step) has
41
+ // `visible: fields.some(...)` = false vacuously; that's "no fields to
42
+ // hide", not "hidden", so it stays too. Unless the create
43
+ // exclusion emptied it: that would leave an empty wizard step / tab.
44
+ () => filterEditSections(vm.sections, fieldsFilter).filter((section) => section.kind === "extension" ||
45
+ section.kind === "relatedList" ||
46
+ section.kind === "writeForm" ||
47
+ (section.fields.length === 0 && section.emptiedByWriteExclusion !== true) ||
48
+ section.visible), [vm.sections, fieldsFilter]);
49
+ }
50
+ function useAllFieldsRequired(filteredSections) {
51
+ return useMemo(() => {
52
+ const fillable = filteredSections.flatMap((section) => section.kind === "fields"
53
+ ? section.fields.filter((field) => field.visible && !field.readOnly && field.type !== "boolean")
54
+ : []);
55
+ return fillable.length > 0 && fillable.every((field) => field.required);
56
+ }, [filteredSections]);
57
+ }
58
+ export function useRenderEditSections(core) {
59
+ const { props, state } = core;
60
+ const { hideSectionTitles, fillScreenHeight } = props;
61
+ const vm = useEditViewModel(core);
62
+ const filteredSections = useFilteredSections(vm, props.fields);
63
+ const allFieldsRequired = useAllFieldsRequired(filteredSections);
64
+ const showsAllRequiredHint = allFieldsRequired && hideSectionTitles !== true && !state.isWizard;
65
+ // true when editable fields exist or an extension opted into composed submit.
66
+ const isFormEditable = hasEditableSection(filteredSections);
67
+ // A fieldless form (input-less secretMint mint — the secret is
68
+ // server-generated, the mint step is only its submit button) has no
69
+ // section to gate on: it IS the action, so it always shows the submit
70
+ // button, and it can never go dirty either.
71
+ const isFieldless = Object.keys(props.entity.fields).length === 0;
72
+ // A lone relatedList tab (`hideSectionTitles` is only ever set by
73
+ // ProjectionDetailBody, which hands this component ONE section — its
74
+ // synthesized screen drops layout.mode, so an entityEdit tabs layout never
75
+ // reaches this line and `filteredSections[0]` stays the active section)
76
+ // needs its own tab panel capped at the available height so its table
77
+ // scrolls inside the panel instead of the whole page stretching to the
78
+ // row count — a short table still sizes to its content instead
79
+ // of stretching the panel to the bottom. Any other layout —
80
+ // multiple sections, a non-relatedList tab, stacked (non-tabs) forms —
81
+ // keeps normal document-flow height untouched.
82
+ const fillHeight = fillScreenHeight === true ||
83
+ (hideSectionTitles === true && filteredSections[0]?.kind === "relatedList");
84
+ // A lone writeForm tab is the whole page, so its submit joins this form's
85
+ // footer (pinned to the window edge like every other screen's save)
86
+ // instead of sitting in the section's title row.
87
+ const isLoneWriteFormTab = hideSectionTitles === true && filteredSections[0]?.kind === "writeForm";
88
+ const [writeFormFooterAction, setWriteFormFooterAction] = useState(undefined);
89
+ return {
90
+ vm,
91
+ filteredSections,
92
+ showsAllRequiredHint,
93
+ isFormEditable,
94
+ isFieldless,
95
+ fillHeight,
96
+ isLoneWriteFormTab,
97
+ writeFormFooterAction,
98
+ setWriteFormFooterAction,
99
+ };
100
+ }
@@ -0,0 +1,65 @@
1
+ import type { EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { ErrorNotice } from "../app/error-reference.tsx";
3
+ import type { DispatcherError, FieldIssue, Translate } from "../headless/index.ts";
4
+ import type { FormErrorSource } from "./render-edit-helpers.tsx";
5
+ export declare function useRenderEditState({ screen, entityId, translate: translateProp, }: {
6
+ readonly screen: EntityEditScreenDefinition;
7
+ readonly entityId: string | null | undefined;
8
+ readonly translate: Translate | undefined;
9
+ }): {
10
+ confirmDeleteOpen: boolean;
11
+ setConfirmDeleteOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
12
+ linkCopied: boolean;
13
+ setLinkCopied: import("react").Dispatch<import("react").SetStateAction<boolean>>;
14
+ isSubmitting: boolean;
15
+ setIsSubmitting: import("react").Dispatch<import("react").SetStateAction<boolean>>;
16
+ focusFirstErrorKey: number;
17
+ setFocusFirstErrorKey: import("react").Dispatch<import("react").SetStateAction<number>>;
18
+ formError: DispatcherError | null;
19
+ setFormError: import("react").Dispatch<import("react").SetStateAction<DispatcherError | null>>;
20
+ formErrorSource: FormErrorSource;
21
+ setFormErrorSource: import("react").Dispatch<import("react").SetStateAction<FormErrorSource>>;
22
+ extraDeleteIssues: readonly FieldIssue[];
23
+ setExtraDeleteIssues: import("react").Dispatch<import("react").SetStateAction<readonly FieldIssue[]>>;
24
+ actionError: ErrorNotice | null;
25
+ setActionError: import("react").Dispatch<import("react").SetStateAction<ErrorNotice | null>>;
26
+ rawStep: number;
27
+ setRawStep: import("react").Dispatch<import("react").SetStateAction<number>>;
28
+ passedStepKeys: ReadonlySet<string>;
29
+ setPassedStepKeys: import("react").Dispatch<import("react").SetStateAction<ReadonlySet<string>>>;
30
+ extensionStepComplete: Readonly<Record<string, boolean>>;
31
+ reportExtensionStepComplete: (stepKey: string, complete: boolean) => void;
32
+ activeTabId: string | undefined;
33
+ setActiveTabId: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
34
+ extensionDirty: boolean;
35
+ extensionDirtyRef: import("react").RefObject<boolean>;
36
+ hasExtensionRegistrations: boolean;
37
+ extensionErrorKey: string | null;
38
+ setExtensionErrorKey: import("react").Dispatch<import("react").SetStateAction<string | null>>;
39
+ extensionFormRegistry: import("../app/extension-form-submit.tsx").ExtensionFormRegistry;
40
+ runExtensionSubmits: (ctx: import("../app/extension-form-submit.tsx").ExtensionSubmitContext) => Promise<readonly import("../app/extension-form-submit.tsx").ExtensionSubmitResult[]>;
41
+ translate: Translate;
42
+ dispatcher: import("../headless/index.ts").Dispatcher | undefined;
43
+ isWizard: boolean;
44
+ isTabs: boolean;
45
+ draftEnabled: boolean;
46
+ isCreateMode: boolean;
47
+ draftStorage: import("../context/draft-storage-context.tsx").DraftStorage;
48
+ draftState: {
49
+ draftId: string | null;
50
+ setDraftId: import("react").Dispatch<import("react").SetStateAction<string | null>>;
51
+ draftCandidates: readonly import("./use-wizard-form-draft.ts").DraftCandidate[] | null;
52
+ setDraftCandidates: import("react").Dispatch<import("react").SetStateAction<readonly import("./use-wizard-form-draft.ts").DraftCandidate[] | null>>;
53
+ mintedDraftIdRef: import("react").RefObject<string | null>;
54
+ didListRef: import("react").RefObject<boolean>;
55
+ draftKey: string | undefined;
56
+ };
57
+ primitives: import("../primitives.tsx").PrimitivesRegistry;
58
+ menuActionToItem: (action: import("./render-edit-types.ts").RenderEditAction) => import("../primitives.tsx").ActionMenuItemSpec;
59
+ menuActionConfirmDialog: import("react").ReactNode;
60
+ pageHeaderSlotAvailable: boolean;
61
+ pageHeaderCompact: boolean;
62
+ insideDrawer: boolean;
63
+ isStepped: boolean;
64
+ };
65
+ export type RenderEditState = ReturnType<typeof useRenderEditState>;
@@ -0,0 +1,158 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import { useExtensionFormHost } from "../app/extension-form-submit.js";
3
+ import { useOptionalDispatcher } from "../context/dispatcher-context.js";
4
+ import { useDraftStorage } from "../context/draft-storage-context.js";
5
+ import { useTranslation } from "../i18n.js";
6
+ import { useInsideDrawer } from "../inside-drawer.js";
7
+ import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header-slot.js";
8
+ import { usePrimitives } from "../primitives.js";
9
+ import { useActionMenuItems } from "./use-action-menu-items.js";
10
+ import { useWizardFormDraftState } from "./use-wizard-form-draft.js";
11
+ function useSubmitFeedbackState() {
12
+ const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
13
+ const [linkCopied, setLinkCopied] = useState(false);
14
+ const [isSubmitting, setIsSubmitting] = useState(false);
15
+ const [focusFirstErrorKey, setFocusFirstErrorKey] = useState(0);
16
+ const [formError, setFormError] = useState(null);
17
+ // Only a submit's validation/server error is cleared by editing; a delete
18
+ // rejection and a version conflict (reload is the way out) stay put.
19
+ const [formErrorSource, setFormErrorSource] = useState("submit");
20
+ // A rejected delete can carry several blocking reasons; the first one is the
21
+ // banner's headline (formError), the rest are listed beneath it.
22
+ const [extraDeleteIssues, setExtraDeleteIssues] = useState([]);
23
+ // One state for all header actions (actions?), not per-button — only one
24
+ // action can be in flight at a time, and this keeps the error surfaced in
25
+ // the shared formError-adjacent banner region instead of inside the
26
+ // button row: a full-width Banner there breaks the
27
+ // flex justify-end action bar.
28
+ const [actionError, setActionError] = useState(null);
29
+ return {
30
+ confirmDeleteOpen,
31
+ setConfirmDeleteOpen,
32
+ linkCopied,
33
+ setLinkCopied,
34
+ isSubmitting,
35
+ setIsSubmitting,
36
+ focusFirstErrorKey,
37
+ setFocusFirstErrorKey,
38
+ formError,
39
+ setFormError,
40
+ formErrorSource,
41
+ setFormErrorSource,
42
+ extraDeleteIssues,
43
+ setExtraDeleteIssues,
44
+ actionError,
45
+ setActionError,
46
+ };
47
+ }
48
+ function useStepState() {
49
+ const [rawStep, setRawStep] = useState(0);
50
+ const [passedStepKeys, setPassedStepKeys] = useState(() => new Set());
51
+ const [extensionStepComplete, setExtensionStepComplete] = useState({});
52
+ const reportExtensionStepComplete = useCallback((stepKey, complete) => {
53
+ setExtensionStepComplete((prev) => (prev[stepKey] === true) === complete ? prev : { ...prev, [stepKey]: complete });
54
+ }, []);
55
+ // Tabs have stable ids, so the active tab follows its id: a field change
56
+ // that hides an earlier tab must not shift the index onto another tab.
57
+ const [activeTabId, setActiveTabId] = useState(undefined);
58
+ return {
59
+ rawStep,
60
+ setRawStep,
61
+ passedStepKeys,
62
+ setPassedStepKeys,
63
+ extensionStepComplete,
64
+ reportExtensionStepComplete,
65
+ activeTabId,
66
+ setActiveTabId,
67
+ };
68
+ }
69
+ // Composed save: extension sections report their dirty state so the Save button
70
+ // activates when only a section changed, and provide a submit handler that runs
71
+ // after the entity write. extensionErrorKey stores the i18n key for a failed
72
+ // section persistence.
73
+ //
74
+ // This is not a second write path for saveDraft()'s data: deriveFormFields
75
+ // skips extension sections entirely, so extension-owned field state never enters
76
+ // `fields` or `controller.getSnapshot().values`; there is no draft-covered state
77
+ // left for persistExtensions() to compete over.
78
+ function useExtensionComposition() {
79
+ const [extensionDirty, setExtensionDirty] = useState(false);
80
+ // Mirror for the leave guard: it must see "saved" before React re-renders.
81
+ const extensionDirtyRef = useRef(false);
82
+ const [hasExtensionRegistrations, setHasExtensionRegistrations] = useState(false);
83
+ const [extensionErrorKey, setExtensionErrorKey] = useState(null);
84
+ const { registry: extensionFormRegistry, runAll: runExtensionSubmits } = useExtensionFormHost(({ anyDirty, hasRegistrations }) => {
85
+ extensionDirtyRef.current = anyDirty;
86
+ setExtensionDirty(anyDirty);
87
+ setHasExtensionRegistrations(hasRegistrations);
88
+ });
89
+ return {
90
+ extensionDirty,
91
+ extensionDirtyRef,
92
+ hasExtensionRegistrations,
93
+ extensionErrorKey,
94
+ setExtensionErrorKey,
95
+ extensionFormRegistry,
96
+ runExtensionSubmits,
97
+ };
98
+ }
99
+ export function useRenderEditState({ screen, entityId, translate: translateProp, }) {
100
+ // Translate fallback: when the caller does not provide a translate function,
101
+ // consume the i18next context directly. Otherwise field labels would remain raw
102
+ // keys (`feature:entity:foo:field:title`) without caller wiring.
103
+ // useTranslation throws without a LocaleProvider, which is acceptable because
104
+ // RenderEdit only runs inside a mounted Kumiko app tree.
105
+ const t = useTranslation();
106
+ const translate = translateProp ?? t;
107
+ const dispatcher = useOptionalDispatcher();
108
+ const isWizard = screen.layout.mode === "wizard";
109
+ // Tabs reuse the wizard's step machinery: one section visible at a time,
110
+ // all of them mounted, `currentStep` as the active index. What they do NOT
111
+ // share is navigation — a tab form submits from any tab, so every
112
+ // step-transition path below stays wizard-only.
113
+ const isTabs = screen.layout.mode === "tabs";
114
+ const draftEnabled = isWizard && screen.layout.draft === true && dispatcher !== undefined;
115
+ const isCreateMode = entityId === undefined || entityId === null || entityId === "";
116
+ const draftStorage = useDraftStorage();
117
+ const feedback = useSubmitFeedbackState();
118
+ const stepState = useStepState();
119
+ const draftState = useWizardFormDraftState({
120
+ screenId: screen.id,
121
+ entityId,
122
+ isCreateMode,
123
+ draftStorage,
124
+ });
125
+ const extensions = useExtensionComposition();
126
+ const primitives = usePrimitives();
127
+ const { toMenuItem: menuActionToItem, confirmDialog: menuActionConfirmDialog } = useActionMenuItems({ Dialog: primitives.Dialog, onError: feedback.setActionError });
128
+ const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
129
+ const pageHeaderCompact = usePageHeaderCompact();
130
+ const insideDrawer = useInsideDrawer();
131
+ // Both stepped layouts show one section at a time and keep the rest mounted
132
+ // but inert. A tabs layout on a host without the Tabs primitive has no strip
133
+ // to switch with, so it falls back to rendering every section stacked rather
134
+ // than stranding the user on section 0. Same fallback when the
135
+ // WizardStepGroup wrapper is missing.
136
+ const isStepped = isWizard ||
137
+ (isTabs && primitives.Tabs !== undefined && primitives.WizardStepGroup !== undefined);
138
+ return {
139
+ translate,
140
+ dispatcher,
141
+ isWizard,
142
+ isTabs,
143
+ draftEnabled,
144
+ isCreateMode,
145
+ draftStorage,
146
+ ...feedback,
147
+ ...stepState,
148
+ draftState,
149
+ ...extensions,
150
+ primitives,
151
+ menuActionToItem,
152
+ menuActionConfirmDialog,
153
+ pageHeaderSlotAvailable,
154
+ pageHeaderCompact,
155
+ insideDrawer,
156
+ isStepped,
157
+ };
158
+ }
@@ -0,0 +1,13 @@
1
+ import type { FormValues } from "../headless/index.ts";
2
+ import type { RenderEditSectioned } from "./render-edit-render-model.ts";
3
+ export type RenderEditSteps = {
4
+ readonly lastStepIndex: number;
5
+ readonly currentStep: number;
6
+ readonly isLastWizardStep: boolean;
7
+ readonly wizardDoneSteps: readonly boolean[] | undefined;
8
+ readonly updateModeStepBarProps: Record<string, never> | {
9
+ readonly doneSteps: readonly boolean[];
10
+ readonly selectableSteps: "all";
11
+ };
12
+ };
13
+ export declare function useRenderEditSteps<TValues extends FormValues, TCtx>({ props, state, form, draft, sections, }: RenderEditSectioned<TValues, TCtx>): RenderEditSteps;
@@ -0,0 +1,52 @@
1
+ import { useMemo } from "react";
2
+ import { dryRunParse, isFieldsStepFilled, tabIdAt, wizardStepKey } from "./render-edit-helpers.js";
3
+ function computeCurrentStep(isTabs, activeTabId, rawStep, filteredSections) {
4
+ const lastStepIndex = Math.max(filteredSections.length - 1, 0);
5
+ // Clamped on read, not on write: section visibility is value-dependent, so a
6
+ // stored stepIndex can point past what is currently rendered — that lands on
7
+ // the last step instead of an empty one.
8
+ const activeTabIndex = isTabs && activeTabId !== undefined
9
+ ? filteredSections.findIndex((s, i) => tabIdAt(s, i) === activeTabId)
10
+ : -1;
11
+ const currentStep = activeTabIndex !== -1 ? activeTabIndex : Math.min(rawStep, lastStepIndex);
12
+ return { currentStep, lastStepIndex };
13
+ }
14
+ function isStepDone(section, index, passedStepKeys, extensionStepComplete, parse) {
15
+ if (passedStepKeys.has(wizardStepKey(section, index)))
16
+ return true;
17
+ if (section.kind === "fields")
18
+ return isFieldsStepFilled(section, parse);
19
+ return (section.kind === "extension" && extensionStepComplete[wizardStepKey(section, index)] === true);
20
+ }
21
+ export function useRenderEditSteps({ props, state, form, draft, sections, }) {
22
+ const { filteredSections } = sections;
23
+ const { schema } = props;
24
+ const { isWizard, isCreateMode, passedStepKeys, extensionStepComplete } = state;
25
+ const { currentStep, lastStepIndex } = computeCurrentStep(state.isTabs, state.activeTabId, state.rawStep, filteredSections);
26
+ // Kept fresh for patchAndScheduleDraftSave's debounce timer above, which
27
+ // is defined before `currentStep` exists (it depends on `filteredSections`,
28
+ // computed further up from `vm`) and so cannot close over it directly.
29
+ draft.refs.currentStepRef.current = currentStep;
30
+ const isLastWizardStep = currentStep >= lastStepIndex;
31
+ const wizardDoneSteps = useMemo(() => {
32
+ if (!isWizard || isCreateMode)
33
+ return undefined;
34
+ const parse = schema === undefined
35
+ ? undefined
36
+ : dryRunParse(schema, { values: form.snapshot.values, fields: form.snapshot.fields });
37
+ return filteredSections.map((section, index) => isStepDone(section, index, passedStepKeys, extensionStepComplete, parse));
38
+ }, [
39
+ isWizard,
40
+ isCreateMode,
41
+ filteredSections,
42
+ passedStepKeys,
43
+ extensionStepComplete,
44
+ schema,
45
+ form.snapshot.values,
46
+ form.snapshot.fields,
47
+ ]);
48
+ const updateModeStepBarProps = wizardDoneSteps === undefined
49
+ ? {}
50
+ : { doneSteps: wizardDoneSteps, selectableSteps: "all" };
51
+ return { lastStepIndex, currentStep, isLastWizardStep, wizardDoneSteps, updateModeStepBarProps };
52
+ }