@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,76 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useState } from "react";
3
+ import { useDispatcher } from "../context/dispatcher-context.js";
4
+ import { useQuery } from "../hooks/use-query.js";
5
+ import { useTranslation } from "../i18n.js";
6
+ import { usePrimitives } from "../primitives.js";
7
+ import { dispatcherErrorText } from "./write-failed-error.js";
8
+ // secrets:query:list never returns plaintext (only a redacted preview), so
9
+ // unlike ConfigEditBody there is no server value to pre-fill a draft with —
10
+ // every input starts at "" and stays that way unless the user types into it.
11
+ export function SecretsEditBody({ screen, translate }) {
12
+ const { Banner, Form, Section, Field, Input, Button, Text } = usePrimitives();
13
+ const t = useTranslation();
14
+ const effectiveTranslate = translate ?? t;
15
+ const dispatcher = useDispatcher();
16
+ const listQuery = useQuery("secrets:query:list", {});
17
+ const [drafts, setDrafts] = useState({});
18
+ const [submitting, setSubmitting] = useState(false);
19
+ const [submitError, setSubmitError] = useState(null);
20
+ const rowsByQualifiedKey = useMemo(() => {
21
+ const out = new Map();
22
+ for (const row of listQuery.data ?? [])
23
+ out.set(row.key, row);
24
+ return out;
25
+ }, [listQuery.data]);
26
+ const setDraft = useCallback((fieldId, value) => {
27
+ setDrafts((prev) => ({ ...prev, [fieldId]: value }));
28
+ }, []);
29
+ const handleSubmit = useCallback(async () => {
30
+ const commands = Object.entries(screen.secretKeys).flatMap(([fieldId, qualified]) => {
31
+ const value = drafts[fieldId]?.trim();
32
+ return value ? [{ type: "secrets:write:set", payload: { key: qualified, value } }] : [];
33
+ });
34
+ if (commands.length === 0)
35
+ return;
36
+ setSubmitting(true);
37
+ setSubmitError(null);
38
+ const result = await dispatcher.batch(commands);
39
+ setSubmitting(false);
40
+ if (!result.isSuccess) {
41
+ setSubmitError(dispatcherErrorText(result.error, effectiveTranslate));
42
+ return;
43
+ }
44
+ setDrafts({});
45
+ await listQuery.refetch();
46
+ }, [dispatcher, drafts, screen.secretKeys, listQuery.refetch, effectiveTranslate]);
47
+ const handleDelete = useCallback(async (qualified) => {
48
+ const result = await dispatcher.write("secrets:write:delete", { key: qualified });
49
+ if (!result.isSuccess) {
50
+ setSubmitError(dispatcherErrorText(result.error, effectiveTranslate));
51
+ return;
52
+ }
53
+ await listQuery.refetch();
54
+ }, [dispatcher, listQuery.refetch, effectiveTranslate]);
55
+ if (listQuery.loading && listQuery.data === null) {
56
+ return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
57
+ }
58
+ if (listQuery.error) {
59
+ return (_jsx(Banner, { padded: true, variant: "error", testId: "kumiko-screen-error", children: dispatcherErrorText(listQuery.error, effectiveTranslate) }));
60
+ }
61
+ return (_jsxs(Form, { onSubmit: () => {
62
+ void handleSubmit();
63
+ }, testId: "secrets-edit-form", ...(screen.description !== undefined && {
64
+ subtitle: effectiveTranslate(screen.description),
65
+ }), actions: _jsx(Button, { type: "submit", variant: "primary", loading: submitting, disabled: submitting, testId: "secrets-edit-submit", children: effectiveTranslate("kumiko.actions.save") }), children: [submitError !== null && (_jsx(Banner, { variant: "error", testId: "secrets-edit-error", children: submitError })), screen.sections.map((section, index) => (_jsx(Section, { ...(section.title !== undefined && { title: effectiveTranslate(section.title) }), children: section.fields.map((fieldId) => {
66
+ const qualified = screen.secretKeys[fieldId];
67
+ if (qualified === undefined)
68
+ return null;
69
+ const row = rowsByQualifiedKey.get(qualified);
70
+ const hintKey = screen.fieldHints?.[fieldId];
71
+ const isRequired = screen.requiredFields?.includes(fieldId) ?? false;
72
+ return (_jsxs(Field, { id: fieldId, label: effectiveTranslate(screen.fieldLabels[fieldId] ?? fieldId), required: isRequired, testId: `field-${fieldId}`, fieldAppendix: _jsxs(_Fragment, { children: [hintKey !== undefined && (_jsx(Text, { variant: "small", children: effectiveTranslate(hintKey) })), row !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { variant: "small", testId: `secret-preview-${fieldId}`, children: row.redactedPreview ?? effectiveTranslate("config.secrets.set") }), _jsx(Button, { type: "button", variant: "danger-ghost", size: "sm", onClick: () => handleDelete(qualified), testId: `secret-delete-${fieldId}`, children: effectiveTranslate("config.secrets.delete") })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { variant: "small", testId: `secret-not-set-${fieldId}`, children: effectiveTranslate("config.secrets.notSet") }), isRequired && (_jsx(Text, { variant: "small", testId: `required-marker-${fieldId}`, children: effectiveTranslate("config.secrets.required") }))] }))] }), children: [_jsx(Text, { variant: "small", children: effectiveTranslate(row !== undefined
73
+ ? "config.secrets.replacePlaceholder"
74
+ : "config.secrets.placeholder") }), _jsx(Input, { kind: "password", id: fieldId, name: fieldId, value: drafts[fieldId] ?? "", onChange: (v) => setDraft(fieldId, v), autoComplete: "new-password", disabled: submitting, testId: `secret-input-${fieldId}` })] }, fieldId));
75
+ }) }, section.title ?? `section-${index}`)))] }));
76
+ }
@@ -0,0 +1,6 @@
1
+ import type { FeatureSchema } from "./feature-schema.js";
2
+ export type EmbeddedScreenTarget = {
3
+ readonly schema: FeatureSchema;
4
+ readonly qn: string;
5
+ };
6
+ export declare function useEmbeddedScreen(hostFeatureName: string, screen: string): EmbeddedScreenTarget | undefined;
@@ -0,0 +1,19 @@
1
+ import { useUserRoles } from "../context/user-roles-context.js";
2
+ import { useAppFeatures } from "./app-features-context.js";
3
+ import { featureNameFromQualifiedScreenId, qualifyScreenId } from "./qualify-screen-id.js";
4
+ import { screenAccessAllows } from "./screen-access.js";
5
+ // Undefined (not KumikoScreen's access-denied banner) so the host can drop the whole tile.
6
+ export function useEmbeddedScreen(hostFeatureName, screen) {
7
+ const features = useAppFeatures();
8
+ const userRoles = useUserRoles();
9
+ const crossFeatureName = featureNameFromQualifiedScreenId(screen);
10
+ const targetFeatureName = crossFeatureName ?? hostFeatureName;
11
+ const qn = crossFeatureName !== undefined ? screen : qualifyScreenId(hostFeatureName, screen);
12
+ const schema = features.find((f) => f.featureName === targetFeatureName);
13
+ const target = schema?.screens.find((s) => qualifyScreenId(targetFeatureName, s.id) === qn);
14
+ if (schema === undefined || target === undefined)
15
+ return undefined;
16
+ if (!screenAccessAllows(target.access, userRoles))
17
+ return undefined;
18
+ return { schema, qn };
19
+ }
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from "react";
2
+ export type VariableChipsProps = {
3
+ readonly variables: readonly string[];
4
+ readonly onInsert: (name: string) => void;
5
+ readonly disabled?: boolean;
6
+ };
7
+ export declare function VariableChips({ variables, onInsert, disabled }: VariableChipsProps): ReactNode;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Editor-agnostic chip bar for a content editor's declared variables
3
+ // (ContentCollectionDefinition.variableSchema). Renders next to the text
4
+ // surface, never inside it — step 3 (rich/tiptap) reuses this same bar over
5
+ // a different editor, so it must not assume a textarea underneath.
6
+ import { Fragment } from "react";
7
+ import { useTranslation } from "../i18n.js";
8
+ import { usePrimitives } from "../primitives.js";
9
+ // No wrapping element: @cosmicdrift/kumiko-renderer is DOM-free (native impls
10
+ // have no <div>), and layout is a platform concern anyway — the caller
11
+ // (PlainContentEditor et al.) places these buttons in its own container.
12
+ export function VariableChips({ variables, onInsert, disabled }) {
13
+ const { Button } = usePrimitives();
14
+ const t = useTranslation();
15
+ return (_jsx(Fragment, { children: variables.map((name) => (_jsx(Button, { type: "button", variant: "secondary", size: "sm", disabled: disabled, onClick: () => onInsert(name), ariaLabel: t("kumiko.contentEditor.insertVariable", { name }), children: `{{${name}}}` }, name))) }));
16
+ }
@@ -0,0 +1,6 @@
1
+ import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
2
+ export declare class WriteFailedError extends Error {
3
+ readonly dispatcherError: DispatcherError;
4
+ constructor(error: DispatcherError, message: string);
5
+ }
6
+ export declare function dispatcherErrorText(error: DispatcherError, translate: (key: string, params?: Readonly<Record<string, unknown>>) => string): string;
@@ -0,0 +1,21 @@
1
+ // dispatcher.write wirft bei Server-Fehlern NICHT — es returnt
2
+ // { isSuccess: false, error }. Action-Wiring das das Result verwirft,
3
+ // macht Fehler unsichtbar ("Klick tut nichts"-Prod-Bug 2026-06-07).
4
+ // Diese Klasse trägt den strukturierten DispatcherError dahin, wo eine
5
+ // UI ihn anzeigen kann (Toast mit docsUrl).
6
+ export class WriteFailedError extends Error {
7
+ dispatcherError;
8
+ constructor(error, message) {
9
+ super(message);
10
+ this.name = "WriteFailedError";
11
+ this.dispatcherError = error;
12
+ }
13
+ }
14
+ // i18nKey gewinnt wenn das Bundle ihn kennt (translate returnt den Key
15
+ // unverändert wenn nicht — Renderer-Convention), sonst message, zuletzt code.
16
+ export function dispatcherErrorText(error, translate) {
17
+ const translated = translate(error.i18nKey, error.i18nParams);
18
+ if (translated !== error.i18nKey)
19
+ return translated;
20
+ return error.message !== "" ? error.message : error.code;
21
+ }
@@ -0,0 +1,10 @@
1
+ import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
2
+ import { type ReactNode } from "react";
3
+ export type EmbeddedListFieldProps = {
4
+ readonly field: EditFieldViewModel;
5
+ readonly id: string;
6
+ readonly onChange: (value: unknown) => void;
7
+ readonly allIssues: Readonly<Record<string, readonly FieldIssue[]>>;
8
+ readonly featureName: string;
9
+ };
10
+ export declare function EmbeddedListField({ field, id, onChange, allIssues, featureName, }: EmbeddedListFieldProps): ReactNode;
@@ -0,0 +1,259 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { computeDerivedCellValue, currencyDecimals, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "@cosmicdrift/kumiko-headless";
3
+ import { useState } from "react";
4
+ import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
5
+ import { useQuery } from "../hooks/use-query.js";
6
+ import { referenceLookupSource } from "../hooks/use-reference-lookup.js";
7
+ import { useTranslation } from "../i18n.js";
8
+ import { usePrimitives } from "../primitives.js";
9
+ function withRecomputedDerived(row, derived, cells) {
10
+ if (derived === undefined)
11
+ return row;
12
+ // Read sources from an unchanged snapshot, never from `result` (which this
13
+ // loop mutates) — mirrors withDerivedCells server-side so chained derived
14
+ // cells (e.g. net=qty*price, then gross=sum(net,tax)) can't drift between
15
+ // client preview and server depending on Object.entries(derived) order.
16
+ const source = { ...row };
17
+ const result = { ...row };
18
+ for (const [derivedField, def] of Object.entries(derived)) {
19
+ const values = def.from.map((src) => {
20
+ const v = source[src];
21
+ return typeof v === "number" ? v : undefined;
22
+ });
23
+ const computed = computeDerivedCellValue(def.op, values);
24
+ const target = cells.find((c) => c.field === derivedField);
25
+ result[derivedField] =
26
+ computed !== undefined && target !== undefined
27
+ ? roundDerivedCellValue(computed, target)
28
+ : computed;
29
+ }
30
+ return result;
31
+ }
32
+ // Locale-aware decimal parse for pasted text: strips space/NBSP thousands
33
+ // separators outright, then treats whichever of "," or "." appears LAST in
34
+ // the string as the decimal separator and strips the other one — handles
35
+ // both "1.234,56" (de) and "1,234.56" (en-US) pastes. Returns undefined for
36
+ // anything that still doesn't parse as a finite number.
37
+ function parseLocaleAwareDecimal(raw) {
38
+ const stripped = raw.replace(/[\s ]/g, "");
39
+ if (stripped === "")
40
+ return undefined;
41
+ const lastComma = stripped.lastIndexOf(",");
42
+ const lastDot = stripped.lastIndexOf(".");
43
+ const decimalIndex = Math.max(lastComma, lastDot);
44
+ if (decimalIndex === -1) {
45
+ const n = Number(stripped);
46
+ return Number.isFinite(n) ? n : undefined;
47
+ }
48
+ const integerPart = stripped.slice(0, decimalIndex).replace(/[.,]/g, "");
49
+ const fractionPart = stripped.slice(decimalIndex + 1);
50
+ if (!/^\d*$/.test(integerPart) || !/^\d*$/.test(fractionPart))
51
+ return undefined;
52
+ const n = Number(`${integerPart === "" ? "0" : integerPart}.${fractionPart === "" ? "0" : fractionPart}`);
53
+ return Number.isFinite(n) ? n : undefined;
54
+ }
55
+ function coerceCellValue(column, text, currency) {
56
+ switch (column.type) {
57
+ case "text":
58
+ return text;
59
+ case "number":
60
+ case "decimal": {
61
+ if (text.trim() === "")
62
+ return undefined;
63
+ const n = Number(text);
64
+ return Number.isFinite(n) ? n : undefined;
65
+ }
66
+ case "money": {
67
+ // Money cells are minor-unit integers (cents) in storage — paste
68
+ // arrives as a major-unit decimal string ("12,99"/"12.99"), so scale
69
+ // by the currency's decimal places. Must agree with the typed-in path
70
+ // (renderCellControl's MoneyInput, which scales by the same
71
+ // currencyDecimals) — a hardcoded ×100 here diverged for zero-/three-
72
+ // decimal currencies (JPY, BHD, ...), landing a pasted value 100x off
73
+ // from the same value typed by hand (kumiko-framework#1972).
74
+ // parseLocaleAwareDecimal handles both "1.234,56" (de) and
75
+ // "1,234.56" (en-US) thousands/decimal conventions — a naive
76
+ // `replace(",", ".")` mangled DE-locale pastes with thousands
77
+ // separators into NaN (kumiko-framework#1838).
78
+ if (text.trim() === "")
79
+ return undefined;
80
+ const n = parseLocaleAwareDecimal(text);
81
+ return n === undefined ? undefined : Math.round(n * 10 ** currencyDecimals(currency));
82
+ }
83
+ case "boolean":
84
+ return ["true", "1", "yes", "y", "ja"].includes(text.trim().toLowerCase());
85
+ case "date":
86
+ case "timestamp":
87
+ return text.trim();
88
+ case "select": {
89
+ const match = (column.options ?? []).find((opt) => opt === text || column.optionLabels?.[opt] === text);
90
+ return match;
91
+ }
92
+ case "reference": {
93
+ const match = (column.referenceOptions ?? []).find((o) => o.label === text || o.value === text);
94
+ return match?.value;
95
+ }
96
+ default: {
97
+ const exhaustiveCheck = column.type;
98
+ return exhaustiveCheck;
99
+ }
100
+ }
101
+ }
102
+ export function EmbeddedListField({ field, id, onChange, allIssues, featureName, }) {
103
+ const { EmbeddedListInput } = usePrimitives();
104
+ const t = useTranslation();
105
+ const [pasteWarning, setPasteWarning] = useState(null);
106
+ const cells = field.embeddedListCells ?? [];
107
+ const rows = Array.isArray(field.value) ? field.value : [];
108
+ const derived = field.embeddedListDerived;
109
+ const referenceCells = cells.filter((c) => c.type === "reference");
110
+ const referenceSources = referenceCells.map((cell) => cell.refOptionsQuery !== undefined
111
+ ? { queryQn: cell.refOptionsQuery, labelKey: "label" }
112
+ : referenceLookupSource(cell.refFeature ?? featureName, cell.refEntity ?? "", cell.refLabelField ?? "id"));
113
+ const referenceQueries = referenceSources.map((source) =>
114
+ // biome-ignore lint/correctness/useHookAtTopLevel: referenceCells comes from the entity-schema definition — fixed for the screen's lifetime, not a real conditional-hook risk.
115
+ useQuery(source.queryQn, {
116
+ limit: REFERENCE_COMBOBOX_LIMIT,
117
+ }));
118
+ if (EmbeddedListInput === undefined)
119
+ return null;
120
+ const columns = cells.map((cell) => {
121
+ const isDerived = derived?.[cell.field] !== undefined;
122
+ if (cell.type === "reference") {
123
+ const idx = referenceCells.indexOf(cell);
124
+ const query = referenceQueries[idx];
125
+ const labelKey = referenceSources[idx]?.labelKey ?? "id";
126
+ const referenceOptions = (query?.data?.rows ?? []).map((row) => ({
127
+ value: String(row["id"] ?? ""),
128
+ label: String(row[labelKey] ?? row["id"] ?? ""),
129
+ }));
130
+ return {
131
+ field: cell.field,
132
+ label: cell.label,
133
+ type: cell.type,
134
+ required: cell.required,
135
+ derived: isDerived,
136
+ referenceOptions,
137
+ referenceLoading: query?.loading ?? false,
138
+ };
139
+ }
140
+ return {
141
+ field: cell.field,
142
+ label: cell.label,
143
+ type: cell.type,
144
+ required: cell.required,
145
+ derived: isDerived,
146
+ ...(cell.options !== undefined && { options: cell.options }),
147
+ ...(cell.optionLabels !== undefined && { optionLabels: cell.optionLabels }),
148
+ };
149
+ });
150
+ const totals = (field.embeddedListTotals ?? []).map((subFieldName) => {
151
+ const cell = cells.find((c) => c.field === subFieldName);
152
+ return {
153
+ field: subFieldName,
154
+ label: cell?.label ?? subFieldName,
155
+ value: sumEmbeddedListColumn(rows, subFieldName),
156
+ };
157
+ });
158
+ const { listIssues, rowIssues, cellIssues } = groupEmbeddedListIssues(allIssues, field.field);
159
+ const combinedListIssues = pasteWarning === null
160
+ ? listIssues
161
+ : [
162
+ ...listIssues,
163
+ ...(pasteWarning.droppedRows > 0
164
+ ? [
165
+ {
166
+ path: field.field,
167
+ code: "paste-rows-truncated",
168
+ i18nKey: "kumiko.field.embedded-list.paste-rows-truncated",
169
+ params: { count: pasteWarning.droppedRows },
170
+ },
171
+ ]
172
+ : []),
173
+ ...(pasteWarning.unmatchedCells > 0
174
+ ? [
175
+ {
176
+ path: field.field,
177
+ code: "paste-cells-unmatched",
178
+ i18nKey: "kumiko.field.embedded-list.paste-cells-unmatched",
179
+ params: { count: pasteWarning.unmatchedCells },
180
+ },
181
+ ]
182
+ : []),
183
+ ];
184
+ function replaceRow(rowIndex, updater) {
185
+ const nextRows = rows.map((row, i) => (i === rowIndex ? updater(row) : row));
186
+ onChange(nextRows);
187
+ }
188
+ function handleCellChange(rowIndex, cellField, value) {
189
+ replaceRow(rowIndex, (row) => withRecomputedDerived({ ...row, [cellField]: value }, derived, cells));
190
+ }
191
+ function handleAddRow() {
192
+ onChange([...rows, withRecomputedDerived({}, derived, cells)]);
193
+ }
194
+ function handleRemoveRow(rowIndex) {
195
+ onChange(rows.filter((_, i) => i !== rowIndex));
196
+ }
197
+ function handleDuplicateRow(rowIndex) {
198
+ const source = rows[rowIndex];
199
+ if (source === undefined)
200
+ return;
201
+ const next = [...rows];
202
+ next.splice(rowIndex + 1, 0, { ...source });
203
+ onChange(next);
204
+ }
205
+ function handleMoveRow(fromIndex, toIndex) {
206
+ if (toIndex < 0 || toIndex >= rows.length)
207
+ return;
208
+ const next = [...rows];
209
+ const [moved] = next.splice(fromIndex, 1);
210
+ if (moved === undefined)
211
+ return;
212
+ next.splice(toIndex, 0, moved);
213
+ onChange(next);
214
+ }
215
+ function handlePasteCells(rowIndex, columnIndex, grid) {
216
+ const maxItems = field.embeddedListMaxItems;
217
+ const nextRows = [...rows];
218
+ const touchedIndices = new Set();
219
+ let droppedRows = 0;
220
+ let unmatchedCells = 0;
221
+ grid.forEach((gridRow, gridRowOffset) => {
222
+ const targetRowIndex = rowIndex + gridRowOffset;
223
+ if (targetRowIndex >= nextRows.length) {
224
+ if (maxItems !== undefined && nextRows.length >= maxItems) {
225
+ droppedRows += 1;
226
+ return;
227
+ }
228
+ nextRows.push({});
229
+ }
230
+ const targetRow = nextRows[targetRowIndex];
231
+ if (targetRow === undefined)
232
+ return;
233
+ let updatedRow = { ...targetRow };
234
+ gridRow.forEach((text, gridColOffset) => {
235
+ const column = columns[columnIndex + gridColOffset];
236
+ if (column === undefined)
237
+ return;
238
+ const coerced = coerceCellValue(column, text, field.embeddedListCurrency ?? "EUR");
239
+ // Also covers an unparseable money paste (e.g. mangled thousands
240
+ // separators) — the cell must stay unchanged instead of silently
241
+ // clearing to undefined (kumiko-framework#1838).
242
+ const isUnmatchedOrUnparseable = (column.type === "select" || column.type === "reference" || column.type === "money") &&
243
+ coerced === undefined &&
244
+ text.trim() !== "";
245
+ if (isUnmatchedOrUnparseable) {
246
+ unmatchedCells += 1;
247
+ return;
248
+ }
249
+ updatedRow = { ...updatedRow, [column.field]: coerced };
250
+ });
251
+ nextRows[targetRowIndex] = updatedRow;
252
+ touchedIndices.add(targetRowIndex);
253
+ });
254
+ setPasteWarning(droppedRows > 0 || unmatchedCells > 0 ? { droppedRows, unmatchedCells } : null);
255
+ const recomputed = nextRows.map((row, i) => touchedIndices.has(i) ? withRecomputedDerived(row, derived, cells) : row);
256
+ onChange(recomputed);
257
+ }
258
+ return (_jsx(EmbeddedListInput, { id: id, columns: columns, rows: rows, totals: totals, currency: field.embeddedListCurrency, disabled: field.readOnly, minItems: field.embeddedListMinItems, maxItems: field.embeddedListMaxItems, listIssues: combinedListIssues, rowIssues: rowIssues, cellIssues: cellIssues, onCellChange: handleCellChange, onAddRow: handleAddRow, onRemoveRow: handleRemoveRow, onDuplicateRow: handleDuplicateRow, onMoveRow: handleMoveRow, onPasteCells: handlePasteCells, addLabel: t("kumiko.field.embedded-list.add-row"), removeLabel: t("kumiko.field.embedded-list.remove-row"), duplicateLabel: t("kumiko.field.embedded-list.duplicate-row"), moveUpLabel: t("kumiko.field.embedded-list.move-up"), moveDownLabel: t("kumiko.field.embedded-list.move-down"), emptyLabel: t("kumiko.field.embedded-list.empty"), emptyCtaLabel: t("kumiko.field.embedded-list.empty-cta"), testId: id }));
259
+ }
@@ -0,0 +1,25 @@
1
+ import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
2
+ import type { ReactNode } from "react";
3
+ import type { usePrimitives } from "../primitives.js";
4
+ export type GridCellForFieldProps = {
5
+ readonly field: EditFieldViewModel;
6
+ readonly columns: number;
7
+ readonly issues: readonly FieldIssue[] | undefined;
8
+ readonly onChange: (value: unknown) => void;
9
+ readonly GridCell: ReturnType<typeof usePrimitives>["GridCell"];
10
+ /** Tier 2.7e-3: passed through so Reference fields can build the correct
11
+ * lookup query QN (`<feature>:query:<refEntity>:list`). */
12
+ readonly featureName: string;
13
+ readonly labelAppendix?: ReactNode;
14
+ readonly fieldAppendix?: ReactNode;
15
+ /** Full issues-by-path map (FormSnapshot.errors) — passed through for
16
+ * embedded-list fields, which bucket row-/cell-level issues themselves. */
17
+ readonly allIssues: Readonly<Record<string, readonly FieldIssue[]>>;
18
+ /** Passed through to RenderField unchanged — see RenderEditProps.valueDisplay. */
19
+ readonly valueDisplay: "form" | "text";
20
+ /** Passed through to RenderField as `row` — see RenderFieldProps.row. */
21
+ readonly row: Readonly<Record<string, unknown>>;
22
+ readonly changed?: boolean;
23
+ readonly flow?: boolean;
24
+ };
25
+ export declare function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, flow, }: GridCellForFieldProps): ReactNode;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { RenderField } from "./render-field.js";
3
+ const FIELD_CELL_WIDTH_BY_TYPE = {
4
+ number: "number",
5
+ decimal: "number",
6
+ bigInt: "number",
7
+ money: "money",
8
+ date: "date",
9
+ select: "select",
10
+ multiSelect: "select",
11
+ boolean: "auto",
12
+ longText: "full",
13
+ embedded: "full",
14
+ jsonb: "full",
15
+ file: "full",
16
+ files: "full",
17
+ image: "full",
18
+ images: "full",
19
+ };
20
+ function fieldCellWidth(field) {
21
+ if (field.span !== undefined || (field.type === "text" && field.multiline))
22
+ return "full";
23
+ return FIELD_CELL_WIDTH_BY_TYPE[field.type] ?? "text";
24
+ }
25
+ export function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, flow, }) {
26
+ // RenderField renders nothing for a hidden field, but the GridCell around it still claims the row.
27
+ if (!field.visible)
28
+ return null;
29
+ const effectiveSpan = field.span !== undefined ? Math.min(field.span, columns) : 1;
30
+ return (_jsx(GridCell, { span: effectiveSpan, ...(flow === true && { width: fieldCellWidth(field) }), children: _jsx(RenderField, { field: field, ...(issues !== undefined && { issues }), onChange: onChange, featureName: featureName, ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), allIssues: allIssues, valueDisplay: valueDisplay, row: row, ...(changed === true && { changed }) }) }));
31
+ }
@@ -0,0 +1,14 @@
1
+ import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
2
+ import type { ReactNode } from "react";
3
+ export type MultiSelectCheckboxOption = {
4
+ readonly value: string;
5
+ readonly label: string;
6
+ };
7
+ export type MultiSelectCheckboxesProps = {
8
+ readonly field: EditFieldViewModel;
9
+ readonly id: string;
10
+ readonly options: readonly MultiSelectCheckboxOption[];
11
+ readonly value: readonly string[];
12
+ readonly onChange: (value: readonly string[]) => void;
13
+ };
14
+ export declare function MultiSelectCheckboxes({ field, id, options, value, onChange, }: MultiSelectCheckboxesProps): ReactNode;
@@ -0,0 +1,36 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useTranslation } from "../i18n.js";
3
+ import { usePrimitives } from "../primitives.js";
4
+ // Renders MultiSelectFieldDef's `display: "checkboxes"` mode — every option
5
+ // as a visible checkbox plus a select-all toggle, built purely from the
6
+ // platform-neutral Primitives contract (Grid/GridCell/Field/Input kind
7
+ // "boolean", the same primitive `case "boolean"` uses in render-field.tsx).
8
+ // No renderer-web import here — this file must stay usable from Expo too.
9
+ export function MultiSelectCheckboxes({ field, id, options, value, onChange, }) {
10
+ const { Grid, GridCell, Field, Input, Button } = usePrimitives();
11
+ const t = useTranslation();
12
+ const disabled = field.readOnly;
13
+ const selected = new Set(value);
14
+ const allSelected = options.length > 0 && options.every((opt) => selected.has(opt.value));
15
+ // Always emits in `field.options` order (MultiSelectFieldDef's documented
16
+ // ordering guarantee), never in click/selection order.
17
+ const emitOrdered = (next) => {
18
+ onChange(options.filter((opt) => next.has(opt.value)).map((opt) => opt.value));
19
+ };
20
+ const toggleOption = (optionValue, checked) => {
21
+ const next = new Set(selected);
22
+ if (checked) {
23
+ next.add(optionValue);
24
+ }
25
+ else {
26
+ next.delete(optionValue);
27
+ }
28
+ emitOrdered(next);
29
+ };
30
+ const toggleAll = () => {
31
+ emitOrdered(allSelected ? new Set() : new Set(options.map((opt) => opt.value)));
32
+ };
33
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", disabled: disabled, onClick: toggleAll, testId: `${id}-select-all`, children: allSelected
34
+ ? t("kumiko.field.multiSelect.deselect-all")
35
+ : t("kumiko.field.multiSelect.select-all") }), _jsx(Grid, { columns: field.columns ?? 2, testId: `${id}-checkboxes`, ...(field.maxRows !== undefined && { maxRows: field.maxRows }), children: options.map((opt) => (_jsx(GridCell, { children: _jsx(Field, { id: `${id}-${opt.value}`, label: opt.label, layout: "inline", children: _jsx(Input, { kind: "boolean", id: `${id}-${opt.value}`, name: `${id}-${opt.value}`, value: selected.has(opt.value), onChange: (checked) => toggleOption(opt.value, checked), disabled: disabled }) }) }, opt.value))) })] }));
36
+ }
@@ -0,0 +1,13 @@
1
+ import type { EntityDefinition, EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { type ReactNode } from "react";
3
+ export declare function extractIdField(data: unknown, field: string): string | undefined;
4
+ export declare function extractCreatedId(data: unknown): string | undefined;
5
+ export type ReferenceCreateDialogProps = {
6
+ readonly open: boolean;
7
+ readonly onClose: () => void;
8
+ readonly onCreated: (id: string | undefined) => void;
9
+ readonly featureName: string;
10
+ readonly screen: EntityEditScreenDefinition;
11
+ readonly entity: EntityDefinition;
12
+ };
13
+ export declare function ReferenceCreateDialog({ open, onClose, onCreated, featureName, screen, entity, }: ReferenceCreateDialogProps): ReactNode;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { buildInitialValues } from "../app/kumiko-screen.js";
4
+ import { toKebab } from "../app/qn.js";
5
+ import { useTranslation } from "../i18n.js";
6
+ import { usePrimitives } from "../primitives.js";
7
+ import { RenderEdit } from "./render-edit.js";
8
+ // Hosts a target entity's create-form inside a bare Modal so a reference
9
+ // field can create a missing record without leaving the current form
10
+ // (kumiko-framework#1681) — same create wiring as EntityEditCreateBody
11
+ // (kumiko-screen.tsx), but reports the new id back via a callback instead
12
+ // of navigating to the entity's list screen.
13
+ function entityWriteCommand(featureName, entity) {
14
+ return `${toKebab(featureName)}:write:${toKebab(entity)}:create`;
15
+ }
16
+ // The create handler's success payload is `{ kind: "save", id, ... }`
17
+ // (see event-store-executor-write.ts) but RenderEdit's onSubmit only
18
+ // types it as `unknown` — narrow defensively instead of casting through it.
19
+ // `field` is author-declared (ActionFormRedirect.idFrom, fw#2670), never
20
+ // user input; the own-property check keeps a prototype key like
21
+ // "constructor" from resolving to anything.
22
+ export function extractIdField(data, field) {
23
+ if (typeof data !== "object" || data === null)
24
+ return undefined;
25
+ if (!Object.hasOwn(data, field))
26
+ return undefined;
27
+ const value = data[field];
28
+ return typeof value === "string" ? value : undefined;
29
+ }
30
+ export function extractCreatedId(data) {
31
+ return extractIdField(data, "id");
32
+ }
33
+ export function ReferenceCreateDialog({ open, onClose, onCreated, featureName, screen, entity, }) {
34
+ const { Modal } = usePrimitives();
35
+ const t = useTranslation();
36
+ const initial = useMemo(() => buildInitialValues(entity.fields, entity.defaultCurrency ?? "EUR"), [entity.fields, entity.defaultCurrency]);
37
+ const writeCommand = entityWriteCommand(featureName, screen.entity);
38
+ const handleSubmitted = (result) => {
39
+ if (result.validationBlocked || !result.isSuccess)
40
+ return;
41
+ onCreated(extractCreatedId(result.data));
42
+ };
43
+ if (!open)
44
+ return null;
45
+ return (_jsx(Modal, { open: open, onOpenChange: (next) => !next && onClose(), title: t("kumiko.actions.create"), children: _jsx(RenderEdit, { screen: screen, entity: entity, featureName: featureName, initial: initial, writeCommand: writeCommand, onSubmit: handleSubmitted, onCancel: onClose, ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }) }) }));
46
+ }
@@ -0,0 +1,25 @@
1
+ import type { RowActionDrawer } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { EditRelatedListSectionViewModel, Translate } from "@cosmicdrift/kumiko-headless";
3
+ import { type ReactNode } from "react";
4
+ export declare function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, }: {
5
+ readonly section: EditRelatedListSectionViewModel;
6
+ readonly parentId: string;
7
+ /** The enclosing projectionDetail's own record (the "Akte") — evaluates
8
+ * toolbarActions' `visible`/`params` (RelatedListToolbarAction), the same
9
+ * record header actions already use. */
10
+ readonly record: Readonly<Record<string, unknown>>;
11
+ readonly featureName: string;
12
+ readonly translate?: Translate;
13
+ readonly hideTitle?: boolean;
14
+ /** Under a fixed-height screen the tab panel chain stretches so the count footer sits at the bottom. */
15
+ readonly grow?: boolean;
16
+ /** Opens a drawer-kind rowAction (fw#2710). Supplied by the parent
17
+ * (ProjectionDetailBody), which owns schema + the actual Drawer render —
18
+ * this component only ever invokes the callback. */
19
+ readonly onOpenDrawer?: (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
20
+ /** section.actions, already resolved into buttons by the caller —
21
+ * rendered in the Section's title row. Only reachable in the standalone
22
+ * (non-`hideTitle`) branch below — `hideTitle` uses `FillContainer`, which
23
+ * has no title row to render actions into (see the comment on that branch). */
24
+ readonly actions?: ReactNode;
25
+ }): ReactNode;