@cosmicdrift/kumiko-renderer 0.386.0 → 0.387.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 (185) 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 +2 -2
  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/app-features-context.js +4 -4
  7. package/dist/app/column-renderers.d.ts +7 -9
  8. package/dist/app/column-renderers.js +15 -18
  9. package/dist/app/config-edit-body.js +1 -1
  10. package/dist/app/config-edit-shim.d.ts +7 -8
  11. package/dist/app/config-edit-shim.js +18 -20
  12. package/dist/app/custom-screens.d.ts +3 -3
  13. package/dist/app/custom-screens.js +8 -8
  14. package/dist/app/dashboard-body.d.ts +2 -2
  15. package/dist/app/dashboard-body.js +2 -2
  16. package/dist/app/entity-edit-prefill.js +9 -9
  17. package/dist/app/entity-edit-screen.js +4 -4
  18. package/dist/app/entity-list-screen.js +3 -3
  19. package/dist/app/extension-form-submit.d.ts +1 -1
  20. package/dist/app/extension-form-submit.js +14 -15
  21. package/dist/app/extension-sections.d.ts +24 -25
  22. package/dist/app/extension-sections.js +10 -10
  23. package/dist/app/feature-schema.js +11 -11
  24. package/dist/app/form-schema.js +1 -1
  25. package/dist/app/header-actions-bar.js +1 -1
  26. package/dist/app/kumiko-screen.js +8 -10
  27. package/dist/app/layout-fields.js +1 -1
  28. package/dist/app/list-facets.js +3 -3
  29. package/dist/app/nav.d.ts +10 -10
  30. package/dist/app/nav.js +13 -13
  31. package/dist/app/projection-detail-body.js +4 -4
  32. package/dist/app/projection-detail-shim.d.ts +6 -5
  33. package/dist/app/projection-detail-shim.js +23 -22
  34. package/dist/app/projection-list-body.js +10 -10
  35. package/dist/app/projection-list-shim.d.ts +5 -5
  36. package/dist/app/projection-list-shim.js +17 -18
  37. package/dist/app/qn.js +10 -12
  38. package/dist/app/qualify-screen-id.js +1 -1
  39. package/dist/app/screen-access.js +2 -2
  40. package/dist/app/screen-shared.js +1 -1
  41. package/dist/app/secret-mint-body.js +4 -4
  42. package/dist/app/write-failed-error.js +8 -7
  43. package/dist/components/embedded-list-field.js +4 -4
  44. package/dist/components/grid-cell-for-field.d.ts +1 -1
  45. package/dist/components/reference-create-dialog.js +3 -3
  46. package/dist/components/related-list-section.d.ts +1 -1
  47. package/dist/components/related-list-section.js +1 -1
  48. package/dist/components/render-edit-logic.js +1 -1
  49. package/dist/components/render-edit-types.d.ts +6 -6
  50. package/dist/components/render-edit.js +63 -65
  51. package/dist/components/render-field.d.ts +7 -7
  52. package/dist/components/render-field.js +57 -59
  53. package/dist/components/render-list.d.ts +42 -43
  54. package/dist/components/render-list.js +63 -67
  55. package/dist/components/use-wizard-form-draft.js +10 -10
  56. package/dist/context/dispatcher-context.js +5 -5
  57. package/dist/context/user-roles-context.js +2 -2
  58. package/dist/headless/apex/index.d.ts +1 -2
  59. package/dist/headless/apex/index.js +5 -6
  60. package/dist/headless/form/form-controller.js +6 -6
  61. package/dist/headless/form/zod-bridge.js +2 -2
  62. package/dist/headless/format/money.js +1 -1
  63. package/dist/headless/index.js +2 -2
  64. package/dist/headless/view-model/edit.js +2 -2
  65. package/dist/headless/view-model/list.js +3 -4
  66. package/dist/headless/view-model/types.d.ts +3 -3
  67. package/dist/hooks/reference-limits.js +11 -12
  68. package/dist/hooks/use-list-url-state.d.ts +13 -13
  69. package/dist/hooks/use-list-url-state.js +21 -22
  70. package/dist/hooks/use-query.js +3 -3
  71. package/dist/hooks/use-reference-lookup.js +16 -17
  72. package/dist/hooks/use-store.js +18 -22
  73. package/dist/hooks/use-stream-handler.js +4 -2
  74. package/dist/i18n-defaults.js +2 -8
  75. package/dist/i18n.d.ts +10 -10
  76. package/dist/i18n.js +25 -25
  77. package/dist/index.js +5 -4
  78. package/dist/live/dispatcher-live.js +2 -2
  79. package/dist/primitives.d.ts +225 -221
  80. package/dist/primitives.js +1 -1
  81. package/dist/sse/live-events.d.ts +5 -5
  82. package/dist/sse/live-events.js +17 -17
  83. package/dist/tokens.d.ts +8 -8
  84. package/dist/tokens.js +16 -17
  85. package/dist/web/app/browser-locale.d.ts +9 -9
  86. package/dist/web/app/browser-locale.js +19 -19
  87. package/dist/web/app/client-plugin.d.ts +44 -52
  88. package/dist/web/app/client-plugin.js +4 -5
  89. package/dist/web/app/create-app.d.ts +28 -31
  90. package/dist/web/app/create-app.js +21 -22
  91. package/dist/web/app/document-lang-sync.js +1 -1
  92. package/dist/web/app/draft-storage.js +1 -1
  93. package/dist/web/app/nav-providers-context.d.ts +3 -3
  94. package/dist/web/app/nav-providers-context.js +3 -3
  95. package/dist/web/app/nav.d.ts +18 -17
  96. package/dist/web/app/nav.js +66 -64
  97. package/dist/web/app/rich-content-editor.js +1 -1
  98. package/dist/web/components/config-cascade.js +17 -18
  99. package/dist/web/components/config-source-badge.js +4 -4
  100. package/dist/web/layout/app-layout.d.ts +9 -9
  101. package/dist/web/layout/app-layout.js +2 -2
  102. package/dist/web/layout/avatar.d.ts +4 -3
  103. package/dist/web/layout/avatar.js +15 -17
  104. package/dist/web/layout/default-app-shell.d.ts +22 -23
  105. package/dist/web/layout/editor-panel.js +3 -3
  106. package/dist/web/layout/nav-tree.js +86 -84
  107. package/dist/web/layout/profile-menu.d.ts +1 -1
  108. package/dist/web/layout/sidebar-brand.d.ts +7 -7
  109. package/dist/web/layout/sidebar-user.d.ts +1 -1
  110. package/dist/web/layout/sidebar-user.js +4 -4
  111. package/dist/web/layout/sidebar.d.ts +8 -8
  112. package/dist/web/layout/sidebar.js +7 -7
  113. package/dist/web/layout/target-resolver-stub.d.ts +5 -5
  114. package/dist/web/layout/target-resolver-stub.js +20 -20
  115. package/dist/web/layout/target-url.d.ts +7 -7
  116. package/dist/web/layout/target-url.js +22 -22
  117. package/dist/web/layout/theme-toggle.js +4 -5
  118. package/dist/web/layout/workspace-shell.d.ts +21 -21
  119. package/dist/web/layout/workspace-shell.js +29 -30
  120. package/dist/web/lib/cn.d.ts +4 -4
  121. package/dist/web/lib/cn.js +4 -4
  122. package/dist/web/primitives/action-menu.d.ts +8 -8
  123. package/dist/web/primitives/action-menu.js +14 -13
  124. package/dist/web/primitives/calendar-popover.d.ts +1 -1
  125. package/dist/web/primitives/calendar-popover.js +10 -10
  126. package/dist/web/primitives/card-styles.js +1 -1
  127. package/dist/web/primitives/card.js +2 -2
  128. package/dist/web/primitives/combobox.d.ts +12 -12
  129. package/dist/web/primitives/combobox.js +34 -35
  130. package/dist/web/primitives/data-table-cell.js +2 -2
  131. package/dist/web/primitives/data-table.js +2 -3
  132. package/dist/web/primitives/date-field.js +2 -2
  133. package/dist/web/primitives/date-input.js +4 -4
  134. package/dist/web/primitives/default-form.js +2 -2
  135. package/dist/web/primitives/dropdown-menu.js +14 -15
  136. package/dist/web/primitives/embedded-list-input.js +3 -3
  137. package/dist/web/primitives/facet-filter.js +1 -1
  138. package/dist/web/primitives/field.js +9 -8
  139. package/dist/web/primitives/form.d.ts +1 -1
  140. package/dist/web/primitives/form.js +6 -6
  141. package/dist/web/primitives/input.js +2 -2
  142. package/dist/web/primitives/layout-elements.js +1 -1
  143. package/dist/web/primitives/layout.js +2 -2
  144. package/dist/web/primitives/located-timestamp-input.js +4 -4
  145. package/dist/web/primitives/money-input.js +13 -17
  146. package/dist/web/primitives/pager.js +1 -1
  147. package/dist/web/primitives/row-actions.js +3 -4
  148. package/dist/web/primitives/secret-reveal.js +1 -1
  149. package/dist/web/primitives/timestamp-input.d.ts +9 -9
  150. package/dist/web/primitives/timestamp-input.js +21 -21
  151. package/dist/web/primitives/toast.js +12 -13
  152. package/dist/web/sse/live-events.d.ts +6 -6
  153. package/dist/web/sse/live-events.js +3 -3
  154. package/dist/web/tokens.d.ts +11 -12
  155. package/dist/web/tokens.js +35 -37
  156. package/dist/web/version/update-checker.js +14 -13
  157. package/dist/web/widgets/ai-text-field.js +1 -1
  158. package/dist/web/widgets/charts.d.ts +10 -10
  159. package/dist/web/widgets/charts.js +8 -8
  160. package/dist/web/widgets/collapsible-section.d.ts +3 -3
  161. package/dist/web/widgets/collapsible-section.js +6 -6
  162. package/dist/web/widgets/detail-list.d.ts +1 -3
  163. package/dist/web/widgets/detail-list.js +1 -3
  164. package/dist/web/widgets/feed-list.d.ts +2 -2
  165. package/dist/web/widgets/feed-list.js +2 -2
  166. package/dist/web/widgets/form-fields.d.ts +14 -14
  167. package/dist/web/widgets/form-fields.js +14 -13
  168. package/dist/web/widgets/index.js +4 -4
  169. package/dist/web/widgets/infinity-list.js +3 -3
  170. package/dist/web/widgets/mode-switch.d.ts +2 -2
  171. package/dist/web/widgets/mode-switch.js +2 -2
  172. package/dist/web/widgets/progress-list.d.ts +1 -2
  173. package/dist/web/widgets/progress-list.js +1 -2
  174. package/dist/web/widgets/query-table.d.ts +6 -6
  175. package/dist/web/widgets/result-panel.d.ts +16 -16
  176. package/dist/web/widgets/result-panel.js +10 -10
  177. package/dist/web/widgets/section-card.d.ts +3 -3
  178. package/dist/web/widgets/section-card.js +3 -3
  179. package/dist/web/widgets/stat.d.ts +4 -4
  180. package/dist/web/widgets/stat.js +6 -6
  181. package/dist/web/widgets/states.d.ts +4 -4
  182. package/dist/web/widgets/states.js +4 -4
  183. package/dist/web/widgets/use-draft.d.ts +7 -7
  184. package/dist/web/widgets/use-draft.js +7 -7
  185. package/package.json +6 -6
