@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/dist/app/projection-detail-body.js +59 -394
  2. package/dist/app/projection-detail-header-model.d.ts +11 -0
  3. package/dist/app/projection-detail-header-model.js +33 -0
  4. package/dist/app/projection-detail-header-region.d.ts +28 -0
  5. package/dist/app/projection-detail-header-region.js +81 -0
  6. package/dist/app/projection-detail-header.d.ts +29 -0
  7. package/dist/app/projection-detail-header.js +84 -0
  8. package/dist/app/projection-detail-navigation.d.ts +13 -0
  9. package/dist/app/projection-detail-navigation.js +91 -0
  10. package/dist/app/projection-detail-states.d.ts +22 -0
  11. package/dist/app/projection-detail-states.js +26 -0
  12. package/dist/app/projection-drawer-host.d.ts +12 -0
  13. package/dist/app/projection-drawer-host.js +10 -0
  14. package/dist/app/projection-list-body.d.ts +1 -1
  15. package/dist/app/projection-list-body.js +69 -189
  16. package/dist/app/projection-list-render-props.d.ts +56 -0
  17. package/dist/app/projection-list-render-props.js +47 -0
  18. package/dist/app/projection-list-render.d.ts +26 -0
  19. package/dist/app/projection-list-render.js +20 -0
  20. package/dist/app/projection-list-row-click.d.ts +12 -0
  21. package/dist/app/projection-list-row-click.js +14 -0
  22. package/dist/app/use-projection-detail-actions.d.ts +35 -0
  23. package/dist/app/use-projection-detail-actions.js +156 -0
  24. package/dist/app/use-projection-detail-data.d.ts +19 -0
  25. package/dist/app/use-projection-detail-data.js +47 -0
  26. package/dist/app/use-projection-detail-layout.d.ts +17 -0
  27. package/dist/app/use-projection-detail-layout.js +23 -0
  28. package/dist/app/use-projection-list-actions.d.ts +31 -0
  29. package/dist/app/use-projection-list-actions.js +73 -0
  30. package/dist/app/use-projection-list-data.d.ts +20 -0
  31. package/dist/app/use-projection-list-data.js +26 -0
  32. package/dist/app/use-projection-list-definitions.d.ts +11 -0
  33. package/dist/app/use-projection-list-definitions.js +20 -0
  34. package/dist/app/use-projection-list-query.d.ts +28 -0
  35. package/dist/app/use-projection-list-query.js +83 -0
  36. package/dist/components/render-edit-banners.d.ts +12 -0
  37. package/dist/components/render-edit-banners.js +90 -0
  38. package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
  39. package/dist/components/render-edit-draft-save-helpers.js +42 -0
  40. package/dist/components/render-edit-field-grid.d.ts +4 -0
  41. package/dist/components/render-edit-field-grid.js +40 -0
  42. package/dist/components/render-edit-fields-section.d.ts +4 -0
  43. package/dist/components/render-edit-fields-section.js +76 -0
  44. package/dist/components/render-edit-form-actions.d.ts +4 -0
  45. package/dist/components/render-edit-form-actions.js +66 -0
  46. package/dist/components/render-edit-header-actions.d.ts +11 -0
  47. package/dist/components/render-edit-header-actions.js +120 -0
  48. package/dist/components/render-edit-layout-flags.d.ts +29 -0
  49. package/dist/components/render-edit-layout-flags.js +106 -0
  50. package/dist/components/render-edit-layout.d.ts +4 -0
  51. package/dist/components/render-edit-layout.js +79 -0
  52. package/dist/components/render-edit-render-model.d.ts +63 -0
  53. package/dist/components/render-edit-render-model.js +14 -0
  54. package/dist/components/render-edit-section-kinds.d.ts +4 -0
  55. package/dist/components/render-edit-section-kinds.js +115 -0
  56. package/dist/components/render-edit-submit-result.d.ts +8 -0
  57. package/dist/components/render-edit-submit-result.js +77 -0
  58. package/dist/components/render-edit-submit.d.ts +7 -0
  59. package/dist/components/render-edit-submit.js +113 -0
  60. package/dist/components/render-edit-titles.d.ts +17 -0
  61. package/dist/components/render-edit-titles.js +99 -0
  62. package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
  63. package/dist/components/render-edit-wizard-navigation.js +115 -0
  64. package/dist/components/render-edit.d.ts +2 -2
  65. package/dist/components/render-edit.js +63 -1128
  66. package/dist/components/render-field-choice-inputs.d.ts +7 -0
  67. package/dist/components/render-field-choice-inputs.js +52 -0
  68. package/dist/components/render-field-control.d.ts +12 -0
  69. package/dist/components/render-field-control.js +93 -0
  70. package/dist/components/render-field-display-text.d.ts +4 -0
  71. package/dist/components/render-field-display-text.js +48 -0
  72. package/dist/components/render-field-fill-action.d.ts +13 -0
  73. package/dist/components/render-field-fill-action.js +63 -0
  74. package/dist/components/render-field-input-shared.d.ts +39 -0
  75. package/dist/components/render-field-input-shared.js +20 -0
  76. package/dist/components/render-field-input.d.ts +3 -0
  77. package/dist/components/render-field-input.js +34 -0
  78. package/dist/components/render-field-meta.d.ts +6 -0
  79. package/dist/components/render-field-meta.js +51 -0
  80. package/dist/components/render-field-read-only.d.ts +11 -0
  81. package/dist/components/render-field-read-only.js +75 -0
  82. package/dist/components/render-field-reference.d.ts +15 -0
  83. package/dist/components/render-field-reference.js +103 -0
  84. package/dist/components/render-field-scalar-inputs.d.ts +10 -0
  85. package/dist/components/render-field-scalar-inputs.js +35 -0
  86. package/dist/components/render-field-text-inputs.d.ts +3 -0
  87. package/dist/components/render-field-text-inputs.js +34 -0
  88. package/dist/components/render-field-values.d.ts +18 -0
  89. package/dist/components/render-field-values.js +111 -0
  90. package/dist/components/render-field-write-only.d.ts +9 -0
  91. package/dist/components/render-field-write-only.js +29 -0
  92. package/dist/components/render-field.d.ts +2 -2
  93. package/dist/components/render-field.js +39 -670
  94. package/dist/components/render-list-header-slot.d.ts +5 -0
  95. package/dist/components/render-list-header-slot.js +30 -0
  96. package/dist/components/render-list-reference-bridge.d.ts +3 -0
  97. package/dist/components/render-list-reference-bridge.js +25 -0
  98. package/dist/components/render-list-reference-columns.d.ts +25 -0
  99. package/dist/components/render-list-reference-columns.js +137 -0
  100. package/dist/components/render-list-table-props.d.ts +52 -0
  101. package/dist/components/render-list-table-props.js +53 -0
  102. package/dist/components/render-list-toolbar-model.d.ts +42 -0
  103. package/dist/components/render-list-toolbar-model.js +82 -0
  104. package/dist/components/render-list-toolbar.d.ts +38 -0
  105. package/dist/components/render-list-toolbar.js +40 -0
  106. package/dist/components/render-list-types.d.ts +113 -0
  107. package/dist/components/render-list-types.js +1 -0
  108. package/dist/components/render-list.d.ts +4 -113
  109. package/dist/components/render-list.js +91 -307
  110. package/dist/components/use-reference-input-state.d.ts +20 -0
  111. package/dist/components/use-reference-input-state.js +85 -0
  112. package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
  113. package/dist/components/use-render-edit-controlled-sync.js +26 -0
  114. package/dist/components/use-render-edit-draft-save.d.ts +29 -0
  115. package/dist/components/use-render-edit-draft-save.js +141 -0
  116. package/dist/components/use-render-edit-form.d.ts +10 -0
  117. package/dist/components/use-render-edit-form.js +101 -0
  118. package/dist/components/use-render-edit-sections.d.ts +15 -0
  119. package/dist/components/use-render-edit-sections.js +100 -0
  120. package/dist/components/use-render-edit-state.d.ts +65 -0
  121. package/dist/components/use-render-edit-state.js +158 -0
  122. package/dist/components/use-render-edit-steps.d.ts +13 -0
  123. package/dist/components/use-render-edit-steps.js +52 -0
  124. package/dist/components/use-render-list-columns.d.ts +18 -0
  125. package/dist/components/use-render-list-columns.js +41 -0
  126. package/dist/components/use-render-list-search.d.ts +4 -0
  127. package/dist/components/use-render-list-search.js +50 -0
  128. package/dist/primitives.d.ts +1 -1
  129. package/dist/web/app/dashboard-body.d.ts +1 -1
  130. package/dist/web/app/dashboard-body.js +42 -246
  131. package/dist/web/app/dashboard-header.d.ts +34 -0
  132. package/dist/web/app/dashboard-header.js +58 -0
  133. package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
  134. package/dist/web/app/dashboard-screen-controls.js +42 -0
  135. package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
  136. package/dist/web/app/dashboard-screen-tiles.js +42 -0
  137. package/dist/web/app/dashboard-visibility.d.ts +42 -0
  138. package/dist/web/app/dashboard-visibility.js +102 -0
  139. package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
  140. package/dist/web/app/use-dashboard-screen-state.js +69 -0
  141. package/dist/web/primitives/data-table/actions-column.js +6 -5
  142. package/dist/web/primitives/data-table/card-body.d.ts +6 -0
  143. package/dist/web/primitives/data-table/card-body.js +32 -0
  144. package/dist/web/primitives/data-table/card-row.d.ts +5 -0
  145. package/dist/web/primitives/data-table/card-row.js +31 -0
  146. package/dist/web/primitives/data-table/card-view.d.ts +3 -0
  147. package/dist/web/primitives/data-table/card-view.js +13 -0
  148. package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
  149. package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
  150. package/dist/web/primitives/data-table/data-cell-props.js +8 -0
  151. package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
  152. package/dist/web/primitives/data-table/row-render-parts.js +35 -0
  153. package/dist/web/primitives/data-table/table-body.d.ts +3 -0
  154. package/dist/web/primitives/data-table/table-body.js +32 -0
  155. package/dist/web/primitives/data-table/table-content.d.ts +3 -0
  156. package/dist/web/primitives/data-table/table-content.js +41 -0
  157. package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
  158. package/dist/web/primitives/data-table/table-data-cell.js +25 -0
  159. package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
  160. package/dist/web/primitives/data-table/table-header-row.js +21 -0
  161. package/dist/web/primitives/data-table/table-view.d.ts +3 -0
  162. package/dist/web/primitives/data-table/table-view.js +25 -0
  163. package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
  164. package/dist/web/primitives/data-table/toolbar.js +53 -0
  165. package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
  166. package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
  167. package/dist/web/primitives/data-table.d.ts +3 -4
  168. package/dist/web/primitives/data-table.js +32 -252
  169. package/dist/web/primitives/file-value-list.js +1 -1
  170. package/dist/web/primitives/form-footer-parts.d.ts +25 -0
  171. package/dist/web/primitives/form-footer-parts.js +55 -0
  172. package/dist/web/primitives/form.js +33 -43
  173. package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
  174. package/dist/web/primitives/input-choice-kinds.js +67 -0
  175. package/dist/web/primitives/input-file-kinds.d.ts +3 -0
  176. package/dist/web/primitives/input-file-kinds.js +6 -0
  177. package/dist/web/primitives/input-number-kinds.d.ts +4 -0
  178. package/dist/web/primitives/input-number-kinds.js +16 -0
  179. package/dist/web/primitives/input-optional-props.d.ts +20 -0
  180. package/dist/web/primitives/input-optional-props.js +24 -0
  181. package/dist/web/primitives/input-select-presentation.d.ts +13 -0
  182. package/dist/web/primitives/input-select-presentation.js +19 -0
  183. package/dist/web/primitives/input-shared.d.ts +22 -0
  184. package/dist/web/primitives/input-shared.js +10 -0
  185. package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
  186. package/dist/web/primitives/input-temporal-kinds.js +10 -0
  187. package/dist/web/primitives/input-text-kinds.d.ts +7 -0
  188. package/dist/web/primitives/input-text-kinds.js +33 -0
  189. package/dist/web/primitives/input.js +35 -161
  190. package/dist/web/widgets/chart-data.d.ts +35 -0
  191. package/dist/web/widgets/chart-data.js +37 -0
  192. package/dist/web/widgets/chart-markers.d.ts +26 -0
  193. package/dist/web/widgets/chart-markers.js +64 -0
  194. package/dist/web/widgets/chart-parts.d.ts +20 -0
  195. package/dist/web/widgets/chart-parts.js +15 -0
  196. package/dist/web/widgets/charts.d.ts +5 -78
  197. package/dist/web/widgets/charts.js +7 -485
  198. package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
  199. package/dist/web/widgets/drawer-panel-class.js +40 -0
  200. package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
  201. package/dist/web/widgets/drawer-resize-controls.js +12 -0
  202. package/dist/web/widgets/drawer-sizing.d.ts +19 -0
  203. package/dist/web/widgets/drawer-sizing.js +60 -0
  204. package/dist/web/widgets/drawer.js +17 -116
  205. package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
  206. package/dist/web/widgets/stacked-area-brush.js +91 -0
  207. package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
  208. package/dist/web/widgets/stacked-area-chart.js +45 -0
  209. package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
  210. package/dist/web/widgets/stacked-area-chrome.js +77 -0
  211. package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
  212. package/dist/web/widgets/stacked-area-geometry.js +83 -0
  213. package/dist/web/widgets/stacked-area-model.d.ts +11 -0
  214. package/dist/web/widgets/stacked-area-model.js +1 -0
  215. package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
  216. package/dist/web/widgets/stacked-area-plot.js +51 -0
  217. package/dist/web/widgets/stacked-area-props.d.ts +83 -0
  218. package/dist/web/widgets/stacked-area-props.js +16 -0
  219. package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
  220. package/dist/web/widgets/stacked-area-svg.js +38 -0
  221. package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
  222. package/dist/web/widgets/stacked-area-tooltip.js +86 -0
  223. package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
  224. package/dist/web/widgets/use-drawer-resize.js +52 -0
  225. package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
  226. package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
  227. package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
  228. package/dist/web/widgets/use-stacked-area-window.js +76 -0
  229. package/package.json +10 -10
  230. package/src/changes.json +54 -0
