@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
@@ -1,58 +0,0 @@
1
- // One bridge component per reference facet, mirroring ReferenceLookupBridge —
2
- // facet count varies per screen, so the hook can't be called in a loop.
3
- import { type ReactNode, useEffect } from "react";
4
- import { useReferenceLookup } from "../hooks/use-reference-lookup";
5
- import type { ResolvedFacetSpec } from "./list-facets";
6
-
7
- export type ReferenceFacetOption = { readonly value: string; readonly label: string };
8
-
9
- function ReferenceFacetOptionsBridge({
10
- field,
11
- refEntity,
12
- refFeature,
13
- labelField,
14
- onOptions,
15
- }: {
16
- readonly field: string;
17
- readonly refEntity: string;
18
- readonly refFeature: string;
19
- readonly labelField: string;
20
- readonly onOptions: (field: string, options: readonly ReferenceFacetOption[]) => void;
21
- }): ReactNode {
22
- const lookup = useReferenceLookup(refFeature, refEntity, labelField);
23
- useEffect(() => {
24
- if (lookup.loading) return;
25
- onOptions(
26
- field,
27
- [...lookup.map.entries()].map(([value, label]) => ({ value, label })),
28
- );
29
- }, [lookup.loading, lookup.map, field, onOptions]);
30
- return null;
31
- }
32
-
33
- // One invisible bridge per "reference" facet spec, each publishing its
34
- // resolved options back via `onOptions` once loaded.
35
- export function ReferenceFacetBridges({
36
- specs,
37
- onOptions,
38
- }: {
39
- readonly specs: readonly ResolvedFacetSpec[];
40
- readonly onOptions: (field: string, options: readonly ReferenceFacetOption[]) => void;
41
- }): ReactNode {
42
- const referenceSpecs = specs.filter((spec) => spec.reference !== undefined);
43
- if (referenceSpecs.length === 0) return null;
44
- return (
45
- <>
46
- {referenceSpecs.map((spec) => (
47
- <ReferenceFacetOptionsBridge
48
- key={spec.field}
49
- field={spec.field}
50
- refEntity={spec.reference?.refEntity ?? ""}
51
- refFeature={spec.reference?.refFeature ?? ""}
52
- labelField={spec.reference?.labelField ?? "id"}
53
- onOptions={onOptions}
54
- />
55
- ))}
56
- </>
57
- );
58
- }
@@ -1,234 +0,0 @@
1
- import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
- import { createContext, type ReactNode, useContext, useMemo } from "react";
3
- import { useUserRoles } from "../context/user-roles-context";
4
- import { useAppFeatures } from "./app-features-context";
5
- import type { FeatureSchema } from "./feature-schema";
6
- import { type NavApi, type NavTarget, type ObjectTarget, type ScreenTarget, useNav } from "./nav";
7
- import { lastSegment } from "./qn";
8
- import { screenAccessAllows } from "./screen-access";
9
-
10
- // A search param, not nav/route state, so it survives a full page reload
11
- // and a copied/shared URL.
12
- export const RETURN_TO_PARAM = "returnTo";
13
-
14
- // Screen levels one returnTo value may carry: the host itself plus its own
15
- // nested chain. Beyond that the host's returnTo is left out of the snapshot.
16
- const MAX_RETURN_DEPTH = 3;
17
- // Serialized host snapshot longer than this is dropped whole — the bare
18
- // target still resolves, only the restored state is lost.
19
- const MAX_RETURN_STATE_LENGTH = 512;
20
-
21
- const NO_RETURN_STATE: Readonly<Record<string, string>> = Object.freeze({});
22
-
23
- export type ReturnHost = {
24
- readonly screenId: string;
25
- readonly entityId?: string;
26
- };
27
-
28
- export type ReturnTo = {
29
- readonly path: string;
30
- readonly state: Readonly<Record<string, string>>;
31
- };
32
-
33
- /** Splits `<screenId>[/<entityId>][?<host-params>]`. One `URLSearchParams`
34
- * pass per nesting level — a deeper level stays encoded inside a value and
35
- * is only decoded when that level is reached. */
36
- export function splitReturnTo(raw: string): ReturnTo {
37
- const queryStart = raw.indexOf("?");
38
- if (queryStart === -1) return { path: raw, state: NO_RETURN_STATE };
39
- return {
40
- path: raw.slice(0, queryStart),
41
- state: Object.fromEntries(new URLSearchParams(raw.slice(queryStart + 1))),
42
- };
43
- }
44
-
45
- // Saturates at MAX_RETURN_DEPTH — callers only ask whether the cap is
46
- // reached, and an unbounded walk would follow attacker-sized nesting.
47
- function returnToDepth(raw: string): number {
48
- let depth = 1;
49
- let current = raw;
50
- while (depth < MAX_RETURN_DEPTH) {
51
- const nested = splitReturnTo(current).state[RETURN_TO_PARAM];
52
- if (nested === undefined) return depth;
53
- depth += 1;
54
- current = nested;
55
- }
56
- return depth;
57
- }
58
-
59
- // Both caps degrade to the pre-snapshot value instead of failing, so a value
60
- // that hits one still navigates.
61
- function returnStateSnapshot(hostParams: Readonly<Record<string, string>> | undefined): string {
62
- if (hostParams === undefined) return "";
63
- const snapshot = new URLSearchParams();
64
- for (const [key, value] of Object.entries(hostParams)) {
65
- if (value === "") continue;
66
- if (key === RETURN_TO_PARAM && returnToDepth(value) >= MAX_RETURN_DEPTH) continue;
67
- snapshot.set(key, value);
68
- }
69
- const serialized = snapshot.toString();
70
- return serialized.length > MAX_RETURN_STATE_LENGTH ? "" : serialized;
71
- }
72
-
73
- export function formatReturnTo(
74
- host: ReturnHost,
75
- hostParams?: Readonly<Record<string, string>>,
76
- ): string {
77
- const path = host.entityId !== undefined ? `${host.screenId}/${host.entityId}` : host.screenId;
78
- const state = returnStateSnapshot(hostParams);
79
- return state === "" ? path : `${path}?${state}`;
80
- }
81
-
82
- // {} when target IS host — pushPath no-ops on the same path, so the param
83
- // would land on the current page instead of the one being left.
84
- export function returnToParams(
85
- host: ReturnHost | undefined,
86
- target: ScreenTarget,
87
- hostParams?: Readonly<Record<string, string>>,
88
- ): Readonly<Record<string, string>> {
89
- if (host === undefined) return {};
90
- if (lastSegment(target.screenId) === host.screenId && target.entityId === host.entityId) {
91
- return {};
92
- }
93
- return { [RETURN_TO_PARAM]: formatReturnTo(host, hostParams) };
94
- }
95
-
96
- // ObjectTarget only resolves to a path inside the NavApi impl (detailFor
97
- // lookup across all features), so compare hrefs instead of the raw target.
98
- function objectTargetIsHost(nav: NavApi, target: ObjectTarget, host: ReturnHost): boolean {
99
- const hostTarget: ScreenTarget = {
100
- screenId: host.screenId,
101
- ...(host.entityId !== undefined && { entityId: host.entityId }),
102
- };
103
- return nav.hrefFor(target) === nav.hrefFor(hostTarget);
104
- }
105
-
106
- function returnParamsFor(
107
- nav: NavApi,
108
- target: NavTarget,
109
- host: ReturnHost | undefined,
110
- ): Readonly<Record<string, string>> {
111
- if ("screenId" in target) return returnToParams(host, target, nav.searchParams);
112
- if (host === undefined || objectTargetIsHost(nav, target, host)) return {};
113
- return { [RETURN_TO_PARAM]: formatReturnTo(host, nav.searchParams) };
114
- }
115
-
116
- export function navigateWithReturnTo(
117
- nav: NavApi,
118
- target: NavTarget,
119
- host: ReturnHost | undefined,
120
- params?: Readonly<Record<string, string | null>>,
121
- ): void {
122
- // Snapshot before navigating — nav.searchParams still describes the host.
123
- const returnParams = returnParamsFor(nav, target, host);
124
- nav.navigate(target);
125
- const merged: Record<string, string | null> = {
126
- ...(params ?? {}),
127
- ...returnParams,
128
- };
129
- if (Object.keys(merged).length > 0) {
130
- nav.setSearchParams(merged);
131
- }
132
- }
133
-
134
- /** Jumps back to an already-validated returnTo target and restores the host
135
- * search params carried in the same value. The host's own returnTo travels
136
- * with them, so a chain unwinds one level per jump. */
137
- export function navigateToReturn(nav: NavApi, target: ScreenTarget): void {
138
- const raw = nav.searchParams[RETURN_TO_PARAM];
139
- const state = raw === undefined ? NO_RETURN_STATE : splitReturnTo(raw).state;
140
- nav.navigate(target);
141
- // The explicit null drops this screen's own returnTo on nav impls whose
142
- // navigate keeps the query.
143
- nav.setSearchParams({ ...state, [RETURN_TO_PARAM]: state[RETURN_TO_PARAM] ?? null });
144
- }
145
-
146
- export function navigateToReturnOr(
147
- nav: NavApi,
148
- target: ScreenTarget | undefined,
149
- fallback: () => void,
150
- ): void {
151
- if (target === undefined) {
152
- fallback();
153
- return;
154
- }
155
- navigateToReturn(nav, target);
156
- }
157
-
158
- // "%" is left alone — route entityIds come raw from the path and can carry it.
159
- const UNSAFE_ENTITY_ID_PATTERN = /[\\?#\s]/;
160
-
161
- // Every failure branch returns undefined so a malformed or stale value falls
162
- // back to the caller's own default instead of throwing.
163
- export function resolveReturnTarget(
164
- raw: string | undefined,
165
- ownScreenId: string,
166
- features: readonly FeatureSchema[],
167
- userRoles: readonly string[] | undefined,
168
- ): ScreenTarget | undefined {
169
- if (raw === undefined || raw === "") return undefined;
170
- const segments = splitReturnTo(raw).path.split("/");
171
- if (segments.length !== 1 && segments.length !== 2) return undefined;
172
- if (segments.some((segment) => segment === "")) return undefined;
173
- const [screenId, entityId] = segments;
174
- if (screenId === undefined || screenId.includes(":")) return undefined;
175
- if (screenId === lastSegment(ownScreenId)) return undefined;
176
-
177
- let matched: ScreenDefinition | undefined;
178
- for (const feature of features) {
179
- matched = feature.screens.find((s) => lastSegment(s.id) === screenId);
180
- if (matched !== undefined) break;
181
- }
182
- if (matched === undefined) return undefined;
183
- if (!screenAccessAllows(matched.access, userRoles)) return undefined;
184
-
185
- if (entityId !== undefined) {
186
- if (entityId === "." || entityId === "..") return undefined;
187
- if (UNSAFE_ENTITY_ID_PATTERN.test(entityId)) return undefined;
188
- let decodedEntityId: string;
189
- try {
190
- decodedEntityId = decodeURIComponent(entityId);
191
- } catch {
192
- return undefined;
193
- }
194
- if (decodedEntityId === "." || decodedEntityId === "..") return undefined;
195
- if (UNSAFE_ENTITY_ID_PATTERN.test(decodedEntityId) || decodedEntityId.includes("/")) {
196
- return undefined;
197
- }
198
- if (matched.type !== "entityEdit" && matched.type !== "projectionDetail") return undefined;
199
- }
200
- if (matched.type === "projectionDetail" && matched.singleton !== true && entityId === undefined) {
201
- return undefined;
202
- }
203
-
204
- return { screenId, ...(entityId !== undefined && { entityId }) };
205
- }
206
-
207
- const ReturnHostContext = createContext<ReturnHost | undefined>(undefined);
208
-
209
- export function ReturnHostProvider({
210
- value,
211
- children,
212
- }: {
213
- readonly value: ReturnHost;
214
- readonly children: ReactNode;
215
- }): ReactNode {
216
- return <ReturnHostContext.Provider value={value}>{children}</ReturnHostContext.Provider>;
217
- }
218
-
219
- export function useReturnHost(): ReturnHost | undefined {
220
- return useContext(ReturnHostContext);
221
- }
222
-
223
- // Reads through useNav().searchParams (reactive), not a useState snapshot —
224
- // a later setSearchParams call must not leave this stale.
225
- export function useReturnTarget(ownScreenId: string): ScreenTarget | undefined {
226
- const nav = useNav();
227
- const features = useAppFeatures();
228
- const userRoles = useUserRoles();
229
- const raw = nav.searchParams[RETURN_TO_PARAM];
230
- return useMemo(
231
- () => resolveReturnTarget(raw, ownScreenId, features, userRoles),
232
- [raw, ownScreenId, features, userRoles],
233
- );
234
- }