@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
@@ -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
- }