@@ -1,23 +1,14 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { UNRESOLVED_REFERENCE_LABEL_KEYS, } from "@cosmicdrift/kumiko-framework/ui-types";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { extensionSectionName, useExtensionSectionComponent } from "../app/extension-sections.js";
5
- import { useReferenceLinkResolver } from "../app/reference-link.js";
6
- import { rendererWarn } from "../dev-console.js";
7
- import { computeListViewModel, EMPTY_VALUE_PLACEHOLDER } from "../headless/index.js";
8
- import { useReferenceLookup } from "../hooks/use-reference-lookup.js";
2
+ import { useState } from "react";
9
3
  import { useTranslation } from "../i18n.js";
10
4
  import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header-slot.js";
11
- import { shouldRenderActionsIconOnly, usePrimitives, } from "../primitives.js";
12
- import { ListEmptyStateBody } from "./list-empty-state.js";
13
- import { needsActionConfirm } from "./render-edit-action-button.js";
14
- import { ToolbarActionView } from "./render-list-toolbar-action.js";
15
- const SEARCH_DEBOUNCE_MS = 300;
16
- // Icon-less actions are promoted too; on phones the call site falls back to a
17
- // "plus" icon, on desktop the label carries the action.
18
- function isHeaderPromotableToolbarAction(action) {
19
- return action.style === "primary" && !needsActionConfirm(action);
20
- }
5
+ import { usePrimitives } from "../primitives.js";
6
+ import { renderReferenceLookupBridges } from "./render-list-reference-bridge.js";
7
+ import { tableChromeProps, tableFilterProps, tableLayoutProps, tableRowProps, tableSortPagingProps, } from "./render-list-table-props.js";
8
+ import { composeEmptyState, renderCreateButton, renderHeaderActions, renderMenuActionError, renderPageHeader, renderSearchInput, renderToolbarEnd, } from "./render-list-toolbar.js";
9
+ import { buildPromotedMenuItems, resolveItemNoun, resolveToolbarLabels, resolveToolbarTitle, splitToolbarActions, } from "./render-list-toolbar-model.js";
10
+ import { useRenderListColumns } from "./use-render-list-columns.js";
11
+ import { useRenderListSearch } from "./use-render-list-search.js";
21
12
  export function RenderList(props) {
22
13
  const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, emptyContent, narrowed = false, defaultFilterActive: defaultFilterActiveProp = false, itemNounKey, searchValue, onSearchChange, sort, onSortChange, pager, onReachEnd, loadingMore, hasMore, rowActions, rowActionMode, bulk, rowTone, toolbarActions, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, ...tableBlockProps } = props;