@@ -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;
@@ -250,9 +250,9 @@ export function RenderEdit(props) {
250
250
  };
251
251
  }, []);
252
252
  // patch() (controlled mode, extension sections) applies immediately —
253
- // only the resulting draft save is debounced (#1914), so a patch() burst
253
+ // only the resulting draft save is debounced, so a patch() burst
254
254
  // (VIN-decode filling several fields, or onChange fanning a keystroke out
255
- // through patch(), see the #1888 test above) collapses into one save.
255
+ // through patch(), see the VIN-decode test above) collapses into one save.
256
256
  const patchAndScheduleDraftSave = useCallback((partial) => {
257
257
  controller.setValues(partial);
258
258
  if (!draftEnabled || dispatcher === undefined || disabled || submittedRef.current)
@@ -270,7 +270,7 @@ export function RenderEdit(props) {
270
270
  // execution order) so that an onChange fired on mount always sees
271
271
  // `controls !== undefined` — both effects depend only on mount-stable
272
272
  // values (controller/scopedValidate/patchAndScheduleDraftSave), so the
273
- // swap is otherwise a no-op. See the #1888 VIN-decode test: an initial
273
+ // swap is otherwise a no-op. See the VIN-decode test: an initial
274
274
  // value that should derive dependent fields on mount needs controls.patch
275
275
  // available on the very first onChange call, not just from the second
276
276
  // keystroke onward.
@@ -377,7 +377,7 @@ export function RenderEdit(props) {
377
377
  // run their own query / own submit), so they always pass through. A
378
378
  // section with no fields at all (e.g. a review-only step) has
379
379
  // `visible: fields.some(...)` = false vacuously; that's "no fields to
380
- // hide", not "hidden", so it stays too (fw#1901). Unless the create
380
+ // hide", not "hidden", so it stays too. Unless the create
381
381
  // exclusion emptied it: that would leave an empty wizard step / tab.
382
382
  () => filterEditSections(vm.sections, fieldsFilter).filter((section) => section.kind === "extension" ||
383
383
  section.kind === "relatedList" ||
@@ -391,9 +391,9 @@ export function RenderEdit(props) {
391
391
  return fillable.length > 0 && fillable.every((field) => field.required);
392
392
  }, [filteredSections]);
393
393
  const showsAllRequiredHint = allFieldsRequired && hideSectionTitles !== true && !isWizard;
394
- // true when editable fields exist or an extension opted into composed submit (fw#2359).
394
+ // true when editable fields exist or an extension opted into composed submit.
395
395
  const isFormEditable = hasEditableSection(filteredSections);
396
- // A fieldless form (input-less secretMint mint, fw#2838 — the secret is
396
+ // A fieldless form (input-less secretMint mint — the secret is
397
397
  // server-generated, the mint step is only its submit button) has no
398
398
  // section to gate on: it IS the action, so it always shows the submit
399
399
  // button, and it can never go dirty either.
@@ -404,8 +404,8 @@ export function RenderEdit(props) {
404
404
  // reaches this line and `filteredSections[0]` stays the active section)
405
405
  // needs its own tab panel capped at the available height so its table
406
406
  // 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 —
407
+ // row count — a short table still sizes to its content instead
408
+ // of stretching the panel to the bottom. Any other layout —
409
409
  // multiple sections, a non-relatedList tab, stacked (non-tabs) forms —
410
410
  // keeps normal document-flow height untouched.
411
411
  const fillHeight = fillScreenHeight === true ||
@@ -415,9 +415,9 @@ export function RenderEdit(props) {
415
415
  // instead of sitting in the section's title row.
416
416
  const isLoneWriteFormTab = hideSectionTitles === true && filteredSections[0]?.kind === "writeForm";
417
417
  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.
418
+ // Persist all composed extension sections using the resolved entityId.
419
+ // false means a section failed and its i18n key is shown in the banner. Without
420
+ // entity context (create mode without a route id), there is nothing to write.
421
421
  async function persistExtensions() {
422
422
  const entityId = resolveExtensionEntityId(entityIdProp, vm.id);
423
423
  if (entityId === null)
@@ -495,7 +495,7 @@ export function RenderEdit(props) {
495
495
  function handleWizardNext() {
496
496
  const next = Math.min(currentStep + 1, lastStepIndex);
497
497
  hasNavigatedRef.current = true;
498
- // Locked state (#1896): `disabled` means "no input/no write", not "no
498
+ // Locked state: `disabled` means "no input/no write", not "no
499
499
  // navigation" — a disabled wizard must still be steppable so Back/Next
500
500
  // reach every step. But neither validate() (would block on an empty
501
501
  // required field nobody can fill in) nor saveDraft() (would mint a
@@ -569,7 +569,7 @@ export function RenderEdit(props) {
569
569
  handleWizardNext();
570
570
  return;
571
571
  }
572
- // Locked state (#1896): the submit button is visibly disabled, but a
572
+ // Locked state: the submit button is visibly disabled, but a
573
573
  // native form submit (Enter key) reaches this handler regardless of the
574
574
  // button's disabled attribute — block it here too, not just in the UI.
575
575
  if (disabled || submitBlocked)
@@ -582,9 +582,9 @@ export function RenderEdit(props) {
582
582
  setIsSubmitting(true);
583
583
  setExtensionErrorKey(null);
584
584
  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.
585
+ // Extension-only: only a section is dirty and the main form is unchanged.
586
+ // Skip the entity write (it would send an empty changes payload) and only
587
+ // run the section handlers.
588
588
  if (snapshot.isUnchanged && extensionDirty) {
589
589
  // Same discard as the main path: an extension-only save is still a
590
590
  // successful submit, so the draft must not survive it.
@@ -597,22 +597,19 @@ export function RenderEdit(props) {
597
597
  }
598
598
  let result;
599
599
  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).
600
+ // customSubmit path (e.g. configEdit, which performs a separate write per
601
+ // field). Validate on the client first, then call the caller; on success,
602
+ // explicitly rebase the form state because controller.submit() normally
603
+ // does this itself, and the controller otherwise knows nothing about a
604
+ // successful custom submission (isUnchanged would remain false).
606
605
  //
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.
606
+ // Read the snapshot directly from the controller rather than React state.
607
+ // With rapid fill-then-click input, React batching may not have committed
608
+ // state updates when the submit click fires. handleSubmit's closure could otherwise
609
+ // see stale snapshot.changes={} and perform no writes, then return success
610
+ // and rebase the form, making it appear that the data was saved.
611
+ // controller.getSnapshot() is always current: the controller is the
612
+ // source of truth, while React state is only a rendering mirror.
616
613
  const valid = controller.validate(scopeFieldNames);
617
614
  if (!valid) {
618
615
  // Field order matters: validationBlocked:true is its own
@@ -642,7 +639,7 @@ export function RenderEdit(props) {
642
639
  // `isNoOp: true` (payloadMode "changes", pre-filled form submitted
643
640
  // untouched) means controller.submit() never called dispatcher.write
644
641
  // — nothing to discard, and discarding here would delete a draft the
645
- // form never persisted (fw#1978). Awaited, not fire-and-forget:
642
+ // form never persisted. Awaited, not fire-and-forget:
646
643
  // `onSubmit` typically navigates away and unmounts this form, which
647
644
  // would abort an in-flight discard and leave the draft behind after
648
645
  // a successful submit.
@@ -679,7 +676,7 @@ export function RenderEdit(props) {
679
676
  // otherwise invisible — jump to the first step that contains one of
680
677
  // the errored fields. For tabs this is what keeps the submit from
681
678
  // blocking in silence, the failure mode the boot-validator used to
682
- // prevent by rejecting the layout outright (fw#3134). An issue no
679
+ // prevent by rejecting the layout outright. An issue no
683
680
  // rendered field can show (`version`, `id`, a root-level refine, a
684
681
  // hidden field) keeps the banner, jump or not.
685
682
  if (isStepped) {
@@ -850,14 +847,14 @@ export function RenderEdit(props) {
850
847
  : hasDirtyFooter
851
848
  ? "kumiko.form.saveChanges"
852
849
  : "kumiko.actions.save")) })), writeFormFooterAction] }));
853
- // Title + Subtitle, create/edit-bewusst. i18n-Keys (mode = "create"|"edit"):
850
+ // Title + Subtitle, create/edit-aware. i18n-Keys (mode = "create"|"edit"):
854
851
  // screen:<id>.<mode>.title / .<mode>.subtitle (mode = "create"|"edit"|"view")
855
- // Fallback-Kette: mode-spezifisch → generisch (screen:<id>.title/.subtitle).
852
+ // Fallback chain: mode-specific → generic (screen:<id>.title/.subtitle).
856
853
  // title falls back to screenId; subtitle falls back to screen.description,
857
854
  // itself run through translate() so an i18n key (the established
858
855
  // convention for description, e.g. bundled-features) resolves instead of
859
856
  // 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
857
+ // prose, same as it does for an unknown key. Same slot
861
858
  // section.description already fills for a section. Falls back to
862
859
  // undefined (no subtitle) when neither is set.
863
860
  const formMode = isCreate ? "create" : viewMode ? "view" : "edit";
@@ -984,7 +981,7 @@ export function RenderEdit(props) {
984
981
  // survives navigating past its step — otherwise Finish only ran the
985
982
  // last mounted step's handler and silently dropped earlier steps'
986
983
  // writes. For tabs this is also what makes ONE submit cover every
987
- // tab, including tabs the user never opened (fw#3134).
984
+ // tab, including tabs the user never opened.
988
985
  const stepHidden = isStepped && sectionIndex !== currentStep;
989
986
  const wrapWizardStep = (key, el) => {
990
987
  if (!isStepped)
@@ -1025,13 +1022,13 @@ export function RenderEdit(props) {
1025
1022
  }
1026
1023
  if (section.kind === "relatedList") {
1027
1024
  // parentId is the displayed record's id — without it there's no
1028
- // parent row whose related rows could be queried (fw#2166).
1025
+ // parent row whose related rows could be queried.
1029
1026
  // Rejected at boot in wizard layouts, so no WizardStepGroup here.
1030
1027
  const parentId = resolveExtensionEntityId(entityIdProp, vm.id);
1031
1028
  if (parentId === null)
1032
1029
  return null;
1033
1030
  const mount = (_jsx(RelatedListSection, { section: section, parentId: parentId,
1034
- // @cast-boundary form-values: TValues ist strukturell ein Record.
1031
+ // @cast-boundary form-values: TValues is structurally a Record.
1035
1032
  record: snapshot.values, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, ...(refreshToken !== undefined && { refreshToken }), ...(fillScreenHeight === true && { grow: true }), ...(hideSectionTitles !== true &&
1036
1033
  sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(onRelatedListDrawerAction !== undefined && {
1037
1034
  onOpenDrawer: onRelatedListDrawerAction,
@@ -1048,7 +1045,8 @@ export function RenderEdit(props) {
1048
1045
  }
1049
1046
  if (!section.visible)
1050
1047
  return null;
1051
- // Titellose Sections kollidieren sonst auf key/testId — Index-Fallback.
1048
+ // Title-less sections would otherwise collide on key/testId, so use the index
1049
+ // as a fallback.
1052
1050
  const sectionKey = section.title ?? `section-${sectionIndex}`;
1053
1051
  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
1052
  layout: "row",
@@ -1085,7 +1083,7 @@ export function RenderEdit(props) {
1085
1083
  // undefined here at runtime.
1086
1084
  return wrapWizardStep(sectionKey, _jsx(Card, { options: { framed: false }, children: groupsGrid }, sectionKey));
1087
1085
  }
1088
- // No section title here (fw#3218) — the Tab strip right above
1086
+ // No section title here — the Tab strip right above
1089
1087
  // already names this panel, so a repeated Card title would just
1090
1088
  // duplicate it. The header row only exists to carry the section
1091
1089
  // description and actions, never the title text.
@@ -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>>;
@@ -29,11 +29,10 @@ const AllFieldsRequiredContext = createContext(false);
29
29
  export const AllFieldsRequiredProvider = AllFieldsRequiredContext.Provider;
30
30
  export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay = "form", row, changed, layout, accent, fillDisabled, hideLabel, }) {
31
31
  const { Field, Input, Banner, Text, JsonView } = usePrimitives();
32
- // App-Locale (i18n) für money/date-Inputs — sonst fielen sie auf
33
- // navigator.language (Browser-Sprache) zurück statt der gewählten
34
- // App-Sprache. BEWUSSTE API-Verschärfung (seit 0.38): RenderField ist
35
- // public exportiert und verlangt jetzt einen LocaleProvider —
36
- // Standalone-Consumer/Tests müssen wrappen (createKumikoApp tut es).
32
+ // The app locale is required for money/date inputs; otherwise they fall back
33
+ // to navigator.language instead of the selected app language. RenderField is
34
+ // publicly exported and therefore requires a LocaleProvider; standalone
35
+ // consumers and tests must wrap it (createKumikoApp does this).
37
36
  const appLocale = useLocale().locale();
38
37
  const t = useTranslation();
39
38
  const allFieldsRequired = useContext(AllFieldsRequiredContext);
@@ -45,13 +44,13 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
45
44
  // the list-column path (render-list.tsx). Only takes effect once the field
46
45
  // is actually readOnly — a FormatSpec/PlatformComponent renderer has no
47
46
  // editable widget of its own, so applying it to an editable field would
48
- // silently make that field un-editable (fw#2245).
47
+ // silently make that field un-editable.
49
48
  const readOnlyText = valueDisplay === "text" && field.readOnly && field.renderer === undefined;
50
- // Reference-Field rendert eine eigene Component — sie nutzt
51
- // useQuery() für den Live-Lookup, also muss sie als React-
52
- // Komponente gemountet werden (nicht als pure render-Call).
53
- // The list input prints its own list-level issues (empty state included), so
54
- // the Field wrapper must not print them a second time.
49
+ // The reference field renders its own component because it uses useQuery()
50
+ // for the live lookup and must therefore be mounted as a React component,
51
+ // not called as a pure render function.
52
+ // The list input prints its own list-level issues (including the empty state),
53
+ // so the Field wrapper must not print them a second time.
55
54
  const listPrintsOwnIssues = field.type === "embedded" &&
56
55
  field.embeddedListCells !== undefined &&
57
56
  !(field.readOnly && field.renderer !== undefined);
@@ -168,38 +167,35 @@ function WriteOnlyTextInput({ field, id, hasError, onChange, changed, }) {
168
167
  },
169
168
  }) }));
170
169
  }
171
- // Tier 2.7e-3 + 2.1c: Reference-Input rendert eine Searchable Combobox
172
- // gefüllt aus einer Live-Query auf die referenced Entity. Default-
173
- // Limit: 200 — bei größeren Datasets fehlt der Tail im Dropdown
174
- // (Tier 2.7e-Remote: server-side Search-Query mit debounce kommt später).
175
- // - Display = row[refLabelField], Default labelField "id".
176
- // - Loading-State: leeres Dropdown bis die rows da sind. Field
177
- // ist disabled während useQuery läuft.
178
- // - Multi-Mode (Tier 2.7e-Multi via field.refMultiple): value ist
179
- // ein UUID-Array, Combobox rendert Selected-Tags.
170
+ // ReferenceInput renders a searchable combobox populated by a live query for
171
+ // the referenced entity. With the default limit of 200, larger datasets miss
172
+ // the tail in the dropdown; a server-side debounced search query comes later.
173
+ // - Display = row[refLabelField], default labelField "id".
174
+ // - During loading, the dropdown is empty and the field is disabled.
175
+ // - In multiple mode (field.refMultiple), value is a UUID array and the
176
+ // combobox renders tags.
180
177
  //
181
- // Storage: UI-Wert ist UUID (row.id) oder UUID-Array bei multiple.
182
- // Server-Schema: z.uuid() bzw. z.array(z.uuid()).
183
- // REFERENCE_COMBOBOX_LIMIT lebt zentral in hooks/reference-limits.ts
184
- // (siehe dort für Begründung der Default-Werte).
178
+ // Storage: the UI value is a UUID (row.id), or a UUID array in multiple mode.
179
+ // Server schema: z.uuid() or z.array(z.uuid()).
180
+ // REFERENCE_COMBOBOX_LIMIT is centralized in hooks/reference-limits.ts
181
+ // (see there for the rationale behind the default values).
185
182
  function ReferenceInput({ field, id, hasError, onChange, Input, featureName, }) {
186
183
  const { Banner } = usePrimitives();
187
184
  const refEntity = field.refEntity ?? "";
188
185
  const refFeature = field.refFeature ?? featureName;
189
186
  const isMultiple = field.refMultiple === true;
190
- // Tier 2.7e Cross-Feature: refFeature kann ≠ featureName sein
191
- // (z.B. items.assignee → users:query:user:list). Default ist
192
- // same-feature, kommt aus dem ViewModel (parseRefTarget).
193
- // fw#2780: refOptionsQuery replaces that source with an author-supplied
194
- // handler whose rows already carry a composed `label`.
187
+ // refFeature may differ from featureName (for example,
188
+ // items.assignee → users:query:user:list). The default is the same feature,
189
+ // as provided by the ViewModel (parseRefTarget).
190
+ // refOptionsQuery replaces that source with an author-supplied handler whose
191
+ // rows already carry a composed `label`.
195
192
  const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
196
- // Issue #1681: "+ Neu" in der Combobox öffnet den Create-Screen der
197
- // referenced entity als Dialog, statt die aktuelle Form zu verlassen.
198
- // refFeature kann ein anderes Feature als das aktuell gerenderte sein
199
- // — appFeatures (createKumikoApp) kennt alle Feature-Schemas, nicht
200
- // nur das der aktiven Screen. Kein Match (Feature/Screen/Entity nicht
201
- // registriert, oder allowCreate:false) → onCreate bleibt undefined,
202
- // Combobox rendert dann ohne den Footer.
193
+ // "+ Neu" in the combobox opens the referenced entity's create screen in a
194
+ // dialog instead of leaving the current form.
195
+ // refFeature may differ from the currently rendered feature; appFeatures
196
+ // (createKumikoApp) knows all feature schemas, not only the active screen.
197
+ // If the feature, screen, or entity is not registered, or allowCreate:false,
198
+ // onCreate remains undefined and the combobox renders without the footer.
203
199
  const appFeatures = useAppFeatures();
204
200
  const userRoles = useUserRoles();
205
201
  const t = useTranslation();
@@ -210,10 +206,10 @@ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, })
210
206
  s.allowCreate !== false &&
211
207
  screenAccessAllows(s.access, userRoles));
212
208
  const refEntityDef = refTargetSchema?.entities[refEntity];
213
- // Tier 2.7e Remote-Search: User tippt im Combobox → Server filtert
214
- // via existing list-payload `search`-Param (Tier 2.6c). Combobox
215
- // debounced den keystroke selbst (300ms) und ruft onSearchChange.
216
- // Initial-State leer → erste 50 Items vom Server (default-sortiert).
209
+ // When the user types in the combobox, the server filters through the
210
+ // existing list-payload `search` parameter. The combobox debounces the
211
+ // keystroke itself (300ms) and calls onSearchChange.
212
+ // An empty initial state requests the first 50 items in default order.
217
213
  const [searchTerm, setSearchTerm] = useState("");
218
214
  const queryPayload = useMemo(() => searchTerm === ""
219
215
  ? { limit: REFERENCE_COMBOBOX_LIMIT }
@@ -254,13 +250,14 @@ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, })
254
250
  return { value: idVal, label };
255
251
  });
256
252
  }, [queryResult.data, labelKey]);
257
- // Single: value ist String/null; Multi: Array. Coerce auf das was
258
- // der Combobox-Mode erwartet, damit Storage-Drift (Server liefert
259
- // alten String wo jetzt Array erwartet wird) keine Crash auslöst.
253
+ // Single mode uses a String/null value; multiple mode uses an array. Coerce
254
+ // to the shape expected by the combobox so storage drift (for example, an
255
+ // old String from the server where an array is now expected) cannot crash it.
260
256
  const baseInputProps = {
261
257
  id,
262
258
  name: field.field,
263
- // Initial-Load disabled — danach loading-Indicator im Popover.
259
+ // Disable the input during the initial load; subsequent loads show a
260
+ // loading indicator in the popover.
264
261
  disabled: field.readOnly || (queryResult.loading && options.length === 0),
265
262
  required: field.required,
266
263
  hasError,
@@ -295,7 +292,7 @@ function FieldRendererOutput({ field, renderer, row, appLocale, }) {
295
292
  if (isFormatSpec(renderer)) {
296
293
  // format:"json" wants the raw value (JsonView stringifies + highlights
297
294
  // itself) — applyFormatSpec's already-indented string is only the
298
- // fallback for primitives-providers without JsonView (fw#2312).
295
+ // fallback for primitives-providers without JsonView.
299
296
  if (renderer.format === "json" && JsonView !== undefined) {
300
297
  return (_jsx(JsonView, { value: field.value, indent: renderer.indent, testId: `field-value-${field.field}` }));
301
298
  }
@@ -308,8 +305,8 @@ function FieldRendererOutput({ field, renderer, row, appLocale, }) {
308
305
  // App locale as default when the FormatSpec declares none of its own —
309
306
  // otherwise locale-sensitive formats (timestamp/date/number/decimal/
310
307
  // bigInt/unit) fell back to Intl's runtime default instead of the app
311
- // language chosen via LocaleProvider (fw#2187). Prefer renderer.locale
312
- // when set; coalesce undefined (spread override) back to appLocale (#2332).
308
+ // language chosen via LocaleProvider. Prefer renderer.locale
309
+ // when set; coalesce undefined (spread override) back to appLocale.
313
310
  return (_jsx(Text, { testId: `field-value-${field.field}`, children: applyFormatSpec({
314
311
  ...renderer,
315
312
  locale: stringPropertyOf(renderer, "locale") ?? appLocale,
@@ -417,7 +414,7 @@ function resolveFieldIcon(field) {
417
414
  // from the EditFieldViewModel (computeEditViewModel pulls them from
418
415
  // SelectFieldDef.options). Structural types without a widget (embedded,
419
416
  // jsonb, multiSelect) render read-only instead of a data-destroying text
420
- // fallback (#1834); truly unknown scalar types still fall back to text.
417
+ // fallback; truly unknown scalar types still fall back to text.
421
418
  function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner, Text, JsonView, t, row, fill, }) {
422
419
  const common = {
423
420
  id,
@@ -438,7 +435,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
438
435
  // decimal/bigInt are both plain numbers on the wire (fieldToZod:
439
436
  // z.number() / z.number().int().safe()) — the number input's onChange
440
437
  // already emits `number | undefined`, so no extra coercion is needed
441
- // beyond what "number" already does (#1925).
438
+ // beyond what "number" already does.
442
439
  case "number":
443
440
  case "bigInt": {
444
441
  const icon = resolveFieldIcon(field);
@@ -472,11 +469,11 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
472
469
  case "locatedTimestamp":
473
470
  return (_jsx(Input, { kind: "locatedTimestamp", ...common, value: locatedValue(field.value), onChange: (v) => onChange(v), locale: field.dateLocale ?? appLocale, ...(field.min !== undefined && { min: field.min }), ...(field.max !== undefined && { max: field.max }) }));
474
471
  case "select": {
475
- // Translated Option-Labels kommen aus dem ViewModel-Builder
476
- // (computeEditViewModel, Convention-Key
472
+ // Translated option labels come from the ViewModel builder
473
+ // (computeEditViewModel, convention key
477
474
  // `<feature>:entity:<entity>:field:<field>:option:<value>`).
478
- // Wenn keine Translations registriert sind, fallback auf raw
479
- // value als Label — der ComboboxInput zeigt dann unverändert.
475
+ // If no translations are registered, fall back to the raw value as the
476
+ // label, which ComboboxInput then shows unchanged.
480
477
  const selectOptions = field.optionLabels !== undefined ? staticSelectOptions(field) : (field.options ?? []);
481
478
  // `display` is shared with multiSelect's "checkboxes", which is not a
482
479
  // select presentation — narrow to the two a select field can request.
@@ -495,11 +492,11 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
495
492
  case "images":
496
493
  return (_jsx(FileListField, { field: field, id: id, kind: field.type === "images" ? "image" : "file", hasError: hasError, onChange: (ids) => onChange(ids) }));
497
494
  // embedded (without embeddedListCells — that's embeddedList, which has
498
- // had its own EmbeddedListField widget since #1838) and jsonb carry
495
+ // had its own EmbeddedListField widget) and jsonb carry
499
496
  // arbitrary objects. Without a dedicated widget these must NOT fall
500
497
  // through to a text input: stringValue() turns them into
501
498
  // "[object Object]" and saving that overwrites the real data with the
502
- // mangled string (#1834). A `required: true` on either is caught loudly
499
+ // mangled string. A `required: true` on either is caught loudly
503
500
  // at boot (validateNoWidgetRequiredField in the framework package)
504
501
  // instead of silently failing here.
505
502
  case "embedded":
@@ -519,7 +516,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
519
516
  // is set (the view-model carries it), the renderer switches to
520
517
  // textarea. longText always renders a textarea — that's the point of
521
518
  // the type — regardless of whether `multiline` is set; `multiline`
522
- // only supplies an optional `{ rows }` override for it (#1925).
519
+ // only supplies an optional `{ rows }` override for it.
523
520
  if (field.type === "longText" || (field.type === "text" && field.multiline)) {
524
521
  const rows = typeof field.multiline === "object" ? field.multiline.rows : undefined;
525
522
  return (_jsx(Input, { kind: "textarea", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(rows !== undefined && { rows }), ...placeholderProp }));
@@ -628,8 +625,9 @@ function moneyPayload(minorUnits, currency) {
628
625
  return undefined;
629
626
  return { amount: minorUnits / 10 ** currencyDecimals(currency), currency };
630
627
  }
631
- // locatedTimestamp-Feldwert: das Read-Wrapper liefert `{ at, tz, utc }`; leer
632
- // (noch nicht gesetzt) → "" als Empty-Sentinel, analog money/timestamp.
628
+ // A locatedTimestamp field value is supplied by the read wrapper as
629
+ // `{ at, tz, utc }`; an unset value becomes "" as the empty sentinel,
630
+ // consistent with money and timestamp fields.
633
631
  function locatedValue(v) {
634
632
  if (v !== null && typeof v === "object" && "at" in v && "tz" in v) {
635
633
  const utc = propertyOf(v, "utc");
@@ -644,7 +642,7 @@ function locatedValue(v) {
644
642
  // Read-only text for a `field.readOnly` field without its own `renderer` —
645
643
  // per-type formatting that reuses the same value-shaping helpers as the
646
644
  // editable widgets above, so a text value and its would-be Input widget stay
647
- // derived from the identical parse (fw#2245). `type: "reference"` isn't
645
+ // derived from the identical parse. `type: "reference"` isn't
648
646
  // covered here — it needs a live label lookup, see ReadOnlyReferenceValue.
649
647
  // `isComplexFieldType` types aren't covered either — callers keep those on
650
648
  // `renderInput`'s existing Banner fallback.