@cosmicdrift/kumiko-renderer 0.386.0 → 0.388.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 (222) hide show
  1. package/LICENSE +21 -57
  2. package/dist/app/action-form-body.d.ts +2 -2
  3. package/dist/app/action-form-body.js +12 -4
  4. package/dist/app/action-form-shim.d.ts +8 -8
  5. package/dist/app/action-form-shim.js +20 -22
  6. package/dist/app/action-form-support.d.ts +1 -0
  7. package/dist/app/action-form-support.js +1 -1
  8. package/dist/app/app-features-context.js +4 -4
  9. package/dist/app/column-renderers.d.ts +7 -9
  10. package/dist/app/column-renderers.js +15 -18
  11. package/dist/app/config-edit-body.js +7 -3
  12. package/dist/app/config-edit-shim.d.ts +7 -8
  13. package/dist/app/config-edit-shim.js +18 -20
  14. package/dist/app/custom-screens.d.ts +3 -3
  15. package/dist/app/custom-screens.js +8 -8
  16. package/dist/app/dashboard-body.d.ts +2 -2
  17. package/dist/app/dashboard-body.js +2 -2
  18. package/dist/app/entity-edit-prefill.d.ts +11 -0
  19. package/dist/app/entity-edit-prefill.js +18 -13
  20. package/dist/app/entity-edit-screen.js +4 -4
  21. package/dist/app/entity-list-screen.js +3 -3
  22. package/dist/app/extension-form-submit.d.ts +1 -1
  23. package/dist/app/extension-form-submit.js +14 -15
  24. package/dist/app/extension-sections.d.ts +29 -28
  25. package/dist/app/extension-sections.js +10 -10
  26. package/dist/app/feature-schema.js +11 -11
  27. package/dist/app/form-schema.js +2 -2
  28. package/dist/app/header-actions-bar.js +2 -2
  29. package/dist/app/kumiko-screen.js +8 -10
  30. package/dist/app/layout-fields.js +1 -1
  31. package/dist/app/list-facets.js +3 -3
  32. package/dist/app/nav.d.ts +10 -10
  33. package/dist/app/nav.js +13 -13
  34. package/dist/app/projection-detail-body.js +4 -4
  35. package/dist/app/projection-detail-shim.d.ts +6 -5
  36. package/dist/app/projection-detail-shim.js +23 -22
  37. package/dist/app/projection-list-body.js +10 -10
  38. package/dist/app/projection-list-shim.d.ts +5 -5
  39. package/dist/app/projection-list-shim.js +17 -18
  40. package/dist/app/qn.js +10 -12
  41. package/dist/app/qualify-screen-id.js +1 -1
  42. package/dist/app/screen-access.js +2 -2
  43. package/dist/app/screen-shared.js +1 -1
  44. package/dist/app/secret-mint-body.js +4 -4
  45. package/dist/app/write-failed-error.js +8 -7
  46. package/dist/components/edit-mounts.d.ts +2 -2
  47. package/dist/components/embedded-list-field.js +5 -5
  48. package/dist/components/grid-cell-for-field.d.ts +1 -1
  49. package/dist/components/reference-create-dialog.js +3 -3
  50. package/dist/components/related-list-section.d.ts +1 -1
  51. package/dist/components/related-list-section.js +15 -2
  52. package/dist/components/render-edit-action-button.d.ts +1 -3
  53. package/dist/components/render-edit-action-button.js +2 -2
  54. package/dist/components/render-edit-logic.js +1 -1
  55. package/dist/components/render-edit-types.d.ts +6 -6
  56. package/dist/components/render-edit.js +74 -73
  57. package/dist/components/render-field.d.ts +7 -7
  58. package/dist/components/render-field.js +62 -61
  59. package/dist/components/render-list.d.ts +42 -43
  60. package/dist/components/render-list.js +93 -84
  61. package/dist/components/use-wizard-form-draft.js +10 -10
  62. package/dist/context/dispatcher-context.js +5 -5
  63. package/dist/context/user-roles-context.js +2 -2
  64. package/dist/flow-diagram/render.js +9 -1
  65. package/dist/headless/apex/index.d.ts +1 -2
  66. package/dist/headless/apex/index.js +5 -6
  67. package/dist/headless/form/form-controller.js +14 -6
  68. package/dist/headless/form/types.d.ts +1 -0
  69. package/dist/headless/form/zod-bridge.js +2 -2
  70. package/dist/headless/format/index.js +89 -64
  71. package/dist/headless/format/money.js +1 -1
  72. package/dist/headless/index.js +2 -2
  73. package/dist/headless/view-model/edit.js +4 -2
  74. package/dist/headless/view-model/list.js +184 -150
  75. package/dist/headless/view-model/types.d.ts +6 -3
  76. package/dist/hooks/live-refetch-scheduler.js +4 -0
  77. package/dist/hooks/reference-limits.js +11 -12
  78. package/dist/hooks/use-list-url-state.d.ts +13 -13
  79. package/dist/hooks/use-list-url-state.js +21 -22
  80. package/dist/hooks/use-query.js +3 -3
  81. package/dist/hooks/use-reference-lookup.d.ts +9 -1
  82. package/dist/hooks/use-reference-lookup.js +32 -19
  83. package/dist/hooks/use-store.js +18 -22
  84. package/dist/hooks/use-stream-handler.js +4 -2
  85. package/dist/i18n-defaults.js +6 -8
  86. package/dist/i18n.d.ts +10 -10
  87. package/dist/i18n.js +25 -25
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.js +6 -4
  90. package/dist/live/dispatcher-live.js +2 -2
  91. package/dist/primitives.d.ts +244 -221
  92. package/dist/primitives.js +1 -1
  93. package/dist/sse/live-events.d.ts +5 -5
  94. package/dist/sse/live-events.js +17 -17
  95. package/dist/tokens.d.ts +8 -8
  96. package/dist/tokens.js +16 -17
  97. package/dist/web/app/browser-locale.d.ts +9 -9
  98. package/dist/web/app/browser-locale.js +19 -19
  99. package/dist/web/app/client-plugin.d.ts +44 -52
  100. package/dist/web/app/client-plugin.js +4 -5
  101. package/dist/web/app/create-app.d.ts +28 -31
  102. package/dist/web/app/create-app.js +29 -25
  103. package/dist/web/app/dashboard-body.js +17 -1
  104. package/dist/web/app/dashboard-panels.js +12 -2
  105. package/dist/web/app/document-lang-sync.js +1 -1
  106. package/dist/web/app/draft-storage.js +1 -1
  107. package/dist/web/app/nav-providers-context.d.ts +3 -3
  108. package/dist/web/app/nav-providers-context.js +3 -3
  109. package/dist/web/app/nav.d.ts +18 -17
  110. package/dist/web/app/nav.js +66 -64
  111. package/dist/web/app/rich-content-editor.js +1 -1
  112. package/dist/web/components/config-cascade.js +17 -18
  113. package/dist/web/components/config-source-badge.js +4 -4
  114. package/dist/web/index.d.ts +1 -1
  115. package/dist/web/index.js +1 -1
  116. package/dist/web/layout/app-layout.d.ts +9 -9
  117. package/dist/web/layout/app-layout.js +2 -2
  118. package/dist/web/layout/avatar.d.ts +4 -3
  119. package/dist/web/layout/avatar.js +15 -17
  120. package/dist/web/layout/default-app-shell.d.ts +22 -23
  121. package/dist/web/layout/editor-panel.js +3 -3
  122. package/dist/web/layout/nav-tree.js +86 -84
  123. package/dist/web/layout/profile-menu.d.ts +1 -1
  124. package/dist/web/layout/shell-header.js +2 -1
  125. package/dist/web/layout/sidebar-brand.d.ts +7 -7
  126. package/dist/web/layout/sidebar-user.d.ts +1 -1
  127. package/dist/web/layout/sidebar-user.js +4 -4
  128. package/dist/web/layout/sidebar.d.ts +8 -8
  129. package/dist/web/layout/sidebar.js +7 -7
  130. package/dist/web/layout/target-resolver-stub.d.ts +5 -5
  131. package/dist/web/layout/target-resolver-stub.js +20 -20
  132. package/dist/web/layout/target-url.d.ts +7 -7
  133. package/dist/web/layout/target-url.js +22 -22
  134. package/dist/web/layout/theme-toggle.js +4 -5
  135. package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
  136. package/dist/web/layout/uniform-bar-controls.js +4 -0
  137. package/dist/web/layout/workspace-shell.d.ts +21 -21
  138. package/dist/web/layout/workspace-shell.js +29 -30
  139. package/dist/web/lib/cn.d.ts +4 -4
  140. package/dist/web/lib/cn.js +4 -4
  141. package/dist/web/lib/file-ref-download.js +4 -1
  142. package/dist/web/primitives/action-menu.d.ts +8 -8
  143. package/dist/web/primitives/action-menu.js +14 -13
  144. package/dist/web/primitives/calendar-popover.d.ts +1 -1
  145. package/dist/web/primitives/calendar-popover.js +10 -10
  146. package/dist/web/primitives/card-styles.js +1 -1
  147. package/dist/web/primitives/card.js +2 -2
  148. package/dist/web/primitives/code-input.d.ts +6 -0
  149. package/dist/web/primitives/code-input.js +31 -0
  150. package/dist/web/primitives/combobox.d.ts +12 -12
  151. package/dist/web/primitives/combobox.js +34 -35
  152. package/dist/web/primitives/data-table-cell.js +2 -2
  153. package/dist/web/primitives/data-table.js +6 -6
  154. package/dist/web/primitives/date-field.js +2 -2
  155. package/dist/web/primitives/date-input.js +4 -4
  156. package/dist/web/primitives/default-form.js +2 -2
  157. package/dist/web/primitives/dropdown-menu.js +14 -15
  158. package/dist/web/primitives/embedded-list-input.js +3 -3
  159. package/dist/web/primitives/facet-filter.js +2 -2
  160. package/dist/web/primitives/field.js +9 -8
  161. package/dist/web/primitives/file-thumbnail.d.ts +3 -0
  162. package/dist/web/primitives/file-thumbnail.js +5 -0
  163. package/dist/web/primitives/form.d.ts +1 -1
  164. package/dist/web/primitives/form.js +6 -6
  165. package/dist/web/primitives/input.js +8 -3
  166. package/dist/web/primitives/layout-elements.js +7 -5
  167. package/dist/web/primitives/layout.js +2 -2
  168. package/dist/web/primitives/located-timestamp-input.js +4 -4
  169. package/dist/web/primitives/money-input.js +13 -17
  170. package/dist/web/primitives/page-header.js +3 -1
  171. package/dist/web/primitives/pager.js +1 -1
  172. package/dist/web/primitives/row-actions.js +6 -7
  173. package/dist/web/primitives/secret-reveal.js +1 -1
  174. package/dist/web/primitives/timestamp-input.d.ts +9 -9
  175. package/dist/web/primitives/timestamp-input.js +21 -21
  176. package/dist/web/primitives/toast.js +12 -13
  177. package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
  178. package/dist/web/primitives/use-focus-on-mount.js +9 -0
  179. package/dist/web/sse/live-events.d.ts +6 -6
  180. package/dist/web/sse/live-events.js +3 -3
  181. package/dist/web/tokens.d.ts +11 -12
  182. package/dist/web/tokens.js +35 -37
  183. package/dist/web/version/update-checker.js +14 -13
  184. package/dist/web/widgets/ai-text-field.js +1 -1
  185. package/dist/web/widgets/bounded-load-cache.d.ts +1 -1
  186. package/dist/web/widgets/bounded-load-cache.js +10 -4
  187. package/dist/web/widgets/charts.d.ts +10 -10
  188. package/dist/web/widgets/charts.js +8 -8
  189. package/dist/web/widgets/choice-cards.d.ts +17 -0
  190. package/dist/web/widgets/choice-cards.js +12 -0
  191. package/dist/web/widgets/collapsible-section.d.ts +3 -3
  192. package/dist/web/widgets/collapsible-section.js +7 -7
  193. package/dist/web/widgets/detail-list.d.ts +5 -3
  194. package/dist/web/widgets/detail-list.js +5 -3
  195. package/dist/web/widgets/feed-list.d.ts +2 -2
  196. package/dist/web/widgets/feed-list.js +2 -2
  197. package/dist/web/widgets/form-fields.d.ts +14 -14
  198. package/dist/web/widgets/form-fields.js +14 -13
  199. package/dist/web/widgets/index.d.ts +1 -0
  200. package/dist/web/widgets/index.js +5 -4
  201. package/dist/web/widgets/infinity-list.js +3 -3
  202. package/dist/web/widgets/mode-switch.d.ts +2 -2
  203. package/dist/web/widgets/mode-switch.js +2 -2
  204. package/dist/web/widgets/pdf-preview.js +1 -1
  205. package/dist/web/widgets/plan-card.d.ts +2 -0
  206. package/dist/web/widgets/plan-card.js +1 -1
  207. package/dist/web/widgets/progress-list.d.ts +1 -2
  208. package/dist/web/widgets/progress-list.js +1 -2
  209. package/dist/web/widgets/query-table.d.ts +6 -6
  210. package/dist/web/widgets/result-panel.d.ts +16 -16
  211. package/dist/web/widgets/result-panel.js +10 -10
  212. package/dist/web/widgets/section-card.d.ts +3 -3
  213. package/dist/web/widgets/section-card.js +3 -3
  214. package/dist/web/widgets/stat.d.ts +4 -4
  215. package/dist/web/widgets/stat.js +6 -6
  216. package/dist/web/widgets/states.d.ts +4 -4
  217. package/dist/web/widgets/states.js +4 -4
  218. package/dist/web/widgets/upload-zone.js +13 -5
  219. package/dist/web/widgets/use-draft.d.ts +7 -7
  220. package/dist/web/widgets/use-draft.js +7 -7
  221. package/package.json +6 -6
  222. package/src/changes.json +49 -0