23
14
  // As in RenderEdit, fall back to the i18next context for translation;
@@ -32,303 +23,96 @@ export function RenderList(props) {
32
23
  // their own, so their busy and error state lives here.
33
24
  const [menuActionBusyId, setMenuActionBusyId] = useState(null);
34
25
  const [menuActionError, setMenuActionError] = useState(null);
35
- // Local search buffer + debounce. External changes (browser back,
36
- // cross-component reset) are mirrored back; typing only fires
37
- // onSearchChange after 300ms without further keystrokes.
38
- const [localQ, setLocalQ] = useState(searchValue ?? "");
39
- // Sync during render, not in a useEffect: the passive effect also fires on
40
- // mount and can flush after a keystroke landing in the same commit,
41
- // resetting localQ and swallowing the input.
42
- const [syncedSearchValue, setSyncedSearchValue] = useState(searchValue);
43
- // The parent may echo our own debounced emits back late (router/URL round
44
- // trip), possibly after a newer emit already went out; adopting such a stale
45
- // echo would erase keys typed in between. All unechoed emits are kept, and a
46
- // matching echo drops itself and every older one. The render phase only reads
47
- // the list (a discarded or repeated render must find the echo again); the
48
- // matched entries are dropped in an effect.
49
- const pendingEmitsRef = useRef([]);
50
- if (searchValue !== syncedSearchValue) {
51
- setSyncedSearchValue(searchValue);
52
- if (!pendingEmitsRef.current.includes(searchValue ?? "")) {
53
- setLocalQ(searchValue ?? "");
54
- pendingEmitsRef.current = [];
55
- }
56
- }
57
- useEffect(() => {
58
- const echoIndex = pendingEmitsRef.current.indexOf(syncedSearchValue ?? "");
59
- if (echoIndex >= 0)
60
- pendingEmitsRef.current.splice(0, echoIndex + 1);
61
- }, [syncedSearchValue]);
62
- useEffect(() => {
63
- if (onSearchChange === undefined)
64
- return;
65
- if (localQ === (searchValue ?? ""))
66
- return;
67
- // A late echo of an older emit changes searchValue but must not re-send
68
- // the value that is already in flight.
69
- if (pendingEmitsRef.current.at(-1) === localQ)
70
- return;
71
- const timer = setTimeout(() => {
72
- pendingEmitsRef.current.push(localQ);
73
- onSearchChange(localQ);
74
- }, SEARCH_DEBOUNCE_MS);
75
- return () => clearTimeout(timer);
76
- }, [localQ, searchValue, onSearchChange]);
77
- const vm = useMemo(() => computeListViewModel({ screen, entity, rows, translate, featureName }), [screen, entity, rows, translate, featureName]);
78
- // Reference-field eager loading through bridge components.
79
- // The Hooks rule requires useReferenceLookup for each reference column,
80
- // but the number of columns varies with the schema. Each column therefore
81
- // mounts a ReferenceLookupBridge that publishes its map to local state
82
- // through onMap. Each bridge makes a constant number of hook calls (one),
83
- // and React manages its lifecycle when the schema changes via key.
84
- const referenceColumns = useMemo(() => vm.columns
85
- .filter((c) => c.type === "reference" && c.refEntity !== undefined)
86
- .map((c) => ({
87
- field: c.field,
88
- refEntity: c.refEntity ?? "",
89
- // refFeature comes from parseRefTarget in the ViewModel
90
- // (default = the current featureName).
91
- refFeature: c.refFeature ?? featureName,
92
- labelField: c.refLabelField ?? "id",
93
- })), [vm.columns, featureName]);
94
- const [referenceLookups, setReferenceLookups] = useState({});
95
- const handleLookupMap = useCallback((field, lookup) => {
96
- setReferenceLookups((prev) => {
97
- // Skip the update when the lookup is identical. Otherwise the bridge
98
- // would re-render and send the same map back until React entered
99
- // an infinite loop.
100
- const current = prev[field];
101
- if (current?.map === lookup.map && current.complete === lookup.complete)
102
- return prev;
103
- return { ...prev, [field]: lookup };
104
- });
105
- }, []);
106
- const resolveReferenceLink = useReferenceLinkResolver();
107
- const enrichedColumns = useMemo(() => {
108
- if (referenceColumns.length === 0)
109
- return vm.columns;
110
- return vm.columns.map((col) => {
111
- if (col.type !== "reference")
112
- return col;
113
- // An author-declared renderer always takes precedence;
114
- // the default is used only when none is set.
115
- if (col.renderer !== undefined)
116
- return col;
117
- const published = referenceLookups[col.field];
118
- const map = published?.map;
119
- const unknownKey = UNRESOLVED_REFERENCE_LABEL_KEYS[`${col.refFeature ?? featureName}:${col.refEntity ?? ""}`];
120
- // No raw id for entities that opt in: a placeholder while the lookup
121
- // loads, the "unknown" label once the complete lookup found nothing. A
122
- // capped lookup page cannot prove absence, so it keeps the raw id.
123
- const unresolvedLabel = (id) => unknownKey === undefined
124
- ? id
125
- : published === undefined
126
- ? EMPTY_VALUE_PLACEHOLDER
127
- : published.complete
128
- ? translate(unknownKey)
129
- : id;
130
- const labelField = col.refLabelField ?? "id";
131
- const refEntity = col.refEntity ?? "";
132
- // Only a row the user can see is linkable: resolved by the server
133
- // eagerload or by the lookup query. A multi-reference links per entry via linkItemsFor.
134
- const linkFor = (value, row) => {
135
- if (value === null || value === undefined || value === "" || Array.isArray(value)) {
136
- return undefined;
137
- }
138
- const id = String(value);
139
- const eagerloaded = eagerloadedRef(row, col.field);
140
- // The server leaves a missing or unreadable target out; null is treated the same.
141
- const resolved = (typeof eagerloaded === "object" &&
142
- eagerloaded !== null &&
143
- !Array.isArray(eagerloaded)) ||
144
- map?.has(id);
145
- return resolved === true ? resolveReferenceLink(refEntity, id) : undefined;
146
- };
147
- const linkItemsFor = (value, row) => {
148
- if (!Array.isArray(value) || value.length === 0)
149
- return undefined;
150
- const eagerloaded = eagerloadedRef(row, col.field);
151
- const eagerById = new Map(Array.isArray(eagerloaded) ? eagerloaded.map((r) => [String(r["id"]), r]) : []);
152
- return value.map((v) => {
153
- const id = String(v);
154
- const target = eagerById.get(id);
155
- const label = String(target?.[labelField] ?? map?.get(id) ?? unresolvedLabel(id));
156
- // Only a row the user can see is linkable: server eagerload or lookup query.
157
- const link = target !== undefined || map?.has(id) === true
158
- ? resolveReferenceLink(refEntity, id)
159
- : undefined;
160
- return { id, label, ...(link !== undefined && { link }) };
161
- });
162
- };
163
- const renderer = (value, row) => {
164
- // Server eager loading: when the server includes _refs, read the display
165
- // value directly from the resolved row. This avoids a round trip
166
- // through the bridge map and the limit:200 constraint. The
167
- // EagerloadedRow type from @cosmicdrift/kumiko-framework/db defines
168
- // the shape of _refs.
169
- const resolved = eagerloadedRef(row, col.field);
170
- if (Array.isArray(resolved) && resolved.length > 0) {
171
- return resolved.map((r) => String(r[labelField] ?? r["id"] ?? "")).join(", ");
172
- }
173
- if (isPlainRecord(resolved)) {
174
- return String(resolved[labelField] ?? resolved["id"] ?? "");
175
- }
176
- // Renderer-side fallback when server eager loading is not active.
177
- if (Array.isArray(value)) {
178
- if (value.length === 0)
179
- return EMPTY_VALUE_PLACEHOLDER;
180
- return value.map((v) => map?.get(String(v)) ?? unresolvedLabel(String(v))).join(", ");
181
- }
182
- if (value === null || value === undefined || value === "")
183
- return EMPTY_VALUE_PLACEHOLDER;
184
- const idStr = String(value);
185
- return map?.get(idStr) ?? unresolvedLabel(idStr);
186
- };
187
- return { ...col, renderer, linkFor, linkItemsFor };
188
- });
189
- }, [
190
- vm.columns,
191
- referenceColumns,
192
- referenceLookups,
193
- resolveReferenceLink,
26
+ const { localQ, setLocalQ } = useRenderListSearch(searchValue, onSearchChange);
27
+ const { referenceColumns, handleLookupMap, enrichedVm } = useRenderListColumns({
28
+ screen,
29
+ entity,
30
+ rows,
194
31
  featureName,
195
32
  translate,
196
- ]);
197
- const enrichedVm = useMemo(() => ({
198
- ...vm,
199
- columns: enrichedColumns,
200
- }), [vm, enrichedColumns]);
201
- // i18n defaults for toolbar and empty-state strings. The caller can
202
- // override each one through a prop; otherwise the framework bundles provide
203
- // them (kumiko.actions.create, kumiko.list.search-placeholder, …).
204
- const effectiveCreateLabel = translate(createLabel ?? screen.createLabel ?? "kumiko.actions.create");
205
- const effectiveSearchPlaceholder = translate(searchPlaceholder ?? screen.searchPlaceholder ?? "kumiko.list.search-placeholder");
206
- const nounKey = itemNounKey ?? `${featureName}:entity:${screen.entity}:noun`;
207
- const itemNoun = (itemNounKey !== undefined || !screen.entity.startsWith("__")) &&
208
- translate(nounKey, { count: 2 }) !== nounKey
209
- ? (count) => translate(nounKey, { count })
33
+ });
34
+ const labelInput = {
35
+ screen,
36
+ translate,
37
+ createLabel,
38
+ searchPlaceholder,
39
+ itemNounKey,
40
+ featureName,
41
+ };
42
+ const { effectiveCreateLabel, effectiveSearchPlaceholder } = resolveToolbarLabels(labelInput);
43
+ const itemNoun = resolveItemNoun(labelInput);
44
+ const toolbarStart = searchable
45
+ ? renderSearchInput(Input, localQ, setLocalQ, effectiveSearchPlaceholder)
210
46
  : undefined;
211
- const toolbarStart = searchable ? (_jsx(Input, { kind: "text", id: "render-list-search", name: "search", value: localQ, onChange: setLocalQ, placeholder: effectiveSearchPlaceholder })) : undefined;
212
47
  // Toolbar-end slot: an optional header slot (for example, a cap counter,
213
48
  // at the left of the right-hand cluster), toolbar actions from the schema,
214
49
  // and an optional "+ New" button at the right edge. The header slot lives
215
50
  // inside the toolbar row, with the same card frame, rather than as loose
216
51
  // text above the screen title. Items retain array order, and "+ New" comes
217
52
  // last because it is the most common and prominent CTA.
218
- const hasToolbarActions = toolbarActions !== undefined && toolbarActions.length > 0;
219
- const hasHeaderSlot = screen.slots?.header !== undefined;
220
- const toolbarIconOnly = hasToolbarActions && shouldRenderActionsIconOnly(toolbarActions);
221
- const createButton = onCreate !== undefined && createInPageHeader && pageHeaderCompact ? (_jsx(Button, { variant: "primary", icon: "plus", size: "icon", ariaLabel: effectiveCreateLabel, title: effectiveCreateLabel, onClick: onCreate, testId: "render-list-create" })) : onCreate !== undefined ? (_jsx(Button, { variant: "primary", icon: "plus", onClick: onCreate, testId: "render-list-create", className: "px-3.5", children: effectiveCreateLabel })) : undefined;
222
- // Every primary toolbar action takes the page-header slot, left of the create
223
- // button (which stays the rightmost primary action): labelled on desktop, so
224
- // projection lists look like entity lists. On phones an icon button, or, next to
225
- // the create button, an entry in the header's overflow menu: the header has no
226
- // room for a labelled action beside it.
227
- const promotedToolbarActions = PageHeader !== undefined && pageHeaderSlotAvailable
228
- ? (toolbarActions?.filter(isHeaderPromotableToolbarAction) ?? [])
229
- : [];
230
- const promotedInOverflowMenu = pageHeaderCompact && createInPageHeader && promotedToolbarActions.length > 0;
231
- const promotedMenuItems = promotedInOverflowMenu
232
- ? promotedToolbarActions.map((a) => ({
233
- id: a.id,
234
- label: a.label,
235
- ...(a.icon !== undefined && { icon: a.icon }),
236
- ...(menuActionBusyId !== null && { disabled: true }),
237
- testId: `render-list-toolbar-action-${a.id}`,
238
- onSelect: () => {
239
- setMenuActionBusyId(a.id);
240
- setMenuActionError(null);
241
- void (async () => {
242
- try {
243
- await a.onTrigger();
244
- }
245
- catch (e) {
246
- setMenuActionError(e instanceof Error ? e.message : String(e));
247
- }
248
- finally {
249
- setMenuActionBusyId(null);
250
- }
251
- })();
252
- },
253
- }))
254
- : [];
255
- const headerActions = promotedToolbarActions.length > 0 && !promotedInOverflowMenu ? (_jsxs(_Fragment, { children: [promotedToolbarActions.map((a) => (_jsx(ToolbarActionView, { action: pageHeaderCompact ? { ...a, icon: a.icon ?? "plus" } : a, iconOnly: pageHeaderCompact, Button: Button, Dialog: Dialog, Banner: Banner }, a.id))), createInPageHeader && createButton] })) : createInPageHeader ? (createButton) : undefined;
256
- const toolbarActionsInToolbar = toolbarActions?.filter((a) => !promotedToolbarActions.includes(a)) ?? [];
257
- const hasToolbarActionsInToolbar = toolbarActionsInToolbar.length > 0;
258
- const toolbarEnd = hasHeaderSlot ||
259
- hasToolbarActionsInToolbar ||
260
- (onCreate !== undefined && !createInPageHeader) ? (_jsxs(_Fragment, { children: [hasHeaderSlot && _jsx(ListHeaderSlotMount, { screen: screen }), hasToolbarActionsInToolbar &&
261
- toolbarActionsInToolbar.map((a) => (_jsx(ToolbarActionView, { action: a, iconOnly: toolbarIconOnly, Button: Button, Dialog: Dialog, Banner: Banner }, a.id))), !createInPageHeader && createButton] })) : undefined;
262
- // Empty state: by default, show a heading, description, and optional CTA
263
- // button. The caller can replace it completely through emptyState.
264
- // Without onCreate or an override, DataTable uses its default
265
- // (`kumiko.list.no-entries`).
266
- const defaultFilterActive = defaultFilterActiveProp && onFilterReset !== undefined;
267
- const composedEmptyState = emptyState ??
268
- (onCreate !== undefined || emptyContent !== undefined || narrowed || defaultFilterActive ? (_jsx(ListEmptyStateBody, { content: emptyContent, narrowed: narrowed, defaultFilterActive: defaultFilterActive, ...(onFilterReset !== undefined && { onShowAll: onFilterReset }), translate: translate, createButton: onCreate !== undefined && !narrowed && !defaultFilterActive && !createInPageHeader ? (_jsx(Button, { variant: "primary", icon: "plus", onClick: onCreate, testId: "render-list-empty-create", children: effectiveCreateLabel })) : undefined })) : undefined);
269
- // Title resolution uses the conventional i18n key `screen:<id>.title`.
270
- // If the bundle knows the key, use its translated title; otherwise use
271
- // screenId so the app developer can see that the translation is missing.
272
- const titleKey = `screen:${screen.id}.title`;
273
- const resolvedTitle = translate(titleKey);
274
- const toolbarTitle = resolvedTitle === titleKey ? screen.id : resolvedTitle;
275
- return (_jsxs(_Fragment, { children: [headerActions !== undefined && PageHeader !== undefined && (_jsx(PageHeader, { actions: headerActions, ...(promotedInOverflowMenu && { overflowItems: promotedMenuItems }) })), menuActionError !== null && (_jsx(Banner, { variant: "error", testId: "render-list-toolbar-action-error", children: menuActionError })), referenceColumns.map((rc) => (_jsx(ReferenceLookupBridge, { field: rc.field, refEntity: rc.refEntity, labelField: rc.labelField, featureName: rc.refFeature, onMap: handleLookupMap }, rc.field))), _jsx(DataTable, { columns: enrichedVm.columns, rows: enrichedVm.rows, toolbarTitle: toolbarTitle, ...(onRowClick !== undefined && { onRowClick }), ...(composedEmptyState !== undefined && { emptyState: composedEmptyState }), ...(toolbarStart !== undefined && { toolbarStart }), ...(description !== undefined && { toolbarDescription: description }), ...(toolbarEnd !== undefined && { toolbarEnd }), ...(sort !== undefined && { sort }), ...(onSortChange !== undefined && { onSortChange }), ...(pager !== undefined && { pager }), ...(onReachEnd !== undefined && { onReachEnd }), ...(loadingMore !== undefined && { loadingMore }), ...(hasMore !== undefined && { hasMore }), ...(itemNoun !== undefined && { itemNoun }), ...(rowActions !== undefined && { rowActions }), ...(rowActionMode !== undefined && { rowActionMode }), ...(bulk !== undefined && { bulk }), ...(rowTone !== undefined && { rowTone }), ...(filterFacets !== undefined && { filterFacets }), ...(filterValues !== undefined && { filterValues }), ...(onFilterChange !== undefined && { onFilterChange }), ...(onFilterReset !== undefined && { onFilterReset }), ...(dateRangeFacets !== undefined && { dateRangeFacets }), ...(onDateRangeChange !== undefined && { onDateRangeChange }), ...(chromeless !== undefined && { chromeless }), ...(scrollBody !== undefined && { scrollBody }), ...(screenPadding !== undefined && { screenPadding }), ...(expandedRowIds !== undefined && { expandedRowIds }), ...(onToggleRowExpanded !== undefined && { onToggleRowExpanded }), ...(renderExpandedRow !== undefined && { renderExpandedRow }), ...tableBlockProps, testId: "render-list-table" })] }));
276
- }
277
- // Header slot in the list toolbar (toolbarEnd): `screen.slots.header` is
278
- // a PlatformComponent (`{ react: { __component: "X" } }`) resolved through
279
- // the same ExtensionSectionsProvider registry as entityEdit sections.
280
- // entityId is null in the list context because there is no row; the component
281
- // loads its own data (for example, a cap counter from a usage query). A
282
- // separate component is required by the Rules of Hooks because
283
- // useExtensionSectionComponent must not run conditionally in RenderList.
284
- function ListHeaderSlotMount({ screen, }) {
285
- const header = screen.slots?.header;
286
- const name = header !== undefined ? extensionSectionName(header) : undefined;
287
- const Component = useExtensionSectionComponent(name);
288
- // A declared-but-unregistered header slot renders nothing; warn so the author
289
- // finds the typo / missing clientFeatures.extensionSectionComponents entry,
290
- // mirroring the diagnostic banner in render-edit's ExtensionSectionMount
291
- // (the toolbar has no room for a banner, so a dev-warn it is).
292
- useEffect(() => {
293
- if (name !== undefined && Component === undefined) {
294
- rendererWarn(`[kumiko] List header slot component "${name}" on screen "${screen.id}" is not registered in clientFeatures.extensionSectionComponents — the header slot renders nothing.`);
295
- }
296
- }, [name, Component, screen.id]);
297
- if (Component === undefined)
298
- return null;
299
- // screenId lets a header control drive this list's URL-filter state
300
- // (useListUrlState) — e.g. a tag filter narrowing the list to an id-set.
301
- return _jsx(Component, { entityName: screen.entity, entityId: null, screenId: screen.id });
302
- }
303
- // Bridge component for each reference column. It is mounted once per
304
- // column, calls useReferenceLookup unconditionally, and publishes the map
305
- // to the parent through onMap. React manages mounting and unmounting when
306
- // the schema changes through the key prop in the parent's map loop.
307
- function ReferenceLookupBridge({ field, refEntity, labelField, featureName, onMap, }) {
308
- const lookup = useReferenceLookup(featureName, refEntity, labelField);
309
- // Use an effect rather than calling directly so setState runs outside the
310
- // render path; otherwise React warns about updating one component while
311
- // rendering another.
312
- // Do not publish while loading or after a failed lookup: a missing map means
313
- // "not resolved", whereas an empty published map means "nothing found".
314
- // Only the latter may display "Unknown".
315
- const resolved = !lookup.loading && !lookup.failed;
316
- useEffect(() => {
317
- if (resolved)
318
- onMap(field, { map: lookup.map, complete: lookup.complete });
319
- }, [field, lookup.map, lookup.complete, resolved, onMap]);
320
- return null;
321
- }
322
- function isPlainRecord(value) {
323
- return typeof value === "object" && value !== null && !Array.isArray(value);
324
- }
325
- // The server leaves a missing or unreadable target out of `_refs`, and a null
326
- // entry means the same: both read as unresolved.
327
- function eagerloadedRef(row, field) {
328
- if (!isPlainRecord(row) || !isPlainRecord(row["_refs"]))
329
- return undefined;
330
- const ref = row["_refs"][field];
331
- if (Array.isArray(ref))
332
- return ref.filter(isPlainRecord);
333
- return isPlainRecord(ref) ? ref : undefined;
53
+ const split = splitToolbarActions({
54
+ toolbarActions,
55
+ pageHeaderAvailable: PageHeader !== undefined,
56
+ pageHeaderSlotAvailable,
57
+ pageHeaderCompact,
58
+ createInPageHeader,
59
+ });
60
+ const toolbarModel = {
61
+ screen,
62
+ Button,
63
+ Dialog,
64
+ Banner,
65
+ onCreate,
66
+ createInPageHeader,
67
+ pageHeaderCompact,
68
+ effectiveCreateLabel,
69
+ split,
70
+ };
71
+ const createButton = renderCreateButton(toolbarModel);
72
+ const promotedMenuItems = buildPromotedMenuItems({
73
+ promotedToolbarActions: split.promotedToolbarActions,
74
+ promotedInOverflowMenu: split.promotedInOverflowMenu,
75
+ menuActionBusyId,
76
+ setMenuActionBusyId,
77
+ setMenuActionError,
78
+ });
79
+ const headerActions = renderHeaderActions(toolbarModel, createButton);
80
+ const toolbarEnd = renderToolbarEnd(toolbarModel, createButton);
81
+ const composedEmptyState = composeEmptyState({
82
+ Button,
83
+ emptyState,
84
+ onCreate,
85
+ emptyContent,
86
+ narrowed,
87
+ defaultFilterActive: defaultFilterActiveProp && onFilterReset !== undefined,
88
+ onFilterReset,
89
+ translate,
90
+ createInPageHeader,
91
+ effectiveCreateLabel,
92
+ });
93
+ const toolbarTitle = resolveToolbarTitle(screen, translate);
94
+ return (_jsxs(_Fragment, { children: [headerActions !== undefined &&
95
+ PageHeader !== undefined &&
96
+ renderPageHeader(PageHeader, headerActions, split.promotedInOverflowMenu, promotedMenuItems), menuActionError !== null && renderMenuActionError(Banner, menuActionError), renderReferenceLookupBridges(referenceColumns, handleLookupMap), _jsx(DataTable, { columns: enrichedVm.columns, rows: enrichedVm.rows, toolbarTitle: toolbarTitle, ...tableChromeProps({
97
+ onRowClick,
98
+ composedEmptyState,
99
+ toolbarStart,
100
+ description,
101
+ toolbarEnd,
102
+ itemNoun,
103
+ }), ...tableSortPagingProps({ sort, onSortChange, pager, onReachEnd, loadingMore, hasMore }), ...tableRowProps({ rowActions, rowActionMode, bulk, rowTone }), ...tableFilterProps({
104
+ filterFacets,
105
+ filterValues,
106
+ onFilterChange,
107
+ onFilterReset,
108
+ dateRangeFacets,
109
+ onDateRangeChange,
110
+ }), ...tableLayoutProps({
111
+ chromeless,
112
+ scrollBody,
113
+ screenPadding,
114
+ expandedRowIds,
115
+ onToggleRowExpanded,
116
+ renderExpandedRow,
117
+ }), ...tableBlockProps, testId: "render-list-table" })] }));
334
118
  }
