@cosmicdrift/kumiko-renderer 0.328.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 (259) 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 -46
  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 -41
  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/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
  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/__tests__/error-i18n-defaults.test.ts +0 -82
  135. package/src/__tests__/format-when.test.ts +0 -12
  136. package/src/__tests__/i18n.test.tsx +0 -283
  137. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  138. package/src/__tests__/qn.test.ts +0 -83
  139. package/src/__tests__/share-links.test.ts +0 -26
  140. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  141. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  142. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  143. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  144. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  145. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  146. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  147. package/src/app/__tests__/content-editors.test.tsx +0 -128
  148. package/src/app/__tests__/content-preview.test.tsx +0 -184
  149. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  150. package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
  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 -128
  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 -3801
  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 -86
  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 -640
  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 -1733
  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 -88
  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 -491
  228. package/src/components/render-edit-action-button.tsx +0 -109
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -226
  231. package/src/components/render-edit.tsx +0 -1871
  232. package/src/components/render-field.tsx +0 -951
  233. package/src/components/render-list.tsx +0 -593
  234. package/src/components/write-form-section.tsx +0 -198
  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-query.test.tsx +0 -149
  241. package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
  242. package/src/hooks/use-ai-text.ts +0 -172
  243. package/src/hooks/use-disclosure.ts +0 -20
  244. package/src/hooks/use-form.ts +0 -78
  245. package/src/hooks/use-list-url-state.ts +0 -207
  246. package/src/hooks/use-mutation.ts +0 -61
  247. package/src/hooks/use-query.ts +0 -166
  248. package/src/hooks/use-reference-lookup.ts +0 -76
  249. package/src/hooks/use-store.ts +0 -48
  250. package/src/hooks/use-stream-handler.ts +0 -134
  251. package/src/i18n-defaults.ts +0 -240
  252. package/src/i18n.tsx +0 -286
  253. package/src/index.ts +0 -279
  254. package/src/primitives.tsx +0 -1567
  255. package/src/screen-fills-height.ts +0 -3
  256. package/src/share-links.ts +0 -12
  257. package/src/sort-by-accessor.ts +0 -20
  258. package/src/sse/live-events.tsx +0 -56
  259. package/src/tokens.tsx +0 -170
@@ -16,21 +16,19 @@
16
16
  // nimmt den letzten ":"-getrennten Teil. Robust gegen Strings ohne
17
17
  // ":" (returnt sie unverändert, damit App-Author-Code mit Short-Form-
18
18
  // ids in eigenen Stellen weiter passt).
