@cosmicdrift/kumiko-renderer 0.327.0 → 0.328.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 (257) hide show
  1. package/dist/app/action-form-shim.d.ts +14 -0
  2. package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -44
  3. package/dist/app/app-features-context.d.ts +8 -0
  4. package/dist/app/app-features-context.js +16 -0
  5. package/dist/app/column-renderers.d.ts +30 -0
  6. package/dist/app/column-renderers.js +34 -0
  7. package/dist/app/config-edit-shim.d.ts +11 -0
  8. package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
  9. package/dist/app/content-editors.d.ts +38 -0
  10. package/dist/app/content-editors.js +26 -0
  11. package/dist/app/content-preview.d.ts +11 -0
  12. package/dist/app/content-preview.js +30 -0
  13. package/dist/app/custom-screens.d.ts +11 -0
  14. package/dist/app/custom-screens.js +20 -0
  15. package/dist/app/dashboard-body.d.ts +16 -0
  16. package/dist/app/dashboard-body.js +11 -0
  17. package/dist/app/extension-form-submit.d.ts +32 -0
  18. package/dist/app/extension-form-submit.js +73 -0
  19. package/dist/app/extension-sections.d.ts +72 -0
  20. package/dist/app/extension-sections.js +34 -0
  21. package/dist/app/feature-schema.d.ts +4 -0
  22. package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
  23. package/dist/app/form-schema.d.ts +4 -0
  24. package/dist/app/form-schema.js +85 -0
  25. package/dist/app/kumiko-screen.d.ts +43 -0
  26. package/dist/app/kumiko-screen.js +2206 -0
  27. package/dist/app/layout-fields.d.ts +3 -0
  28. package/dist/app/layout-fields.js +20 -0
  29. package/dist/app/list-facets.d.ts +48 -0
  30. package/dist/app/list-facets.js +185 -0
  31. package/dist/app/nav.d.ts +61 -0
  32. package/dist/app/nav.js +139 -0
  33. package/dist/app/projection-detail-shim.d.ts +8 -0
  34. package/dist/app/projection-detail-shim.js +71 -0
  35. package/dist/app/projection-list-shim.d.ts +13 -0
  36. package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -38
  37. package/dist/app/qn.d.ts +7 -0
  38. package/{src/app/qn.ts → dist/app/qn.js} +9 -11
  39. package/dist/app/qualify-screen-id.d.ts +2 -0
  40. package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
  41. package/dist/app/reference-facet-bridge.d.ts +10 -0
  42. package/dist/app/reference-facet-bridge.js +22 -0
  43. package/dist/app/return-to.d.ts +31 -0
  44. package/dist/app/return-to.js +188 -0
  45. package/dist/app/row-actions.d.ts +77 -0
  46. package/dist/app/row-actions.js +416 -0
  47. package/dist/app/screen-access.d.ts +8 -0
  48. package/dist/app/screen-access.js +48 -0
  49. package/dist/app/secret-mint-body.d.ts +10 -0
  50. package/dist/app/secret-mint-body.js +140 -0
  51. package/dist/app/secrets-edit-body.d.ts +8 -0
  52. package/dist/app/secrets-edit-body.js +76 -0
  53. package/dist/app/use-embedded-screen.d.ts +6 -0
  54. package/dist/app/use-embedded-screen.js +19 -0
  55. package/dist/app/variable-chips.d.ts +7 -0
  56. package/dist/app/variable-chips.js +16 -0
  57. package/dist/app/write-failed-error.d.ts +6 -0
  58. package/dist/app/write-failed-error.js +21 -0
  59. package/dist/components/embedded-list-field.d.ts +10 -0
  60. package/dist/components/embedded-list-field.js +259 -0
  61. package/dist/components/grid-cell-for-field.d.ts +25 -0
  62. package/dist/components/grid-cell-for-field.js +31 -0
  63. package/dist/components/multi-select-checkboxes.d.ts +14 -0
  64. package/dist/components/multi-select-checkboxes.js +36 -0
  65. package/dist/components/reference-create-dialog.d.ts +13 -0
  66. package/dist/components/reference-create-dialog.js +46 -0
  67. package/dist/components/related-list-section.d.ts +25 -0
  68. package/dist/components/related-list-section.js +273 -0
  69. package/dist/components/render-edit-action-button.d.ts +22 -0
  70. package/dist/components/render-edit-action-button.js +43 -0
  71. package/dist/components/render-edit-logic.d.ts +6 -0
  72. package/dist/components/render-edit-logic.js +63 -0
  73. package/dist/components/render-edit-types.d.ts +209 -0
  74. package/dist/components/render-edit-types.js +1 -0
  75. package/dist/components/render-edit.d.ts +6 -0
  76. package/dist/components/render-edit.js +1169 -0
  77. package/dist/components/render-field.d.ts +38 -0
  78. package/dist/components/render-field.js +544 -0
  79. package/dist/components/render-list.d.ts +117 -0
  80. package/dist/components/render-list.js +232 -0
  81. package/dist/components/write-form-section.d.ts +17 -0
  82. package/dist/components/write-form-section.js +104 -0
  83. package/dist/context/dispatcher-context.d.ts +10 -0
  84. package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
  85. package/dist/context/draft-storage-context.d.ts +12 -0
  86. package/dist/context/draft-storage-context.js +18 -0
  87. package/dist/context/user-roles-context.d.ts +7 -0
  88. package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
  89. package/dist/format-when.d.ts +1 -0
  90. package/{src/format-when.ts → dist/format-when.js} +7 -7
  91. package/dist/hooks/reference-limits.d.ts +3 -0
  92. package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
  93. package/dist/hooks/use-ai-text.d.ts +52 -0
  94. package/dist/hooks/use-ai-text.js +90 -0
  95. package/dist/hooks/use-disclosure.d.ts +7 -0
  96. package/dist/hooks/use-disclosure.js +8 -0
  97. package/dist/hooks/use-form.d.ts +7 -0
  98. package/dist/hooks/use-form.js +36 -0
  99. package/dist/hooks/use-list-url-state.d.ts +43 -0
  100. package/dist/hooks/use-list-url-state.js +131 -0
  101. package/dist/hooks/use-mutation.d.ts +9 -0
  102. package/dist/hooks/use-mutation.js +37 -0
  103. package/dist/hooks/use-query.d.ts +14 -0
  104. package/dist/hooks/use-query.js +118 -0
  105. package/dist/hooks/use-reference-lookup.d.ts +14 -0
  106. package/dist/hooks/use-reference-lookup.js +63 -0
  107. package/dist/hooks/use-store.d.ts +3 -0
  108. package/dist/hooks/use-store.js +38 -0
  109. package/dist/hooks/use-stream-handler.d.ts +14 -0
  110. package/dist/hooks/use-stream-handler.js +93 -0
  111. package/dist/i18n-defaults.d.ts +2 -0
  112. package/dist/i18n-defaults.js +201 -0
  113. package/dist/i18n.d.ts +64 -0
  114. package/dist/i18n.js +166 -0
  115. package/dist/index.d.ts +77 -0
  116. package/dist/index.js +55 -0
  117. package/dist/inside-drawer.d.ts +2 -0
  118. package/dist/inside-drawer.js +9 -0
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/dist/page-header-slot.js +9 -0
  121. package/dist/primitives.d.ts +1351 -0
  122. package/dist/primitives.js +73 -0
  123. package/dist/screen-fills-height.d.ts +3 -0
  124. package/dist/screen-fills-height.js +3 -0
  125. package/dist/share-links.d.ts +4 -0
  126. package/dist/share-links.js +11 -0
  127. package/dist/sort-by-accessor.d.ts +5 -0
  128. package/dist/sort-by-accessor.js +16 -0
  129. package/dist/sse/live-events.d.ts +23 -0
  130. package/dist/sse/live-events.js +28 -0
  131. package/dist/tokens.d.ts +83 -0
  132. package/dist/tokens.js +80 -0
  133. package/package.json +16 -9
  134. package/src/changes.json +52 -0
  135. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  136. package/src/__tests__/format-when.test.ts +0 -12
  137. package/src/__tests__/i18n.test.tsx +0 -283
  138. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  139. package/src/__tests__/qn.test.ts +0 -83
  140. package/src/__tests__/share-links.test.ts +0 -26
  141. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  142. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  143. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  144. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  145. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  146. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  147. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  148. package/src/app/__tests__/content-editors.test.tsx +0 -128
  149. package/src/app/__tests__/content-preview.test.tsx +0 -184
  150. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  151. package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
  152. package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
  153. package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
  154. package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
  155. package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
  156. package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
  157. package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -111
  158. package/src/app/__tests__/feature-schema.test.ts +0 -40
  159. package/src/app/__tests__/form-schema.test.ts +0 -247
  160. package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
  161. package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
  162. package/src/app/__tests__/nav.test.ts +0 -198
  163. package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
  164. package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
  165. package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
  166. package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
  167. package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
  168. package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
  169. package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
  170. package/src/app/__tests__/qualified-names.test.ts +0 -37
  171. package/src/app/__tests__/return-to.test.ts +0 -396
  172. package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
  173. package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
  174. package/src/app/__tests__/screen-access-allows.test.ts +0 -34
  175. package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
  176. package/src/app/__tests__/variable-chips.test.tsx +0 -70
  177. package/src/app/app-features-context.tsx +0 -25
  178. package/src/app/column-renderers.tsx +0 -68
  179. package/src/app/content-editors.tsx +0 -85
  180. package/src/app/content-preview.tsx +0 -67
  181. package/src/app/custom-screens.tsx +0 -29
  182. package/src/app/dashboard-body.tsx +0 -33
  183. package/src/app/extension-form-submit.tsx +0 -123
  184. package/src/app/extension-sections.tsx +0 -111
  185. package/src/app/form-schema.ts +0 -91
  186. package/src/app/kumiko-screen.tsx +0 -3614
  187. package/src/app/layout-fields.ts +0 -31
  188. package/src/app/list-facets.ts +0 -259
  189. package/src/app/nav.tsx +0 -258
  190. package/src/app/projection-detail-shim.ts +0 -85
  191. package/src/app/reference-facet-bridge.tsx +0 -58
  192. package/src/app/return-to.tsx +0 -234
  193. package/src/app/row-actions.ts +0 -639
  194. package/src/app/screen-access.ts +0 -63
  195. package/src/app/secret-mint-body.tsx +0 -259
  196. package/src/app/secrets-edit-body.tsx +0 -194
  197. package/src/app/use-embedded-screen.ts +0 -27
  198. package/src/app/variable-chips.tsx +0 -39
  199. package/src/app/write-failed-error.ts +0 -27
  200. package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
  201. package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
  202. package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
  203. package/src/components/__tests__/related-list-section.test.tsx +0 -1669
  204. package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
  205. package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
  206. package/src/components/__tests__/render-edit-logic.test.ts +0 -314
  207. package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
  208. package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
  209. package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
  210. package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
  211. package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
  212. package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
  213. package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
  214. package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
  215. package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
  216. package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
  217. package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
  218. package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
  219. package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
  220. package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
  221. package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
  222. package/src/components/__tests__/write-form-section.test.tsx +0 -375
  223. package/src/components/embedded-list-field.tsx +0 -354
  224. package/src/components/grid-cell-for-field.tsx +0 -60
  225. package/src/components/multi-select-checkboxes.tsx +0 -93
  226. package/src/components/reference-create-dialog.tsx +0 -91
  227. package/src/components/related-list-section.tsx +0 -467
  228. package/src/components/render-edit-action-button.tsx +0 -106
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -216
  231. package/src/components/render-edit.tsx +0 -1700
  232. package/src/components/render-field.tsx +0 -942
  233. package/src/components/render-list.tsx +0 -561
  234. package/src/components/write-form-section.tsx +0 -199
  235. package/src/context/draft-storage-context.tsx +0 -39
  236. package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
  237. package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
  238. package/src/hooks/__tests__/use-form.test.tsx +0 -65
  239. package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
  240. package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
  241. package/src/hooks/use-ai-text.ts +0 -172
  242. package/src/hooks/use-disclosure.ts +0 -20
  243. package/src/hooks/use-form.ts +0 -78
  244. package/src/hooks/use-list-url-state.ts +0 -171
  245. package/src/hooks/use-mutation.ts +0 -61
  246. package/src/hooks/use-query.ts +0 -132
  247. package/src/hooks/use-reference-lookup.ts +0 -76
  248. package/src/hooks/use-store.ts +0 -48
  249. package/src/hooks/use-stream-handler.ts +0 -134
  250. package/src/i18n-defaults.ts +0 -220
  251. package/src/i18n.tsx +0 -286
  252. package/src/index.ts +0 -272
  253. package/src/primitives.tsx +0 -1446
  254. package/src/share-links.ts +0 -12
  255. package/src/sort-by-accessor.ts +0 -20
  256. package/src/sse/live-events.tsx +0 -56
  257. package/src/tokens.tsx +0 -170