@@ -0,0 +1,20 @@
1
+ import type { EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { EditFieldViewModel } from "../headless/index.ts";
3
+ export declare function useReferenceInputState(field: EditFieldViewModel, featureName: string, onChange: (value: unknown) => void): {
4
+ refFeature: string;
5
+ isMultiple: boolean;
6
+ options: {
7
+ value: string;
8
+ label: string;
9
+ }[];
10
+ loading: boolean;
11
+ createTarget: {
12
+ screen: EntityEditScreenDefinition;
13
+ entity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition;
14
+ } | undefined;
15
+ createOpen: boolean;
16
+ setCreateOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
17
+ createdWithoutIdWarning: boolean;
18
+ handleCreated: (newId: string | undefined) => void;
19
+ handleSearchChange: (q: string) => void;
20
+ };
@@ -0,0 +1,85 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import { useAppFeatures } from "../app/app-features-context.js";
3
+ import { screenAccessAllows } from "../app/screen-access.js";
4
+ import { useUserRoles } from "../context/user-roles-context.js";
5
+ import { rendererError } from "../dev-console.js";
6
+ import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
7
+ import { useQuery } from "../hooks/use-query.js";
8
+ import { referenceOptionSource } from "../hooks/use-reference-lookup.js";
9
+ import { stringArrayOf } from "./render-field-values.js";
10
+ export function useReferenceInputState(field, featureName, onChange) {
11
+ const refEntity = field.refEntity ?? "";
12
+ const refFeature = field.refFeature ?? featureName;
13
+ const isMultiple = field.refMultiple === true;
14
+ // refFeature may differ from featureName (for example,
15
+ // items.assignee → users:query:user:list). The default is the same feature,
16
+ // as provided by the ViewModel (parseRefTarget).
17
+ // refOptionsQuery replaces that source with an author-supplied handler whose
18
+ // rows already carry a composed `label`.
19
+ const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
20
+ // "+ Neu" in the combobox opens the referenced entity's create screen in a
21
+ // dialog instead of leaving the current form.
22
+ // refFeature may differ from the currently rendered feature; appFeatures
23
+ // (createKumikoApp) knows all feature schemas, not only the active screen.
24
+ // If the feature, screen, or entity is not registered, or allowCreate:false,
25
+ // onCreate remains undefined and the combobox renders without the footer.
26
+ const appFeatures = useAppFeatures();
27
+ const userRoles = useUserRoles();
28
+ const [createOpen, setCreateOpen] = useState(false);
29
+ const refTargetSchema = appFeatures.find((f) => f.featureName === refFeature);
30
+ const refCreateScreen = refTargetSchema?.screens.find((s) => s.type === "entityEdit" &&
31
+ s.entity === refEntity &&
32
+ s.allowCreate !== false &&
33
+ screenAccessAllows(s.access, userRoles));
34
+ const refEntityDef = refTargetSchema?.entities[refEntity];
35
+ // When the user types in the combobox, the server filters through the
36
+ // existing list-payload `search` parameter. The combobox debounces the
37
+ // keystroke itself (300ms) and calls onSearchChange.
38
+ // An empty initial state requests the first 50 items in default order.
39
+ const [searchTerm, setSearchTerm] = useState("");
40
+ const queryPayload = useMemo(() => searchTerm === ""
41
+ ? { limit: REFERENCE_COMBOBOX_LIMIT }
42
+ : { limit: REFERENCE_COMBOBOX_LIMIT, search: searchTerm }, [searchTerm]);
43
+ const queryResult = useQuery(queryQn, queryPayload);
44
+ const handleSearchChange = useCallback((q) => setSearchTerm(q), []);
45
+ const createTarget = !field.readOnly && refCreateScreen !== undefined && refEntityDef !== undefined
46
+ ? { screen: refCreateScreen, entity: refEntityDef }
47
+ : undefined;
48
+ const [createdWithoutIdWarning, setCreatedWithoutIdWarning] = useState(false);
49
+ const handleCreated = useCallback((newId) => {
50
+ setCreateOpen(false);
51
+ // Clear the search filter before refetching — the just-created record
52
+ // won't match whatever the user had typed pre-create, and would then
53
+ // be missing from `options` (shown as a raw id instead of a label).
54
+ setSearchTerm("");
55
+ queryResult.refetch().catch((err) => {
56
+ rendererError("render-field: refetch after create failed", err);
57
+ });
58
+ // Record was created server-side but the payload carried no id —
59
+ // can't auto-select it, surface that instead of failing silently.
60
+ setCreatedWithoutIdWarning(newId === undefined);
61
+ if (newId !== undefined) {
62
+ onChange(isMultiple ? [...stringArrayOf(field.value), newId] : newId);
63
+ }
64
+ }, [isMultiple, field.value, onChange, queryResult.refetch]);
65
+ const options = useMemo(() => {
66
+ const rows = queryResult.data?.rows ?? [];
67
+ return rows.map((row) => {
68
+ const idVal = String(row["id"] ?? "");
69
+ const label = String(row[labelKey] ?? idVal);
70
+ return { value: idVal, label };
71
+ });
72
+ }, [queryResult.data, labelKey]);
73
+ return {
74
+ refFeature,
75
+ isMultiple,
76
+ options,
77
+ loading: queryResult.loading,
78
+ createTarget,
79
+ createOpen,
80
+ setCreateOpen,
81
+ createdWithoutIdWarning,
82
+ handleCreated,
83
+ handleSearchChange,
84
+ };
85
+ }
@@ -0,0 +1,3 @@
1
+ import type { FormValues } from "../headless/index.ts";
2
+ import type { RenderEditCore } from "./render-edit-render-model.ts";
3
+ export declare function useRenderEditControlledSync<TValues extends FormValues, TCtx>({ props, state, form, draft, }: RenderEditCore<TValues, TCtx>): void;
@@ -0,0 +1,26 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useUnsavedInputSource } from "../leave-guard.js";
3
+ import { useWizardFormDraftRestore } from "./use-wizard-form-draft.js";
4
+ export function useRenderEditControlledSync({ props, state, form, draft, }) {
5
+ const { controller, snapshot } = form;
6
+ const { extensionDirty } = state;
7
+ const onDirtyChangeRef = useRef(props.onDirtyChange);
8
+ onDirtyChangeRef.current = props.onDirtyChange;
9
+ const hasUnsavedInput = snapshot.isDirty || extensionDirty;
10
+ useEffect(() => {
11
+ onDirtyChangeRef.current?.(hasUnsavedInput);
12
+ }, [hasUnsavedInput]);
13
+ // Draft wizards keep their input server-side, so leaving loses nothing.
14
+ const persistsDraft = state.isWizard && props.screen.layout.draft === true;
15
+ useUnsavedInputSource(() => !persistsDraft && (controller.getSnapshot().isDirty || state.extensionDirtyRef.current));
16
+ useWizardFormDraftRestore({
17
+ screenId: props.screen.id,
18
+ isCreateMode: state.isCreateMode,
19
+ draftEnabled: state.draftEnabled,
20
+ dispatcher: state.dispatcher,
21
+ controller,
22
+ hasNavigatedRef: draft.refs.hasNavigatedRef,
23
+ setRawStep: state.setRawStep,
24
+ draftState: state.draftState,
25
+ });
26
+ }
@@ -0,0 +1,29 @@
1
+ import { type MutableRefObject } from "react";
2
+ import type { FormValues } from "../headless/index.ts";
3
+ import type { ResolvedRenderEditProps } from "./render-edit-render-model.ts";
4
+ import type { RenderEditForm } from "./use-render-edit-form.ts";
5
+ import type { RenderEditState } from "./use-render-edit-state.ts";
6
+ export type RenderEditRefs<TValues extends FormValues> = {
7
+ readonly onChangeRef: MutableRefObject<ResolvedRenderEditProps<TValues>["onChange"]>;
8
+ readonly deliveredControlsToRef: MutableRefObject<ResolvedRenderEditProps<TValues>["onControlsReady"]>;
9
+ readonly onControlsReadyRef: MutableRefObject<ResolvedRenderEditProps<TValues>["onControlsReady"]>;
10
+ readonly hasNavigatedRef: MutableRefObject<boolean>;
11
+ readonly submittedRef: MutableRefObject<boolean>;
12
+ readonly isSubmittingRef: MutableRefObject<boolean>;
13
+ readonly scopeFieldNamesRef: MutableRefObject<readonly string[] | undefined>;
14
+ readonly saveDraftRef: MutableRefObject<(stepIndex: number) => void>;
15
+ readonly handleSubmitRef: MutableRefObject<(options?: {
16
+ readonly saveNow?: boolean;
17
+ }) => Promise<void>>;
18
+ readonly wizardNextRef: MutableRefObject<() => void>;
19
+ readonly wizardBackRef: MutableRefObject<() => void>;
20
+ readonly currentStepRef: MutableRefObject<number>;
21
+ readonly draftSaveTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>;
22
+ };
23
+ export type RenderEditDraft<TValues extends FormValues> = {
24
+ readonly refs: RenderEditRefs<TValues>;
25
+ readonly scopedValidate: () => boolean;
26
+ readonly markSaved: (fields?: readonly string[]) => void;
27
+ readonly patchAndScheduleDraftSave: (partial: Partial<TValues>) => void;
28
+ };
29
+ export declare function useRenderEditDraftSave<TValues extends FormValues, TCtx>(props: ResolvedRenderEditProps<TValues, TCtx>, state: RenderEditState, form: RenderEditForm<TValues>): RenderEditDraft<TValues>;