19
-
20
- export function lastSegment(qn: string): string {
21
- const idx = qn.lastIndexOf(":");
22
- return idx < 0 ? qn : qn.slice(idx + 1);
19
+ export function lastSegment(qn) {
20
+ const idx = qn.lastIndexOf(":");
21
+ return idx < 0 ? qn : qn.slice(idx + 1);
23
22
  }
24
-
25
23
  /**
26
24
  * Client-safe copy of framework `toKebab` (engine/qualified-name.ts).
27
25
  * Must stay in sync — do NOT import from `@cosmicdrift/kumiko-framework/engine`
28
26
  * here: that barrel pulls server deps into the browser bundle.
29
27
  */
30
- export function toKebab(input: string): string {
31
- return input
32
- .replace(/\./g, "-")
33
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
34
- .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
35
- .toLowerCase();
28
+ export function toKebab(input) {
29
+ return input
30
+ .replace(/\./g, "-")
31
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
32
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
33
+ .toLowerCase();
36
34
  }
@@ -0,0 +1,2 @@
1
+ export declare function qualifyScreenId(featureName: string, screenId: string): string;
2
+ export declare function featureNameFromQualifiedScreenId(id: string): string | undefined;
@@ -1,15 +1,14 @@
1
1
  // Dependency-free: kumiko-screen.tsx already imports useNav from nav.tsx, so
2
2
  // this can't live in either without a cycle. Form must match the registry's
3
3
  // qualification rule (packages/framework/src/engine/qualified-name.ts).
4
- export function qualifyScreenId(featureName: string, screenId: string): string {
5
- return `${featureName}:screen:${screenId}`;
4
+ export function qualifyScreenId(featureName, screenId) {
5
+ return `${featureName}:screen:${screenId}`;
6
6
  }
7
-
8
7
  // Inverse of qualifyScreenId. A short id ("task-list") has no feature
9
8
  // segment and returns undefined — callers need that to tell "this redirect
10
9
  // names a feature" apart from "this redirect is feature-less, resolve by
11
10
  // short id alone" (fw#2485).
12
- export function featureNameFromQualifiedScreenId(id: string): string | undefined {
13
- const parts = id.split(":");
14
- return parts.length === 3 && parts[1] === "screen" ? parts[0] : undefined;
11
+ export function featureNameFromQualifiedScreenId(id) {
12
+ const parts = id.split(":");
13
+ return parts.length === 3 && parts[1] === "screen" ? parts[0] : undefined;
15
14
  }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ResolvedFacetSpec } from "./list-facets.js";
3
+ export type ReferenceFacetOption = {
4
+ readonly value: string;
5
+ readonly label: string;
6
+ };
7
+ export declare function ReferenceFacetBridges({ specs, onOptions, }: {
8
+ readonly specs: readonly ResolvedFacetSpec[];
9
+ readonly onOptions: (field: string, options: readonly ReferenceFacetOption[]) => void;
10
+ }): ReactNode;
@@ -0,0 +1,22 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ // One bridge component per reference facet, mirroring ReferenceLookupBridge —
3
+ // facet count varies per screen, so the hook can't be called in a loop.
4
+ import { useEffect } from "react";
5
+ import { useReferenceLookup } from "../hooks/use-reference-lookup.js";
6
+ function ReferenceFacetOptionsBridge({ field, refEntity, refFeature, labelField, onOptions, }) {
7
+ const lookup = useReferenceLookup(refFeature, refEntity, labelField);
8
+ useEffect(() => {
9
+ if (lookup.loading)
10
+ return;
11
+ onOptions(field, [...lookup.map.entries()].map(([value, label]) => ({ value, label })));
12
+ }, [lookup.loading, lookup.map, field, onOptions]);
13
+ return null;
14
+ }
15
+ // One invisible bridge per "reference" facet spec, each publishing its
16
+ // resolved options back via `onOptions` once loaded.
17
+ export function ReferenceFacetBridges({ specs, onOptions, }) {
18
+ const referenceSpecs = specs.filter((spec) => spec.reference !== undefined);
19
+ if (referenceSpecs.length === 0)
20
+ return null;
21
+ return (_jsx(_Fragment, { children: referenceSpecs.map((spec) => (_jsx(ReferenceFacetOptionsBridge, { field: spec.field, refEntity: spec.reference?.refEntity ?? "", refFeature: spec.reference?.refFeature ?? "", labelField: spec.reference?.labelField ?? "id", onOptions: onOptions }, spec.field))) }));
22
+ }
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from "react";
2
+ import type { FeatureSchema } from "./feature-schema.js";
3
+ import { type NavApi, type NavTarget, type ScreenTarget } from "./nav.js";
4
+ export declare const RETURN_TO_PARAM = "returnTo";
5
+ export type ReturnHost = {
6
+ readonly screenId: string;
7
+ readonly entityId?: string;
8
+ };
9
+ export type ReturnTo = {
10
+ readonly path: string;
11
+ readonly state: Readonly<Record<string, string>>;
12
+ };
13
+ /** Splits `<screenId>[/<entityId>][?<host-params>]`. One `URLSearchParams`
14
+ * pass per nesting level — a deeper level stays encoded inside a value and
15
+ * is only decoded when that level is reached. */
16
+ export declare function splitReturnTo(raw: string): ReturnTo;
17
+ export declare function formatReturnTo(host: ReturnHost, hostParams?: Readonly<Record<string, string>>): string;
18
+ export declare function returnToParams(host: ReturnHost | undefined, target: ScreenTarget, hostParams?: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
19
+ export declare function navigateWithReturnTo(nav: NavApi, target: NavTarget, host: ReturnHost | undefined, params?: Readonly<Record<string, string | null>>): void;
20
+ /** Jumps back to an already-validated returnTo target and restores the host
21
+ * search params carried in the same value. The host's own returnTo travels
22
+ * with them, so a chain unwinds one level per jump. */
23
+ export declare function navigateToReturn(nav: NavApi, target: ScreenTarget): void;
24
+ export declare function navigateToReturnOr(nav: NavApi, target: ScreenTarget | undefined, fallback: () => void): void;
25
+ export declare function resolveReturnTarget(raw: string | undefined, ownScreenId: string, features: readonly FeatureSchema[], userRoles: readonly string[] | undefined): ScreenTarget | undefined;
26
+ export declare function ReturnHostProvider({ value, children, }: {
27
+ readonly value: ReturnHost;
28
+ readonly children: ReactNode;
29
+ }): ReactNode;
30
+ export declare function useReturnHost(): ReturnHost | undefined;
31
+ export declare function useReturnTarget(ownScreenId: string): ScreenTarget | undefined;
@@ -0,0 +1,188 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { useUserRoles } from "../context/user-roles-context.js";
4
+ import { useAppFeatures } from "./app-features-context.js";
5
+ import { useNav, } from "./nav.js";
6
+ import { lastSegment } from "./qn.js";
7
+ import { screenAccessAllows } from "./screen-access.js";
8
+ // A search param, not nav/route state, so it survives a full page reload
9
+ // and a copied/shared URL.
10
+ export const RETURN_TO_PARAM = "returnTo";
11
+ // Screen levels one returnTo value may carry: the host itself plus its own
12
+ // nested chain. Beyond that the host's returnTo is left out of the snapshot.
13
+ const MAX_RETURN_DEPTH = 3;
14
+ // Serialized host snapshot longer than this is dropped whole — the bare
15
+ // target still resolves, only the restored state is lost.
16
+ const MAX_RETURN_STATE_LENGTH = 512;
17
+ const NO_RETURN_STATE = Object.freeze({});
18
+ /** Splits `<screenId>[/<entityId>][?<host-params>]`. One `URLSearchParams`
19
+ * pass per nesting level — a deeper level stays encoded inside a value and
20
+ * is only decoded when that level is reached. */
21
+ export function splitReturnTo(raw) {
22
+ const queryStart = raw.indexOf("?");
23
+ if (queryStart === -1)
24
+ return { path: raw, state: NO_RETURN_STATE };
25
+ return {
26
+ path: raw.slice(0, queryStart),
27
+ state: Object.fromEntries(new URLSearchParams(raw.slice(queryStart + 1))),
28
+ };
29
+ }
30
+ // Saturates at MAX_RETURN_DEPTH — callers only ask whether the cap is
31
+ // reached, and an unbounded walk would follow attacker-sized nesting.
32
+ function returnToDepth(raw) {
33
+ let depth = 1;
34
+ let current = raw;
35
+ while (depth < MAX_RETURN_DEPTH) {
36
+ const nested = splitReturnTo(current).state[RETURN_TO_PARAM];
37
+ if (nested === undefined)
38
+ return depth;
39
+ depth += 1;
40
+ current = nested;
41
+ }
42
+ return depth;
43
+ }
44
+ // Both caps degrade to the pre-snapshot value instead of failing, so a value
45
+ // that hits one still navigates.
46
+ function returnStateSnapshot(hostParams) {
47
+ if (hostParams === undefined)
48
+ return "";
49
+ const snapshot = new URLSearchParams();
50
+ for (const [key, value] of Object.entries(hostParams)) {
51
+ if (value === "")
52
+ continue;
53
+ if (key === RETURN_TO_PARAM && returnToDepth(value) >= MAX_RETURN_DEPTH)
54
+ continue;
55
+ snapshot.set(key, value);
56
+ }
57
+ const serialized = snapshot.toString();
58
+ return serialized.length > MAX_RETURN_STATE_LENGTH ? "" : serialized;
59
+ }
60
+ export function formatReturnTo(host, hostParams) {
61
+ const path = host.entityId !== undefined ? `${host.screenId}/${host.entityId}` : host.screenId;
62
+ const state = returnStateSnapshot(hostParams);
63
+ return state === "" ? path : `${path}?${state}`;
64
+ }
65
+ // {} when target IS host — pushPath no-ops on the same path, so the param
66
+ // would land on the current page instead of the one being left.
67
+ export function returnToParams(host, target, hostParams) {
68
+ if (host === undefined)
69
+ return {};
70
+ if (lastSegment(target.screenId) === host.screenId && target.entityId === host.entityId) {
71
+ return {};
72
+ }
73
+ return { [RETURN_TO_PARAM]: formatReturnTo(host, hostParams) };
74
+ }
75
+ // ObjectTarget only resolves to a path inside the NavApi impl (detailFor
76
+ // lookup across all features), so compare hrefs instead of the raw target.
77
+ function objectTargetIsHost(nav, target, host) {
78
+ const hostTarget = {
79
+ screenId: host.screenId,
80
+ ...(host.entityId !== undefined && { entityId: host.entityId }),
81
+ };
82
+ return nav.hrefFor(target) === nav.hrefFor(hostTarget);
83
+ }
84
+ function returnParamsFor(nav, target, host) {
85
+ if ("screenId" in target)
86
+ return returnToParams(host, target, nav.searchParams);
87
+ if (host === undefined || objectTargetIsHost(nav, target, host))
88
+ return {};
89
+ return { [RETURN_TO_PARAM]: formatReturnTo(host, nav.searchParams) };
90
+ }
91
+ export function navigateWithReturnTo(nav, target, host, params) {
92
+ // Snapshot before navigating — nav.searchParams still describes the host.
93
+ const returnParams = returnParamsFor(nav, target, host);
94
+ nav.navigate(target);
95
+ const merged = {
96
+ ...(params ?? {}),
97
+ ...returnParams,
98
+ };
99
+ if (Object.keys(merged).length > 0) {
100
+ nav.setSearchParams(merged);
101
+ }
102
+ }
103
+ /** Jumps back to an already-validated returnTo target and restores the host
104
+ * search params carried in the same value. The host's own returnTo travels
105
+ * with them, so a chain unwinds one level per jump. */
106
+ export function navigateToReturn(nav, target) {
107
+ const raw = nav.searchParams[RETURN_TO_PARAM];
108
+ const state = raw === undefined ? NO_RETURN_STATE : splitReturnTo(raw).state;
109
+ nav.navigate(target);
110
+ // The explicit null drops this screen's own returnTo on nav impls whose
111
+ // navigate keeps the query.
112
+ nav.setSearchParams({ ...state, [RETURN_TO_PARAM]: state[RETURN_TO_PARAM] ?? null });
113
+ }
114
+ export function navigateToReturnOr(nav, target, fallback) {
115
+ if (target === undefined) {
116
+ fallback();
117
+ return;
118
+ }
119
+ navigateToReturn(nav, target);
120
+ }
121
+ // "%" is left alone — route entityIds come raw from the path and can carry it.
122
+ const UNSAFE_ENTITY_ID_PATTERN = /[\\?#\s]/;
123
+ // Every failure branch returns undefined so a malformed or stale value falls
124
+ // back to the caller's own default instead of throwing.
125
+ export function resolveReturnTarget(raw, ownScreenId, features, userRoles) {
126
+ if (raw === undefined || raw === "")
127
+ return undefined;
128
+ const segments = splitReturnTo(raw).path.split("/");
129
+ if (segments.length !== 1 && segments.length !== 2)
130
+ return undefined;
131
+ if (segments.some((segment) => segment === ""))
132
+ return undefined;
133
+ const [screenId, entityId] = segments;
134
+ if (screenId === undefined || screenId.includes(":"))
135
+ return undefined;
136
+ if (screenId === lastSegment(ownScreenId))
137
+ return undefined;
138
+ let matched;
139
+ for (const feature of features) {
140
+ matched = feature.screens.find((s) => lastSegment(s.id) === screenId);
141
+ if (matched !== undefined)
142
+ break;
143
+ }
144
+ if (matched === undefined)
145
+ return undefined;
146
+ if (!screenAccessAllows(matched.access, userRoles))
147
+ return undefined;
148
+ if (entityId !== undefined) {
149
+ if (entityId === "." || entityId === "..")
150
+ return undefined;
151
+ if (UNSAFE_ENTITY_ID_PATTERN.test(entityId))
152
+ return undefined;
153
+ let decodedEntityId;
154
+ try {
155
+ decodedEntityId = decodeURIComponent(entityId);
156
+ }
157
+ catch {
158
+ return undefined;
159
+ }
160
+ if (decodedEntityId === "." || decodedEntityId === "..")
161
+ return undefined;
162
+ if (UNSAFE_ENTITY_ID_PATTERN.test(decodedEntityId) || decodedEntityId.includes("/")) {
163
+ return undefined;
164
+ }
165
+ if (matched.type !== "entityEdit" && matched.type !== "projectionDetail")
166
+ return undefined;
167
+ }
168
+ if (matched.type === "projectionDetail" && matched.singleton !== true && entityId === undefined) {
169
+ return undefined;
170
+ }
171
+ return { screenId, ...(entityId !== undefined && { entityId }) };
172
+ }
173
+ const ReturnHostContext = createContext(undefined);
174
+ export function ReturnHostProvider({ value, children, }) {
175
+ return _jsx(ReturnHostContext.Provider, { value: value, children: children });
176
+ }
177
+ export function useReturnHost() {
178
+ return useContext(ReturnHostContext);
179
+ }
180
+ // Reads through useNav().searchParams (reactive), not a useState snapshot —
181
+ // a later setSearchParams call must not leave this stale.
182
+ export function useReturnTarget(ownScreenId) {
183
+ const nav = useNav();
184
+ const features = useAppFeatures();
185
+ const userRoles = useUserRoles();
186
+ const raw = nav.searchParams[RETURN_TO_PARAM];
187
+ return useMemo(() => resolveReturnTarget(raw, ownScreenId, features, userRoles), [raw, ownScreenId, features, userRoles]);
188
+ }
@@ -0,0 +1,77 @@
1
+ import type { EntityEditScreenDefinition, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ToolbarAction } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { Dispatcher, ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
3
+ import type { RenderEditAction } from "../components/render-edit-types.js";
4
+ import type { ToolbarActionButton } from "../components/render-list.js";
5
+ import type { DataTableRowAction } from "../primitives.js";
6
+ import type { NavApi } from "./nav.js";
7
+ import { type ReturnHost } from "./return-to.js";
8
+ export declare function buildDefaultEditRowAction(editScreen: EntityEditScreenDefinition | undefined, idColumn?: string): RowActionNavigate | undefined;
9
+ export declare function evalRowExtractor(extractor: RowFieldExtractor, row: Record<string, unknown>): Record<string, unknown>;
10
+ export declare function isWriteHandlerRowAction(action: RowAction): action is RowActionWriteHandler;
11
+ export declare function stringifyNavParams(params: Record<string, unknown>): Record<string, string | null>;
12
+ export declare function navigateActionSearchParams(action: RowActionNavigate, values: Readonly<Record<string, unknown>>): Record<string, string | null> | undefined;
13
+ export declare function refetchAfterWrite(refetch: () => Promise<unknown>): Promise<void>;
14
+ export declare function runProjectionRowNavigate(nav: NavApi, action: RowActionNavigate, row: ListRowViewModel, host: ReturnHost | undefined): void;
15
+ type OpenDrawer = (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
16
+ /** @internal test-only */
17
+ export declare function resetDrawerActionWarningsForTests(): void;
18
+ export declare function buildProjectionRowActions(options: {
19
+ readonly rowActions: readonly RowAction[] | undefined;
20
+ readonly translate: Translate;
21
+ readonly dispatcher: Dispatcher | undefined;
22
+ readonly nav: NavApi;
23
+ readonly refetch: () => Promise<unknown>;
24
+ /** Opens the drawer-kind action's target actionForm, prefilled from the
25
+ * clicked row. Omitted callers (e.g. RelatedListSection embedded directly
26
+ * without onOpenDrawer) drop drawer actions and get a dev warning —
27
+ * mirrors the `dispatcher === undefined` skip below for writeHandler. */
28
+ readonly openDrawer?: OpenDrawer;
29
+ /** Prepended unless a declared action already has id "edit" — declared wins. */
30
+ readonly defaultEditRowAction?: RowActionNavigate;
31
+ readonly host: ReturnHost | undefined;
32
+ }): readonly DataTableRowAction[] | undefined;
33
+ type OpenToolbarDrawer = (action: ToolbarAction & {
34
+ readonly kind: "drawer";
35
+ }) => void;
36
+ export declare function buildProjectionToolbarActions(options: {
37
+ readonly toolbarActions: readonly RelatedListToolbarAction[] | undefined;
38
+ readonly translate: Translate;
39
+ readonly dispatcher: Dispatcher | undefined;
40
+ readonly nav: NavApi;
41
+ readonly refetch: () => Promise<unknown>;
42
+ /** Omitted callers drop drawer-kind toolbar actions and get a dev warning,
43
+ * mirroring the rowActions behavior above. */
44
+ readonly openDrawer?: OpenToolbarDrawer;
45
+ /** Search params set on the target after a navigate-kind action, e.g. a
46
+ * relatedList's parent id for create-form prefill. */
47
+ readonly navigatePrefill?: Readonly<Record<string, unknown>>;
48
+ /** The relatedList's parent record — only a relatedList caller has one.
49
+ * Enables `visible`/`params` (RelatedListToolbarAction), evaluated
50
+ * against it exactly like header actions/RowAction.visible. Plain
51
+ * entityList/projectionList toolbars have no record and omit this. */
52
+ readonly record?: Readonly<Record<string, unknown>>;
53
+ readonly host: ReturnHost | undefined;
54
+ }): readonly ToolbarActionButton[] | undefined;
55
+ export declare function buildRecordActions(options: {
56
+ readonly actions: readonly RowAction[];
57
+ readonly record: Readonly<Record<string, unknown>>;
58
+ readonly translate: Translate;
59
+ readonly nav: NavApi;
60
+ readonly host: ReturnHost | undefined;
61
+ readonly dispatcher: Dispatcher | undefined;
62
+ readonly openDrawer: (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
63
+ /** Called after a writeHandler action succeeds (refetch/reload). */
64
+ readonly onWriteSuccess: () => void | Promise<void>;
65
+ /** Screen-target navigate default entityId when the action declares no
66
+ * explicit `entityId` field-source. A caller editing a fixed entity can
67
+ * pass its own `entityId` here directly (the record it already is). A
68
+ * caller whose shown record may differ from the navigate target's entity
69
+ * resolves this per-target via `sameEntityScreenId` instead. */
70
+ readonly defaultScreenTargetEntityId?: string;
71
+ /** Given a navigate action's target screen id, returns the record id to
72
+ * default to IF that screen edits the same entity the current record
73
+ * belongs to (cross-feature lookup) — undefined otherwise. Omitted by
74
+ * callers with no such cross-screen lookup. */
75
+ readonly sameEntityScreenId?: (targetScreen: string) => string | undefined;
76
+ }): readonly RenderEditAction[] | undefined;
77
+ export {};