@@ -32,11 +32,11 @@ export function RenderEdit(props) {
32
32
  const disabled = disabledProp || viewMode;
33
33
  const i18nScreenId = props.i18nScreenId ?? screen.id;
34
34
  const { customSubmit } = props;
35
- // Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
36
- // konsumieren wir den i18next-Context direkt. Sonst wären Field-
37
- // Labels ohne Caller-Wiring raw-Keys (`feature:entity:foo:field:title`).
38
- // useTranslation throwt ohne LocaleProvider — das ist ok, weil RenderEdit
39
- // ohnehin nur in einem mounted Kumiko-App-Tree läuft.
35
+ // Translate fallback: when the caller does not provide a translate function,
36
+ // consume the i18next context directly. Otherwise field labels would remain raw
37
+ // keys (`feature:entity:foo:field:title`) without caller wiring.
38
+ // useTranslation throws without a LocaleProvider, which is acceptable because
39
+ // RenderEdit only runs inside a mounted Kumiko app tree.
40
40
  const t = useTranslation();
41
41
  const translate = translateProp ?? t;
42
42
  const dispatcher = useOptionalDispatcher();
@@ -44,7 +44,7 @@ export function RenderEdit(props) {
44
44
  // Tabs reuse the wizard's step machinery: one section visible at a time,
45
45
  // all of them mounted, `currentStep` as the active index. What they do NOT
46
46
  // share is navigation — a tab form submits from any tab, so every
47
- // step-transition path below stays wizard-only (fw#3134).
47
+ // step-transition path below stays wizard-only.
48
48
  const isTabs = screen.layout.mode === "tabs";
49
49
  const draftEnabled = isWizard && screen.layout.draft === true && dispatcher !== undefined;
50
50
  const isCreateMode = entityIdProp === undefined || entityIdProp === null || entityIdProp === "";
@@ -63,8 +63,8 @@ export function RenderEdit(props) {
63
63
  // One state for all header actions (actions?), not per-button — only one
64
64
  // action can be in flight at a time, and this keeps the error surfaced in
65
65
  // the shared formError-adjacent banner region instead of inside the
66
- // button row (fw#2166 review: a full-width Banner there breaks the
67
- // flex justify-end action bar).
66
+ // button row: a full-width Banner there breaks the
67
+ // flex justify-end action bar.
68
68
  const [actionError, setActionError] = useState(null);
69
69
  const [rawStep, setRawStep] = useState(0);
70
70
  const [passedStepKeys, setPassedStepKeys] = useState(() => new Set());
@@ -82,15 +82,15 @@ export function RenderEdit(props) {
82
82
  draftStorage,
83
83
  });
84
84
  const { draftCandidates, setDraftCandidates } = draftState;
85
- // Composed-Save: Extension-Sections melden hier ihren dirty-State (damit der
86
- // Save-Button aktiv wird wenn NUR eine Section geändert wurde) + ihren
87
- // Submit-Handler (läuft nach dem Entity-Write). extensionErrorKey hält den
88
- // i18n-Key einer fehlgeschlagenen Section-Persistierung.
85
+ // Composed save: extension sections report their dirty state so the Save button
86
+ // activates when only a section changed, and provide a submit handler that runs
87
+ // after the entity write. extensionErrorKey stores the i18n key for a failed
88
+ // section persistence.
89
89
  //
90
- // Not a second write path for saveDraft()'s data (#1914): deriveFormFields
91
- // (above) skips extension sections entirely, so extension-owned field state
92
- // never enters `fields`/`controller.getSnapshot().values` — there is no
93
- // draft-blob-covered state left for persistExtensions() to compete over.
90
+ // This is not a second write path for saveDraft()'s data: deriveFormFields
91
+ // skips extension sections entirely, so extension-owned field state never enters
92
+ // `fields` or `controller.getSnapshot().values`; there is no draft-covered state
93
+ // left for persistExtensions() to compete over.
94
94
  const [extensionDirty, setExtensionDirty] = useState(false);
95
95
  // Mirror for the leave guard: it must see "saved" before React re-renders.
96
96
  const extensionDirtyRef = useRef(false);
@@ -109,7 +109,7 @@ export function RenderEdit(props) {
109
109
  // Both stepped layouts show one section at a time and keep the rest mounted
110
110
  // but inert. A tabs layout on a host without the Tabs primitive has no strip
111
111
  // to switch with, so it falls back to rendering every section stacked rather
112
- // than stranding the user on section 0 (fw#3134). Same fallback when the
112
+ // than stranding the user on section 0. Same fallback when the
113
113
  // WizardStepGroup wrapper is missing.
114
114
  const isStepped = isWizard || (isTabs && Tabs !== undefined && WizardStepGroup !== undefined);
115
115
  const fields = useMemo(() => deriveFormFields(screen), [screen]);
@@ -121,7 +121,7 @@ export function RenderEdit(props) {
121
121
  // scoping would silently drop root-level .refine() issues.
122
122
  // A `fieldsFilter` that matches nothing in `fields` (typo, renamed field)
123
123
  // must fall back to unscoped too — an empty scope array would filter out
124
- // ALL validation issues (kumiko-framework#1907), letting submit() through
124
+ // ALL validation issues, letting submit() through
125
125
  // unvalidated on a form that also renders no sections.
126
126
  const scopeFieldNames = useMemo(() => {
127
127
  if (fieldsFilter === undefined)
@@ -129,9 +129,9 @@ export function RenderEdit(props) {
129
129
  const matched = fieldsFilter.filter((f) => Object.hasOwn(fields, f));
130
130
  return matched.length === 0 ? undefined : matched;
131
131
  }, [fieldsFilter, fields]);
132
- // Submit-Config nur wenn der Caller einen writeCommand mitgibt; bei
133
- // customSubmit-Pfad kommt der Form-Controller ohne Submit-Wiring,
134
- // weil wir controller.submit() eh nicht rufen.
132
+ // Configure submission only when the caller provides a writeCommand; with
133
+ // customSubmit, the form controller has no submit wiring because
134
+ // controller.submit() is not called.
135
135
  const writeVerb = isCreateMode ? "create" : "update";
136
136
  // A write-excluded field must never reach the payload: the handler schema is
137
137
  // z.never() for it, so even an unchanged read-only value would be rejected.
@@ -191,12 +191,12 @@ export function RenderEdit(props) {
191
191
  controller.setValues(initial);
192
192
  controller.rebase();
193
193
  }, [refreshToken, controller, initial]);
194
- // Controlled mode (Issue #1887). Both callbacks live in refs so a
194
+ // Controlled mode. Both callbacks live in refs so a
195
195
  // re-rendering caller (identity-unstable `onChange`/`onControlsReady`
196
196
  // closures, e.g. inline arrows) doesn't retrigger these effects — only a
197
197
  // real snapshot mutation (typing, patch()) does. Without this, a caller
198
198
  // whose onChange calls patch() to fill other fields (the VIN-decode use
199
- // case from #1888) would loop: patch → new snapshot → effect
199
+ // case) would loop: patch → new snapshot → effect
200
200
  // re-subscribes with a "new" onChange → refires.
201
201
  const onChangeRef = useRef(onChange);
202
202
  onChangeRef.current = onChange;
@@ -205,7 +205,7 @@ export function RenderEdit(props) {
205
205
  // effect below on every render since the callback itself is now a dep).
206
206
  // Tracks the actual prop, not a ref snapshot, so a caller that swaps in a
207
207
  // real handler after mount (e.g. `onControlsReady={ready ? cb : undefined}`)
208
- // gets delivered to for THIS mount instead of never (fw#1899).
208
+ // gets delivered to for THIS mount instead of never.
209
209
  const deliveredControlsToRef = useRef(undefined);
210
210
  const onControlsReadyRef = useRef(onControlsReady);
211
211
  onControlsReadyRef.current = onControlsReady;
@@ -215,10 +215,13 @@ export function RenderEdit(props) {
215
215
  const scopeFieldNamesRef = useRef(scopeFieldNames);
216
216
  scopeFieldNamesRef.current = scopeFieldNames;
217
217
  const scopedValidate = useCallback(() => controller.validate(scopeFieldNamesRef.current), [controller]);
218
- const markSaved = useCallback(() => {
219
- controller.rebase();
220
- extensionDirtyRef.current = false;
221
- setExtensionDirty(false);
218
+ // The extension dirty flag aggregates every section; each section clears its
219
+ // own part, so a host-side markSaved must not reset it.
220
+ const markSaved = useCallback((fields) => {
221
+ if (fields === undefined)
222
+ controller.rebase();
223
+ else
224
+ controller.rebaseFields(fields);
222
225
  }, [controller]);
223
226
  // `saveDraft` (defined below) is a fresh closure every render, over that
224
227
  // render's `draftKey`/`draftId` — the ref lets the debounce timer below
@@ -250,9 +253,9 @@ export function RenderEdit(props) {
250
253
  };
251
254
  }, []);
252
255
  // patch() (controlled mode, extension sections) applies immediately —
253
- // only the resulting draft save is debounced (#1914), so a patch() burst
256
+ // only the resulting draft save is debounced, so a patch() burst
254
257
  // (VIN-decode filling several fields, or onChange fanning a keystroke out
255
- // through patch(), see the #1888 test above) collapses into one save.
258
+ // through patch(), see the VIN-decode test above) collapses into one save.
256
259
  const patchAndScheduleDraftSave = useCallback((partial) => {
257
260
  controller.setValues(partial);
258
261
  if (!draftEnabled || dispatcher === undefined || disabled || submittedRef.current)
@@ -270,7 +273,7 @@ export function RenderEdit(props) {
270
273
  // execution order) so that an onChange fired on mount always sees
271
274
  // `controls !== undefined` — both effects depend only on mount-stable
272
275
  // values (controller/scopedValidate/patchAndScheduleDraftSave), so the
273
- // swap is otherwise a no-op. See the #1888 VIN-decode test: an initial
276
+ // swap is otherwise a no-op. See the VIN-decode test: an initial
274
277
  // value that should derive dependent fields on mount needs controls.patch
275
278
  // available on the very first onChange call, not just from the second
276
279
  // keystroke onward.
@@ -377,7 +380,7 @@ export function RenderEdit(props) {
377
380
  // run their own query / own submit), so they always pass through. A
378
381
  // section with no fields at all (e.g. a review-only step) has
379
382
  // `visible: fields.some(...)` = false vacuously; that's "no fields to
380
- // hide", not "hidden", so it stays too (fw#1901). Unless the create
383
+ // hide", not "hidden", so it stays too. Unless the create
381
384
  // exclusion emptied it: that would leave an empty wizard step / tab.
382
385
  () => filterEditSections(vm.sections, fieldsFilter).filter((section) => section.kind === "extension" ||
383
386
  section.kind === "relatedList" ||
@@ -391,9 +394,9 @@ export function RenderEdit(props) {
391
394
  return fillable.length > 0 && fillable.every((field) => field.required);
392
395
  }, [filteredSections]);
393
396
  const showsAllRequiredHint = allFieldsRequired && hideSectionTitles !== true && !isWizard;
394
- // true when editable fields exist or an extension opted into composed submit (fw#2359).
397
+ // true when editable fields exist or an extension opted into composed submit.
395
398
  const isFormEditable = hasEditableSection(filteredSections);
396
- // A fieldless form (input-less secretMint mint, fw#2838 — the secret is
399
+ // A fieldless form (input-less secretMint mint — the secret is
397
400
  // server-generated, the mint step is only its submit button) has no
398
401
  // section to gate on: it IS the action, so it always shows the submit
399
402
  // button, and it can never go dirty either.
@@ -404,8 +407,8 @@ export function RenderEdit(props) {
404
407
  // reaches this line and `filteredSections[0]` stays the active section)
405
408
  // needs its own tab panel capped at the available height so its table
406
409
  // scrolls inside the panel instead of the whole page stretching to the
407
- // row count (fw#2722) — a short table still sizes to its content instead
408
- // of stretching the panel to the bottom (fw#2778). Any other layout —
410
+ // row count — a short table still sizes to its content instead
411
+ // of stretching the panel to the bottom. Any other layout —
409
412
  // multiple sections, a non-relatedList tab, stacked (non-tabs) forms —
410
413
  // keeps normal document-flow height untouched.
411
414
  const fillHeight = fillScreenHeight === true ||
@@ -415,9 +418,9 @@ export function RenderEdit(props) {
415
418
  // instead of sitting in the section's title row.
416
419
  const isLoneWriteFormTab = hideSectionTitles === true && filteredSections[0]?.kind === "writeForm";
417
420
  const [writeFormFooterAction, setWriteFormFooterAction] = useState(undefined);
418
- // Persistiert alle composed Extension-Sections mit der aufgelösten entityId.
419
- // false = eine Section schlug fehl (ihr i18n-Key landet im Banner). Ohne
420
- // Entity-Kontext (create-mode ohne route-id) gibt es nichts zu schreiben.
421
+ // Persist all composed extension sections using the resolved entityId.
422
+ // false means a section failed and its i18n key is shown in the banner. Without
423
+ // entity context (create mode without a route id), there is nothing to write.
421
424
  async function persistExtensions() {
422
425
  const entityId = resolveExtensionEntityId(entityIdProp, vm.id);
423
426
  if (entityId === null)
@@ -495,7 +498,7 @@ export function RenderEdit(props) {
495
498
  function handleWizardNext() {
496
499
  const next = Math.min(currentStep + 1, lastStepIndex);
497
500
  hasNavigatedRef.current = true;
498
- // Locked state (#1896): `disabled` means "no input/no write", not "no
501
+ // Locked state: `disabled` means "no input/no write", not "no
499
502
  // navigation" — a disabled wizard must still be steppable so Back/Next
500
503
  // reach every step. But neither validate() (would block on an empty
501
504
  // required field nobody can fill in) nor saveDraft() (would mint a
@@ -569,7 +572,7 @@ export function RenderEdit(props) {
569
572
  handleWizardNext();
570
573
  return;
571
574
  }
572
- // Locked state (#1896): the submit button is visibly disabled, but a
575
+ // Locked state: the submit button is visibly disabled, but a
573
576
  // native form submit (Enter key) reaches this handler regardless of the
574
577
  // button's disabled attribute — block it here too, not just in the UI.
575
578
  if (disabled || submitBlocked)
@@ -582,9 +585,9 @@ export function RenderEdit(props) {
582
585
  setIsSubmitting(true);
583
586
  setExtensionErrorKey(null);
584
587
  try {
585
- // Extension-only: only a section is dirty, the main form is unchanged.
586
- // No entity write (would send an empty changes payload) — only
587
- // die Section-Handler laufen lassen.
588
+ // Extension-only: only a section is dirty and the main form is unchanged.
589
+ // Skip the entity write (it would send an empty changes payload) and only
590
+ // run the section handlers.
588
591
  if (snapshot.isUnchanged && extensionDirty) {
589
592
  // Same discard as the main path: an extension-only save is still a
590
593
  // successful submit, so the draft must not survive it.
@@ -597,22 +600,19 @@ export function RenderEdit(props) {
597
600
  }
598
601
  let result;
599
602
  if (customSubmit !== undefined) {
600
- // customSubmit path (e.g. configEdit, which fires a separate write
601
- // per field). Client-side validation first, then
602
- // an den Caller; on-success rebased der Form-State explizit
603
- // because controller.submit() normally does this itself and
604
- // without customSubmit's help the controller knows nothing about
605
- // erfolgreichen Submit (isUnchanged blieb sonst false).
603
+ // customSubmit path (e.g. configEdit, which performs a separate write per
604
+ // field). Validate on the client first, then call the caller; on success,
605
+ // explicitly rebase the form state because controller.submit() normally
606
+ // does this itself, and the controller otherwise knows nothing about a
607
+ // successful custom submission (isUnchanged would remain false).
606
608
  //
607
- // WICHTIG: snapshot direkt vom Controller holen statt aus
608
- // React state. On rapid fill→click, React batching may not have
609
- // committed input state updates yet when the submit click fires.
610
- // handleSubmit's closure would then run with
611
- // stale snapshot.changes={} laufen, customSubmit fired keine
612
- // Writes, returnt success, Form rebase → User glaubt "saved"
613
- // aber gar nichts ist passiert. controller.getSnapshot() ist
614
- // immer aktuell — der Controller ist die Source-of-Truth, die
615
- // React state is only a mirror for rendering.
609
+ // Read the snapshot directly from the controller rather than React state.
610
+ // With rapid fill-then-click input, React batching may not have committed
611
+ // state updates when the submit click fires. handleSubmit's closure could otherwise
612
+ // see stale snapshot.changes={} and perform no writes, then return success
613
+ // and rebase the form, making it appear that the data was saved.
614
+ // controller.getSnapshot() is always current: the controller is the
615
+ // source of truth, while React state is only a rendering mirror.
616
616
  const valid = controller.validate(scopeFieldNames);
617
617
  if (!valid) {
618
618
  // Field order matters: validationBlocked:true is its own
@@ -642,7 +642,7 @@ export function RenderEdit(props) {
642
642
  // `isNoOp: true` (payloadMode "changes", pre-filled form submitted
643
643
  // untouched) means controller.submit() never called dispatcher.write
644
644
  // — nothing to discard, and discarding here would delete a draft the
645
- // form never persisted (fw#1978). Awaited, not fire-and-forget:
645
+ // form never persisted. Awaited, not fire-and-forget:
646
646
  // `onSubmit` typically navigates away and unmounts this form, which
647
647
  // would abort an in-flight discard and leave the draft behind after
648
648
  // a successful submit.
@@ -679,7 +679,7 @@ export function RenderEdit(props) {
679
679
  // otherwise invisible — jump to the first step that contains one of
680
680
  // the errored fields. For tabs this is what keeps the submit from
681
681
  // blocking in silence, the failure mode the boot-validator used to
682
- // prevent by rejecting the layout outright (fw#3134). An issue no
682
+ // prevent by rejecting the layout outright. An issue no
683
683
  // rendered field can show (`version`, `id`, a root-level refine, a
684
684
  // hidden field) keeps the banner, jump or not.
685
685
  if (isStepped) {
@@ -850,14 +850,14 @@ export function RenderEdit(props) {
850
850
  : hasDirtyFooter
851
851
  ? "kumiko.form.saveChanges"
852
852
  : "kumiko.actions.save")) })), writeFormFooterAction] }));
853
- // Title + Subtitle, create/edit-bewusst. i18n-Keys (mode = "create"|"edit"):
853
+ // Title + Subtitle, create/edit-aware. i18n-Keys (mode = "create"|"edit"):
854
854
  // screen:<id>.<mode>.title / .<mode>.subtitle (mode = "create"|"edit"|"view")
855
- // Fallback-Kette: mode-spezifisch → generisch (screen:<id>.title/.subtitle).
855
+ // Fallback chain: mode-specific → generic (screen:<id>.title/.subtitle).
856
856
  // title falls back to screenId; subtitle falls back to screen.description,
857
857
  // itself run through translate() so an i18n key (the established
858
858
  // convention for description, e.g. bundled-features) resolves instead of
859
859
  // showing the raw key — translate() returns the input unchanged for plain
860
- // prose, same as it does for an unknown key (fw#2723 review). Same slot
860
+ // prose, same as it does for an unknown key. Same slot
861
861
  // section.description already fills for a section. Falls back to
862
862
  // undefined (no subtitle) when neither is set.
863
863
  const formMode = isCreate ? "create" : viewMode ? "view" : "edit";
@@ -984,7 +984,7 @@ export function RenderEdit(props) {
984
984
  // survives navigating past its step — otherwise Finish only ran the
985
985
  // last mounted step's handler and silently dropped earlier steps'
986
986
  // writes. For tabs this is also what makes ONE submit cover every
987
- // tab, including tabs the user never opened (fw#3134).
987
+ // tab, including tabs the user never opened.
988
988
  const stepHidden = isStepped && sectionIndex !== currentStep;
989
989
  const wrapWizardStep = (key, el) => {
990
990
  if (!isStepped)
@@ -1025,13 +1025,13 @@ export function RenderEdit(props) {
1025
1025
  }
1026
1026
  if (section.kind === "relatedList") {
1027
1027
  // parentId is the displayed record's id — without it there's no
1028
- // parent row whose related rows could be queried (fw#2166).
1028
+ // parent row whose related rows could be queried.
1029
1029
  // Rejected at boot in wizard layouts, so no WizardStepGroup here.
1030
1030
  const parentId = resolveExtensionEntityId(entityIdProp, vm.id);
1031
1031
  if (parentId === null)
1032
1032
  return null;
1033
1033
  const mount = (_jsx(RelatedListSection, { section: section, parentId: parentId,
1034
- // @cast-boundary form-values: TValues ist strukturell ein Record.
1034
+ // @cast-boundary form-values: TValues is structurally a Record.
1035
1035
  record: snapshot.values, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, ...(refreshToken !== undefined && { refreshToken }), ...(fillScreenHeight === true && { grow: true }), ...(hideSectionTitles !== true &&
1036
1036
  sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(onRelatedListDrawerAction !== undefined && {
1037
1037
  onOpenDrawer: onRelatedListDrawerAction,
@@ -1048,9 +1048,10 @@ export function RenderEdit(props) {
1048
1048
  }
1049
1049
  if (!section.visible)
1050
1050
  return null;
1051
- // Titellose Sections kollidieren sonst auf key/testId — Index-Fallback.
1051
+ // Title-less sections would otherwise collide on key/testId, so use the index
1052
+ // as a fallback.
1052
1053
  const sectionKey = section.title ?? `section-${sectionIndex}`;
1053
- const renderFieldGrid = (fields, columns, gridKey) => (_jsx(Grid, { columns: columns, ...(isSettingsList ? { list: true } : usesFieldFlow && { flow: true }), children: fields.map((field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(!isSettingsList && usesFieldFlow && { flow: true }), ...(isSettingsList && {
1054
+ const renderFieldGrid = (fields, columns, gridKey, columnsDeclared) => (_jsx(Grid, { columns: usesFieldFlow && columnsDeclared !== true ? 1 : columns, ...(isSettingsList ? { list: true } : usesFieldFlow && { flow: true }), children: fields.map((field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(!isSettingsList && usesFieldFlow && { flow: true }), ...(isSettingsList && {
1054
1055
  layout: "row",
1055
1056
  accent: fieldAccent?.(field.field) === true,
1056
1057
  }), issues: snapshot.errors[field.field], onChange: (v) => {
@@ -1077,7 +1078,7 @@ export function RenderEdit(props) {
1077
1078
  // cannot stand in for the tabs-mode card here.
1078
1079
  if (hideSectionTitles === true) {
1079
1080
  if (section.groups !== undefined) {
1080
- const groupsGrid = (_jsx(Grid, { columns: 1, children: section.groups.map((group, groupIndex) => (_jsx(Card, { slots: { title: group.title }, testId: `section-${sectionKey}-group-${groupIndex}`, children: renderFieldGrid(group.fields, group.columns, `${sectionKey}-group-${groupIndex}-grid`) }, group.title))) }, `${sectionKey}-groups`));
1081
+ const groupsGrid = (_jsx(Grid, { columns: 1, children: section.groups.map((group, groupIndex) => (_jsx(Card, { slots: { title: group.title }, testId: `section-${sectionKey}-group-${groupIndex}`, children: renderFieldGrid(group.fields, group.columns, `${sectionKey}-group-${groupIndex}-grid`, group.columnsDeclared) }, group.title))) }, `${sectionKey}-groups`));
1081
1082
  // `actions` on a `groups` section has no group of its own to
1082
1083
  // sit in and would need an extra title-less Card around the
1083
1084
  // group cards (Card-in-Card) — the boot-validator rejects that
@@ -1085,7 +1086,7 @@ export function RenderEdit(props) {
1085
1086
  // undefined here at runtime.
1086
1087
  return wrapWizardStep(sectionKey, _jsx(Card, { options: { framed: false }, children: groupsGrid }, sectionKey));
1087
1088
  }
1088
- // No section title here (fw#3218) — the Tab strip right above
1089
+ // No section title here — the Tab strip right above
1089
1090
  // already names this panel, so a repeated Card title would just
1090
1091
  // duplicate it. The header row only exists to carry the section
1091
1092
  // description and actions, never the title text.
@@ -1094,7 +1095,7 @@ export function RenderEdit(props) {
1094
1095
  ...(section.description !== undefined && { subtitle: section.description }),
1095
1096
  ...(sectionActionsEl !== undefined && { headerActions: sectionActionsEl }),
1096
1097
  },
1097
- }), testId: `section-${sectionKey}`, children: renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`) }, sectionKey));
1098
+ }), testId: `section-${sectionKey}`, children: renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`, section.columnsDeclared) }, sectionKey));
1098
1099
  return wrapWizardStep(sectionKey, cardEl);
1099
1100
  }
1100
1101
  // Suppress the section header when it would just repeat the form
@@ -1111,7 +1112,7 @@ export function RenderEdit(props) {
1111
1112
  const sectionSubtitle = section.description ?? (headerFallback ? formSubtitle : undefined);
1112
1113
  const sectionEl = (_jsxs(Section, { ...(sectionNavItems.some((item) => item.id === formSectionDomId(sectionIndex)) && {
1113
1114
  id: formSectionDomId(sectionIndex),
1114
- }), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(sectionSubtitle !== undefined && { subtitle: sectionSubtitle }), ...(section.icon !== undefined && { icon: section.icon }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(isSettingsList && { layout: "settings-list" }), testId: `section-${sectionKey}`, children: [showsAllRequiredHint && insideDrawer && sectionIndex === 0 && allRequiredHint, renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`)] }, sectionKey));
1115
+ }), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(sectionSubtitle !== undefined && { subtitle: sectionSubtitle }), ...(section.icon !== undefined && { icon: section.icon }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(isSettingsList && { layout: "settings-list" }), testId: `section-${sectionKey}`, children: [showsAllRequiredHint && insideDrawer && sectionIndex === 0 && allRequiredHint, renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`, section.columnsDeclared)] }, sectionKey));
1115
1116
  return wrapWizardStep(sectionKey, sectionEl);
1116
1117
  }), formError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-form-error", actions: formError.code === "version_conflict" && onReload !== undefined ? (_jsx(Button, { variant: "secondary", onClick: () => {
1117
1118
  onReload();
@@ -5,14 +5,14 @@ export type RenderFieldProps = {
5
5
  readonly field: EditFieldViewModel;
6
6
  readonly issues?: readonly FieldIssue[];
7
7
  readonly onChange: (val: unknown) => void;
8
- /** Nur bei type:"reference" relevant — Feature-Name für die Lookup-
9
- * Query-QN (`<feature>:query:<refEntity>:list`). Andere Field-Types
10
- * ignorieren das Prop. */
8
+ /** Relevant only for type:"reference": the feature name for the lookup
9
+ * query QN (`<feature>:query:<refEntity>:list`). Other field types
10
+ * ignore this prop. */
11
11
  readonly featureName?: string;
12
- /** Optionaler Zusatz-Inhalt der nach dem Label gerendert wird (z.B.
12
+ /** Optional content rendered after the label (for example,
13
13
  * ConfigSourceBadge). */
14
14
  readonly labelAppendix?: ReactNode;
15
- /** Optionaler Zusatz-Inhalt der nach dem Input gerendert wird (z.B.
15
+ /** Optional content rendered after the input (for example,
16
16
  * ConfigCascade). */
17
17
  readonly fieldAppendix?: ReactNode;
18
18
  readonly appendixPlacement?: FieldProps["appendixPlacement"];
@@ -26,12 +26,12 @@ export type RenderFieldProps = {
26
26
  /** "form" (default) renders every field as its editable Input, disabled
27
27
  * when `field.readOnly` — unchanged behavior. "text" renders a
28
28
  * `field.readOnly` field as plain text instead (projectionDetail's read
29
- * view, fw#2245); editable fields are untouched by this prop either way. */
29
+ * view); editable fields are untouched by this prop either way. */
30
30
  readonly valueDisplay?: "form" | "text";
31
31
  /** Current form values, keyed by field name. Consulted when
32
32
  * `field.renderer` resolves to a `{ react: { __component } }` registry
33
33
  * component (passed through as `ColumnRendererProps.row`, same contract
34
- * as list-column renderers, fw#2245), and to resolve a `type: "number"`
34
+ * as list-column renderers), and to resolve a `type: "number"`
35
35
  * field's sibling-field `unit`. Omitted → falls back to a single-key
36
36
  * `{ [field.field]: field.value }` row. */
37
37
  readonly row?: Readonly<Record<string, unknown>>;