@@ -0,0 +1,38 @@
1
+ import { type EditFieldViewModel, type FieldIssue } from "@cosmicdrift/kumiko-headless";
2
+ import { type ReactNode } from "react";
3
+ export type RenderFieldProps = {
4
+ readonly field: EditFieldViewModel;
5
+ readonly issues?: readonly FieldIssue[];
6
+ readonly onChange: (val: unknown) => void;
7
+ /** Nur bei type:"reference" relevant — Feature-Name für die Lookup-
8
+ * Query-QN (`<feature>:query:<refEntity>:list`). Andere Field-Types
9
+ * ignorieren das Prop. */
10
+ readonly featureName?: string;
11
+ /** Optionaler Zusatz-Inhalt der nach dem Label gerendert wird (z.B.
12
+ * ConfigSourceBadge). */
13
+ readonly labelAppendix?: ReactNode;
14
+ /** Optionaler Zusatz-Inhalt der nach dem Input gerendert wird (z.B.
15
+ * ConfigCascade). */
16
+ readonly fieldAppendix?: ReactNode;
17
+ /** Flat issues-by-path map (FormSnapshot.errors) — only relevant for
18
+ * type:"embedded" with embeddedListCells, to bucket row-/cell-issues
19
+ * (`${field}.${rowIndex}` / `${field}.${rowIndex}.${cellField}`).
20
+ * Other field types ignore this prop. */
21
+ readonly allIssues?: Readonly<Record<string, readonly FieldIssue[]>>;
22
+ /** "form" (default) renders every field as its editable Input, disabled
23
+ * when `field.readOnly` — unchanged behavior. "text" renders a
24
+ * `field.readOnly` field as plain text instead (projectionDetail's read
25
+ * view, fw#2245); editable fields are untouched by this prop either way. */
26
+ readonly valueDisplay?: "form" | "text";
27
+ /** Current form values, keyed by field name. Consulted when
28
+ * `field.renderer` resolves to a `{ react: { __component } }` registry
29
+ * component (passed through as `ColumnRendererProps.row`, same contract
30
+ * as list-column renderers, fw#2245), and to resolve a `type: "number"`
31
+ * field's sibling-field `unit`. Omitted → falls back to a single-key
32
+ * `{ [field.field]: field.value }` row. */
33
+ readonly row?: Readonly<Record<string, unknown>>;
34
+ /** Marks the field as modified against the loaded record (edit mode only). */
35
+ readonly changed?: boolean;
36
+ };
37
+ export declare const AllFieldsRequiredProvider: import("react").Provider<boolean>;
38
+ export declare function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, }: RenderFieldProps): ReactNode;
@@ -0,0 +1,544 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isFormatSpec, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
3
+ import { applyFormatSpec, currencyDecimals, } from "@cosmicdrift/kumiko-headless";
4
+ import { createContext, useCallback, useContext, useMemo, useState } from "react";
5
+ import { useAppFeatures } from "../app/app-features-context.js";
6
+ import { useColumnRenderer } from "../app/column-renderers.js";
7
+ import { extensionSectionName } from "../app/extension-sections.js";
8
+ import { toKebab } from "../app/qn.js";
9
+ import { screenAccessAllows } from "../app/screen-access.js";
10
+ import { useUserRoles } from "../context/user-roles-context.js";
11
+ import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
12
+ import { useQuery } from "../hooks/use-query.js";
13
+ import { referenceLookupSource } from "../hooks/use-reference-lookup.js";
14
+ import { useLocale, useTranslation } from "../i18n.js";
15
+ import { usePrimitives } from "../primitives.js";
16
+ import { EmbeddedListField } from "./embedded-list-field.js";
17
+ import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
18
+ import { ReferenceCreateDialog } from "./reference-create-dialog.js";
19
+ const AllFieldsRequiredContext = createContext(false);
20
+ export const AllFieldsRequiredProvider = AllFieldsRequiredContext.Provider;
21
+ export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay = "form", row, changed, }) {
22
+ const { Field, Input, Banner, Text, JsonView } = usePrimitives();
23
+ // App-Locale (i18n) für money/date-Inputs — sonst fielen sie auf
24
+ // navigator.language (Browser-Sprache) zurück statt der gewählten
25
+ // App-Sprache. BEWUSSTE API-Verschärfung (seit 0.38): RenderField ist
26
+ // public exportiert und verlangt jetzt einen LocaleProvider —
27
+ // Standalone-Consumer/Tests müssen wrappen (createKumikoApp tut es).
28
+ const appLocale = useLocale().locale();
29
+ const t = useTranslation();
30
+ const allFieldsRequired = useContext(AllFieldsRequiredContext);
31
+ if (!field.visible)
32
+ return null;
33
+ const id = inputId(field);
34
+ const hasError = issues !== undefined && issues.length > 0;
35
+ // An author-declared renderer always wins over the input widget, same as
36
+ // the list-column path (render-list.tsx). Only takes effect once the field
37
+ // is actually readOnly — a FormatSpec/PlatformComponent renderer has no
38
+ // editable widget of its own, so applying it to an editable field would
39
+ // silently make that field un-editable (fw#2245).
40
+ const readOnlyText = valueDisplay === "text" && field.readOnly && field.renderer === undefined;
41
+ // Reference-Field rendert eine eigene Component — sie nutzt
42
+ // useQuery() für den Live-Lookup, also muss sie als React-
43
+ // Komponente gemountet werden (nicht als pure render-Call).
44
+ const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale) })) : (renderInput({
45
+ field,
46
+ id,
47
+ hasError,
48
+ onChange,
49
+ Input,
50
+ appLocale,
51
+ Banner,
52
+ Text,
53
+ JsonView,
54
+ t,
55
+ row,
56
+ }));
57
+ return (_jsx(Field, { id: id, label: field.label, required: field.required && !allFieldsRequired, ...(issues !== undefined && { issues }), ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(changed === true && { changed }), testId: `field-${field.field}`, children: control }));
58
+ }
59
+ // Where a reference field's option rows come from, and which of their keys
60
+ // holds the display text — the entity's own list handler plus `refLabelField`
61
+ // by default, or an author-supplied handler whose rows carry `label`
62
+ // (fw#2780), for targets with no readable column of their own.
63
+ function referenceOptionSource(field, refFeature, refEntity) {
64
+ if (field.refOptionsQuery !== undefined) {
65
+ return { queryQn: field.refOptionsQuery, labelKey: "label" };
66
+ }
67
+ return referenceLookupSource(refFeature, refEntity, field.refLabelField ?? "id");
68
+ }
69
+ // Tier 2.7e-3 + 2.1c: Reference-Input rendert eine Searchable Combobox
70
+ // gefüllt aus einer Live-Query auf die referenced Entity. Default-
71
+ // Limit: 200 — bei größeren Datasets fehlt der Tail im Dropdown
72
+ // (Tier 2.7e-Remote: server-side Search-Query mit debounce kommt später).
73
+ // - Display = row[refLabelField], Default labelField "id".
74
+ // - Loading-State: leeres Dropdown bis die rows da sind. Field
75
+ // ist disabled während useQuery läuft.
76
+ // - Multi-Mode (Tier 2.7e-Multi via field.refMultiple): value ist
77
+ // ein UUID-Array, Combobox rendert Selected-Tags.
78
+ //
79
+ // Storage: UI-Wert ist UUID (row.id) oder UUID-Array bei multiple.
80
+ // Server-Schema: z.uuid() bzw. z.array(z.uuid()).
81
+ // REFERENCE_COMBOBOX_LIMIT lebt zentral in hooks/reference-limits.ts
82
+ // (siehe dort für Begründung der Default-Werte).
83
+ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, }) {
84
+ const { Banner } = usePrimitives();
85
+ const refEntity = field.refEntity ?? "";
86
+ const refFeature = field.refFeature ?? featureName;
87
+ const isMultiple = field.refMultiple === true;
88
+ // Tier 2.7e Cross-Feature: refFeature kann ≠ featureName sein
89
+ // (z.B. items.assignee → users:query:user:list). Default ist
90
+ // same-feature, kommt aus dem ViewModel (parseRefTarget).
91
+ // fw#2780: refOptionsQuery replaces that source with an author-supplied
92
+ // handler whose rows already carry a composed `label`.
93
+ const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
94
+ // Issue #1681: "+ Neu" in der Combobox öffnet den Create-Screen der
95
+ // referenced entity als Dialog, statt die aktuelle Form zu verlassen.
96
+ // refFeature kann ein anderes Feature als das aktuell gerenderte sein
97
+ // — appFeatures (createKumikoApp) kennt alle Feature-Schemas, nicht
98
+ // nur das der aktiven Screen. Kein Match (Feature/Screen/Entity nicht
99
+ // registriert, oder allowCreate:false) → onCreate bleibt undefined,
100
+ // Combobox rendert dann ohne den Footer.
101
+ const appFeatures = useAppFeatures();
102
+ const userRoles = useUserRoles();
103
+ const t = useTranslation();
104
+ const [createOpen, setCreateOpen] = useState(false);
105
+ const refTargetSchema = appFeatures.find((f) => f.featureName === refFeature);
106
+ const refCreateScreen = refTargetSchema?.screens.find((s) => s.type === "entityEdit" &&
107
+ s.entity === refEntity &&
108
+ s.allowCreate !== false &&
109
+ screenAccessAllows(s.access, userRoles));
110
+ const refEntityDef = refTargetSchema?.entities[refEntity];
111
+ // Tier 2.7e Remote-Search: User tippt im Combobox → Server filtert
112
+ // via existing list-payload `search`-Param (Tier 2.6c). Combobox
113
+ // debounced den keystroke selbst (300ms) und ruft onSearchChange.
114
+ // Initial-State leer → erste 50 Items vom Server (default-sortiert).
115
+ const [searchTerm, setSearchTerm] = useState("");
116
+ const queryPayload = useMemo(() => searchTerm === ""
117
+ ? { limit: REFERENCE_COMBOBOX_LIMIT }
118
+ : { limit: REFERENCE_COMBOBOX_LIMIT, search: searchTerm }, [searchTerm]);
119
+ const queryResult = useQuery(queryQn, queryPayload);
120
+ const handleSearchChange = useCallback((q) => setSearchTerm(q), []);
121
+ const canCreate = !field.readOnly && refCreateScreen !== undefined && refEntityDef !== undefined;
122
+ const [createdWithoutIdWarning, setCreatedWithoutIdWarning] = useState(false);
123
+ const handleCreated = useCallback((newId) => {
124
+ setCreateOpen(false);
125
+ // Clear the search filter before refetching — the just-created record
126
+ // won't match whatever the user had typed pre-create, and would then
127
+ // be missing from `options` (shown as a raw id instead of a label).
128
+ setSearchTerm("");
129
+ queryResult.refetch().catch((err) => {
130
+ // biome-ignore lint/suspicious/noConsole: no error-surfacing path exists in this widget yet — at minimum, don't swallow it silently.
131
+ console.error("render-field: refetch after create failed", err);
132
+ });
133
+ if (newId === undefined) {
134
+ // Record was created server-side but the payload carried no id —
135
+ // can't auto-select it, surface that instead of failing silently.
136
+ setCreatedWithoutIdWarning(true);
137
+ return;
138
+ }
139
+ setCreatedWithoutIdWarning(false);
140
+ if (isMultiple) {
141
+ const current = Array.isArray(field.value) ? field.value : [];
142
+ onChange([...current, newId]);
143
+ }
144
+ else {
145
+ onChange(newId);
146
+ }
147
+ }, [isMultiple, field.value, onChange, queryResult.refetch]);
148
+ const options = useMemo(() => {
149
+ const rows = queryResult.data?.rows ?? [];
150
+ return rows.map((row) => {
151
+ const idVal = String(row["id"] ?? "");
152
+ const label = String(row[labelKey] ?? idVal);
153
+ return { value: idVal, label };
154
+ });
155
+ }, [queryResult.data, labelKey]);
156
+ // Single: value ist String/null; Multi: Array. Coerce auf das was
157
+ // der Combobox-Mode erwartet, damit Storage-Drift (Server liefert
158
+ // alten String wo jetzt Array erwartet wird) keine Crash auslöst.
159
+ const baseInputProps = {
160
+ id,
161
+ name: field.field,
162
+ // Initial-Load disabled — danach loading-Indicator im Popover.
163
+ disabled: field.readOnly || (queryResult.loading && options.length === 0),
164
+ required: field.required,
165
+ hasError,
166
+ options,
167
+ onSearchChange: handleSearchChange,
168
+ loading: queryResult.loading,
169
+ ...(canCreate && {
170
+ onCreate: () => setCreateOpen(true),
171
+ createLabel: t("kumiko.actions.create"),
172
+ }),
173
+ };
174
+ const createDialog = (_jsxs(_Fragment, { children: [canCreate && refCreateScreen && refEntityDef && (_jsx(ReferenceCreateDialog, { open: createOpen, onClose: () => setCreateOpen(false), onCreated: handleCreated, featureName: refFeature, screen: refCreateScreen, entity: refEntityDef })), createdWithoutIdWarning && (_jsx(Banner, { variant: "error", children: t("kumiko.field.reference-created-no-id") }))] }));
175
+ if (isMultiple) {
176
+ const arrayValue = Array.isArray(field.value)
177
+ ? field.value
178
+ : [];
179
+ return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, multiple: true, value: arrayValue, onChange: (v) => onChange(v) }), createDialog] }));
180
+ }
181
+ const stringValue = field.value === undefined || field.value === null ? "" : String(field.value);
182
+ return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, value: stringValue, onChange: (v) => onChange(v === "" ? null : v) }), createDialog] }));
183
+ }
184
+ function inputId(field) {
185
+ return `kumiko-edit-${field.field}`;
186
+ }
187
+ // Doesn't resolve the `string` (cross-feature QN) FieldRenderer variant — the
188
+ // list-column path (DataTableCell) doesn't either, so this stays in parity.
189
+ function FieldRendererOutput({ field, renderer, row, appLocale, }) {
190
+ const { Text, JsonView } = usePrimitives();
191
+ const t = useTranslation();
192
+ const componentName = !isFormatSpec(renderer) && typeof renderer === "object" && renderer !== null
193
+ ? extensionSectionName(renderer)
194
+ : undefined;
195
+ const Component = useColumnRenderer(componentName);
196
+ if (isFormatSpec(renderer)) {
197
+ // format:"json" wants the raw value (JsonView stringifies + highlights
198
+ // itself) — applyFormatSpec's already-indented string is only the
199
+ // fallback for primitives-providers without JsonView (fw#2312).
200
+ if (renderer.format === "json" && JsonView !== undefined) {
201
+ return (_jsx(JsonView, { value: field.value, indent: renderer.indent, testId: `field-value-${field.field}` }));
202
+ }
203
+ // App locale as default when the FormatSpec declares none of its own —
204
+ // otherwise locale-sensitive formats (timestamp/date/number/decimal/
205
+ // bigInt/unit) fell back to Intl's runtime default instead of the app
206
+ // language chosen via LocaleProvider (fw#2187). Prefer renderer.locale
207
+ // when set; coalesce undefined (spread override) back to appLocale (#2332).
208
+ return (_jsx(Text, { testId: `field-value-${field.field}`, children: applyFormatSpec({
209
+ ...renderer,
210
+ locale: renderer.locale ?? appLocale,
211
+ }, field.value, t) }));
212
+ }
213
+ if (componentName !== undefined) {
214
+ if (Component !== undefined) {
215
+ return (_jsx(Component, { value: field.value, row: row ?? { [field.field]: field.value }, column: { field: field.field } }));
216
+ }
217
+ // biome-ignore lint/suspicious/noConsole: dev-warning for a registry mismatch, mirrors DataTableCell's columnRenderer warning.
218
+ console.warn(`[kumiko] fieldRenderer "${componentName}" not registered`);
219
+ }
220
+ return _jsx(Text, { testId: `field-value-${field.field}`, children: stringValue(field.value) });
221
+ }
222
+ // Read-only text for `type: "reference"` — resolves the referenced row's
223
+ // label via the same lookup query as ReferenceInput's combobox, but shows
224
+ // plain text instead of mounting an (unusable, disabled) combobox.
225
+ function ReadOnlyReferenceValue({ field, featureName, }) {
226
+ const { Text } = usePrimitives();
227
+ const t = useTranslation();
228
+ const refEntity = field.refEntity ?? "";
229
+ const refFeature = field.refFeature ?? featureName;
230
+ const isMultiple = field.refMultiple === true;
231
+ const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
232
+ const queryResult = useQuery(queryQn, {
233
+ limit: REFERENCE_COMBOBOX_LIMIT,
234
+ });
235
+ const ids = isMultiple
236
+ ? Array.isArray(field.value)
237
+ ? field.value
238
+ : []
239
+ : typeof field.value === "string" && field.value !== ""
240
+ ? [field.value]
241
+ : [];
242
+ if (ids.length === 0)
243
+ return _jsx(Text, { testId: `field-value-${field.field}`, children: "\u2014" });
244
+ const rows = queryResult.data?.rows ?? [];
245
+ // System-scope ids (e.g. SYSTEM_TENANT_ID) never have a backing row —
246
+ // same central fallback as useReferenceLookup (render-list.tsx), so a
247
+ // reference field showing one on the detail path also gets a label
248
+ // instead of the raw id.
249
+ const systemLabel = SYSTEM_REFERENCE_LABELS[`${refFeature}:${refEntity}`];
250
+ const labels = ids.map((id) => {
251
+ if (systemLabel !== undefined && id === systemLabel.id)
252
+ return t(systemLabel.labelKey);
253
+ const row = rows.find((r) => String(r["id"] ?? "") === id);
254
+ return row !== undefined ? String(row[labelKey] ?? id) : id;
255
+ });
256
+ return _jsx(Text, { testId: `field-value-${field.field}`, children: labels.join(", ") });
257
+ }
258
+ // Field types whose read display already isn't a boxed "disabled input" look
259
+ // (embedded/jsonb render as an info Banner, files/images as an unsupported-
260
+ // type Banner) — `readOnlyDisplayText` doesn't cover these, they keep going
261
+ // through `renderInput`'s existing Banner branch in text-display mode too.
262
+ function isComplexFieldType(type) {
263
+ return type === "embedded" || type === "jsonb" || type === "files" || type === "images";
264
+ }
265
+ // Mirrors ACTION_ICON_BY_ID (kumiko-screen.tsx) for fields: only names with
266
+ // an unambiguous matching FieldIconKey are listed here, everything else
267
+ // stays iconless rather than guessing.
268
+ const FIELD_ICON_BY_NAME = {
269
+ email: "mail",
270
+ phone: "phone",
271
+ tel: "phone",
272
+ mobile: "phone",
273
+ url: "link",
274
+ website: "link",
275
+ link: "link",
276
+ password: "lock",
277
+ search: "search",
278
+ city: "map-pin",
279
+ address: "map-pin",
280
+ street: "map-pin",
281
+ user: "user",
282
+ owner: "user",
283
+ assignee: "user",
284
+ };
285
+ function kebabLastSegment(value) {
286
+ const kebab = toKebab(value);
287
+ const lastDash = kebab.lastIndexOf("-");
288
+ return lastDash === -1 ? kebab : kebab.slice(lastDash + 1);
289
+ }
290
+ // Derives a prefix icon when the field declares none: field.icon wins, then
291
+ // the field name. Deliberately no type-based fallback — a hash in front of a
292
+ // number field restates what the field already shows and reads as noise.
293
+ function resolveFieldIcon(field) {
294
+ if (field.icon !== undefined)
295
+ return field.icon;
296
+ const byName = FIELD_ICON_BY_NAME[field.field.toLowerCase()] ??
297
+ FIELD_ICON_BY_NAME[kebabLastSegment(field.field)];
298
+ return byName;
299
+ }
300
+ // Dispatches field.type → Input-kind. Select threads options through
301
+ // from the EditFieldViewModel (computeEditViewModel pulls them from
302
+ // SelectFieldDef.options). Structural types without a widget (embedded,
303
+ // jsonb, multiSelect) render read-only instead of a data-destroying text
304
+ // fallback (#1834); truly unknown scalar types still fall back to text.
305
+ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner, Text, JsonView, t, row, }) {
306
+ const common = {
307
+ id,
308
+ name: field.field,
309
+ disabled: field.readOnly,
310
+ required: field.required,
311
+ hasError,
312
+ };
313
+ switch (field.type) {
314
+ // decimal/bigInt are both plain numbers on the wire (fieldToZod:
315
+ // z.number() / z.number().int().safe()) — the number input's onChange
316
+ // already emits `number | undefined`, so no extra coercion is needed
317
+ // beyond what "number" already does (#1925).
318
+ case "number":
319
+ case "bigInt": {
320
+ const icon = resolveFieldIcon(field);
321
+ const unit = resolveNumberUnit(field.unit, row);
322
+ return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), ...(icon !== undefined && { icon }), ...(unit !== undefined && { unit }) }));
323
+ }
324
+ case "decimal": {
325
+ // step="any" disables the native stepMismatch constraint — without it
326
+ // <input type="number"> defaults to step=1 and blocks form submit on
327
+ // any fractional value via silent browser-native validation.
328
+ const icon = resolveFieldIcon(field);
329
+ return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), step: "any", ...(icon !== undefined && { icon }) }));
330
+ }
331
+ case "tz":
332
+ return (_jsx(Input, { kind: "tz", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
333
+ case "multiSelect": {
334
+ const rawOptions = field.options ?? [];
335
+ const labels = field.optionLabels;
336
+ const multiSelectOptions = labels !== undefined
337
+ ? rawOptions.map((value) => ({ value, label: labels[value] ?? value }))
338
+ : rawOptions.map((value) => ({ value, label: value }));
339
+ const arrayValue = Array.isArray(field.value) ? field.value : [];
340
+ if (field.display === "checkboxes") {
341
+ return (_jsx(MultiSelectCheckboxes, { field: field, id: id, options: multiSelectOptions, value: arrayValue, onChange: (v) => onChange(v) }));
342
+ }
343
+ return (_jsx(Input, { kind: "combobox", ...common, multiple: true, value: arrayValue, onChange: (v) => onChange(v), options: multiSelectOptions }));
344
+ }
345
+ case "money": {
346
+ const currency = resolveMoneyCurrency(field.value, field.currency);
347
+ return (_jsx(Input, { kind: "money", ...common, value: moneyMinorValue(field.value, currency), onChange: (v) => onChange(moneyPayload(v, currency)), currency: currency, locale: appLocale }));
348
+ }
349
+ case "boolean":
350
+ return (_jsx(Input, { kind: "boolean", ...common, value: field.value === true, onChange: (v) => onChange(v) }));
351
+ case "date":
352
+ return (_jsx(Input, { kind: "date", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), locale: field.dateLocale ?? appLocale, ...(field.min !== undefined && { min: field.min }), ...(field.max !== undefined && { max: field.max }) }));
353
+ case "timestamp":
354
+ return (_jsx(Input, { kind: "timestamp", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), locale: field.dateLocale ?? appLocale, ...(field.wallClock !== undefined && { wallClock: field.wallClock }), ...(field.min !== undefined && { min: field.min }), ...(field.max !== undefined && { max: field.max }) }));
355
+ case "locatedTimestamp":
356
+ 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 }) }));
357
+ case "select": {
358
+ // Translated Option-Labels kommen aus dem ViewModel-Builder
359
+ // (computeEditViewModel, Convention-Key
360
+ // `<feature>:entity:<entity>:field:<field>:option:<value>`).
361
+ // Wenn keine Translations registriert sind, fallback auf raw
362
+ // value als Label — der ComboboxInput zeigt dann unverändert.
363
+ const rawOptions = field.options ?? [];
364
+ const labels = field.optionLabels;
365
+ const selectOptions = labels !== undefined
366
+ ? rawOptions.map((value) => ({ value, label: labels[value] ?? value }))
367
+ : rawOptions;
368
+ // `display` is shared with multiSelect's "checkboxes", which is not a
369
+ // select presentation — narrow to the two a select field can request.
370
+ const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
371
+ return (_jsx(Input, { kind: "select", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), options: selectOptions, ...(display !== undefined && { display }) }));
372
+ }
373
+ case "file":
374
+ case "image": {
375
+ const kind = field.type === "image" ? "image" : "file";
376
+ const fileId = typeof field.value === "string" && field.value !== "" ? field.value : null;
377
+ return (_jsx(Input, { kind: kind, ...common, value: fileId, onChange: (v) => onChange(v), ...(field.accept !== undefined && { accept: field.accept }), ...(field.maxSize !== undefined && { maxSize: field.maxSize }), ...(field.entityType !== undefined && { entityType: field.entityType }), ...(field.fieldName !== undefined && { fieldName: field.fieldName }), ...(field.imageVariant !== undefined && { imageVariant: field.imageVariant }), ...(field.capture !== undefined && { capture: field.capture }) }));
378
+ }
379
+ // embedded (without embeddedListCells — that's embeddedList, which has
380
+ // had its own EmbeddedListField widget since #1838) and jsonb carry
381
+ // arbitrary objects; files/images carry a FileRef-UUID array and have
382
+ // no multi-upload widget yet (deliberately deferred, #1925). Without a
383
+ // dedicated widget these must NOT fall through to a text input:
384
+ // stringValue() turns them into "[object Object]" / a comma-joined
385
+ // string, and saving that overwrites the real data with the mangled
386
+ // string (#1834). A `required: true` on any of these is caught loudly
387
+ // at boot (validateNoWidgetRequiredField in the framework package)
388
+ // instead of silently failing here.
389
+ case "embedded":
390
+ case "jsonb":
391
+ case "files":
392
+ case "images": {
393
+ const hasValue = field.value !== undefined && field.value !== null && field.value !== "";
394
+ return (_jsxs(Banner, { id: id, variant: "info", children: [t("kumiko.field.unsupported"), hasValue &&
395
+ (JsonView !== undefined ? (_jsx(JsonView, { value: field.value, testId: `field-value-${field.field}` })) : (_jsx(Text, { variant: "code", children: JSON.stringify(field.value) })))] }));
396
+ }
397
+ default: {
398
+ if (field.type === "text" && field.format === "password") {
399
+ return (_jsx(Input, { kind: "password", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
400
+ }
401
+ // text + unknown scalar type → text input. If TextFieldDef.multiline
402
+ // is set (the view-model carries it), the renderer switches to
403
+ // textarea. longText always renders a textarea — that's the point of
404
+ // the type — regardless of whether `multiline` is set; `multiline`
405
+ // only supplies an optional `{ rows }` override for it (#1925).
406
+ if (field.type === "longText" || (field.type === "text" && field.multiline)) {
407
+ const rows = typeof field.multiline === "object" ? field.multiline.rows : undefined;
408
+ return (_jsx(Input, { kind: "textarea", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(rows !== undefined && { rows }) }));
409
+ }
410
+ const icon = resolveFieldIcon(field);
411
+ return (_jsx(Input, { kind: "text", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(icon !== undefined && { icon }) }));
412
+ }
413
+ }
414
+ }
415
+ function stringValue(v) {
416
+ if (v === undefined || v === null)
417
+ return "";
418
+ return typeof v === "string" ? v : String(v);
419
+ }
420
+ function numberValue(v) {
421
+ if (v === undefined || v === null || v === "")
422
+ return "";
423
+ return typeof v === "number" ? v : Number(v);
424
+ }
425
+ // Read/initial value → MoneyInput's minor-units contract. The server's
426
+ // read shape is `{amount, currency, amountMinor}` in MAJOR units — deliberately
427
+ // NOT reading `amountMinor` here: the server derives it via a flat
428
+ // MINOR_UNIT_SCALE=100 (money.ts), which disagrees with currencyDecimals for
429
+ // zero-decimal currencies like JPY, so it would double-scale JPY amounts.
430
+ // Deriving from `amount * 10**currencyDecimals(currency)` keeps this the
431
+ // exact inverse of moneyPayload below. A bare number (e.g. ConfigEditBody's
432
+ // stored-config coercion) is MAJOR units too — every producer in this repo
433
+ // hands rehydrateMoney's `{amount,…}` shape or a raw major-unit number, never
434
+ // pre-scaled minor units.
435
+ function resolveMoneyCurrency(value, fieldCurrency) {
436
+ if (typeof value === "object" &&
437
+ value !== null &&
438
+ typeof value.currency === "string") {
439
+ return value.currency;
440
+ }
441
+ return fieldCurrency ?? "EUR";
442
+ }
443
+ // Static unit → used as-is. Sibling-field reference → read the live row
444
+ // value; missing/empty/non-string sibling means no suffix, never a guess.
445
+ function resolveNumberUnit(unit, row) {
446
+ if (unit === undefined)
447
+ return undefined;
448
+ if (typeof unit === "string")
449
+ return unit;
450
+ const sibling = row?.[unit.field];
451
+ return typeof sibling === "string" && sibling.length > 0 ? sibling : undefined;
452
+ }
453
+ function moneyMinorValue(v, currency) {
454
+ if (v === undefined || v === null || v === "")
455
+ return "";
456
+ if (typeof v === "number")
457
+ return Math.round(v * 10 ** currencyDecimals(currency));
458
+ if (typeof v === "object") {
459
+ const amount = v.amount;
460
+ if (typeof amount === "number")
461
+ return Math.round(amount * 10 ** currencyDecimals(currency));
462
+ }
463
+ return "";
464
+ }
465
+ // MoneyInput's minor-units onChange → the server payload shape
466
+ // (`z.object({amount, currency})`, schema-builder.ts) — MAJOR units, no
467
+ // `amountMinor` (the server derives that itself on write).
468
+ function moneyPayload(minorUnits, currency) {
469
+ if (minorUnits === undefined)
470
+ return undefined;
471
+ return { amount: minorUnits / 10 ** currencyDecimals(currency), currency };
472
+ }
473
+ // locatedTimestamp-Feldwert: das Read-Wrapper liefert `{ at, tz, utc }`; leer
474
+ // (noch nicht gesetzt) → "" als Empty-Sentinel, analog money/timestamp.
475
+ function locatedValue(v) {
476
+ if (v !== null && typeof v === "object" && "at" in v && "tz" in v) {
477
+ const o = v;
478
+ return {
479
+ at: typeof o.at === "string" ? o.at : "",
480
+ tz: typeof o.tz === "string" ? o.tz : "",
481
+ ...(typeof o.utc === "string" && { utc: o.utc }),
482
+ };
483
+ }
484
+ return "";
485
+ }
486
+ // Read-only text for a `field.readOnly` field without its own `renderer` —
487
+ // per-type formatting that reuses the same value-shaping helpers as the
488
+ // editable widgets above, so a text value and its would-be Input widget stay
489
+ // derived from the identical parse (fw#2245). `type: "reference"` isn't
490
+ // covered here — it needs a live label lookup, see ReadOnlyReferenceValue.
491
+ // `isComplexFieldType` types aren't covered either — callers keep those on
492
+ // `renderInput`'s existing Banner fallback.
493
+ function readOnlyDisplayText(field, appLocale) {
494
+ const { type, value } = field;
495
+ if (value === undefined || value === null || value === "")
496
+ return "—";
497
+ if (type === "text" && field.format === "password")
498
+ return "••••••••";
499
+ switch (type) {
500
+ case "boolean":
501
+ return applyFormatSpec({ format: "boolean" }, value);
502
+ case "date":
503
+ return applyFormatSpec({ format: "date", locale: field.dateLocale ?? appLocale }, value);
504
+ case "timestamp":
505
+ return applyFormatSpec({ format: "timestamp", locale: field.dateLocale ?? appLocale }, value);
506
+ case "locatedTimestamp": {
507
+ const located = locatedValue(value);
508
+ if (located === "" || located.at === "")
509
+ return "—";
510
+ return applyFormatSpec({ format: "timestamp", locale: field.dateLocale ?? appLocale }, located.utc ?? located.at);
511
+ }
512
+ case "number":
513
+ case "bigInt":
514
+ case "decimal": {
515
+ const n = numberValue(value);
516
+ return n === ""
517
+ ? "—"
518
+ : new Intl.NumberFormat(appLocale, { useGrouping: field.grouping ?? true }).format(n);
519
+ }
520
+ case "money": {
521
+ const currency = resolveMoneyCurrency(value, field.currency);
522
+ const minor = moneyMinorValue(value, currency);
523
+ if (minor === "")
524
+ return "—";
525
+ const major = minor / 10 ** currencyDecimals(currency);
526
+ return new Intl.NumberFormat(appLocale, { style: "currency", currency }).format(major);
527
+ }
528
+ case "select":
529
+ case "multiSelect": {
530
+ const labels = field.optionLabels;
531
+ const values = Array.isArray(value) ? value : [value];
532
+ if (values.length === 0)
533
+ return "—";
534
+ return values
535
+ .map((v) => (typeof v === "string" ? (labels?.[v] ?? v) : stringValue(v)))
536
+ .join(", ");
537
+ }
538
+ case "file":
539
+ case "image":
540
+ return typeof value === "string" ? value : "—";
541
+ default:
542
+ return stringValue(value);
543
+ }
544
+ }