@jskit-ai/shell-web 0.1.250 → 0.1.252

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 (47) hide show
  1. package/fixtures/adaptive-shell/src/main.js +6 -3
  2. package/package.json +2 -2
  3. package/patterns/application-shell/PATTERN.md +18 -4
  4. package/patterns/application-shell/example/src/pages/home/index.vue +15 -5
  5. package/patterns/application-shell/example/tests/e2e/adaptive-shell.spec.ts +1 -1
  6. package/src/architecture/inspectVueArchitecture.js +1092 -336
  7. package/src/client/asyncModuleRecovery/runtime.js +116 -0
  8. package/src/client/components/ShellLayout.vue +18 -288
  9. package/src/client/components/ShellMenuLinkItem.vue +3 -21
  10. package/src/client/components/ShellOutlet.vue +2 -24
  11. package/src/client/components/ShellRouteTransition.vue +16 -114
  12. package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +5 -100
  13. package/src/client/components/ShellTabLinkItem.vue +5 -101
  14. package/src/client/composables/useShellNavigationLink.js +85 -0
  15. package/src/client/composables/useShellPullToRefresh.js +260 -0
  16. package/src/client/error/index.js +1 -1
  17. package/src/client/error/inject.js +4 -1
  18. package/src/client/error/policy.js +4 -13
  19. package/src/client/error/presenters.js +4 -9
  20. package/src/client/error/store.js +1 -1
  21. package/src/client/navigation/linkResolver.js +5 -27
  22. package/src/client/navigation/usePaths.js +1 -21
  23. package/src/client/placement/index.js +2 -0
  24. package/src/client/placement/inject.js +4 -1
  25. package/src/client/placement/runtime.js +2 -4
  26. package/src/client/placement/surfaceContext.js +11 -8
  27. package/src/client/placement/validators.js +18 -65
  28. package/src/client/providers/ShellWebClientProvider.js +100 -356
  29. package/src/client/requestRecovery/runtime.js +29 -77
  30. package/src/client/runtime/bootstrapRuntime.js +5 -4
  31. package/src/client/runtime/refreshRuntime.js +122 -0
  32. package/src/client/support/gestureTargets.js +26 -0
  33. package/src/client/support/menuLinkTarget.js +35 -19
  34. package/src/client/support/routeParams.js +26 -0
  35. package/src/client/support/routeSwipeGesture.js +88 -0
  36. package/src/client/support/routeTransitionKey.js +1 -3
  37. package/src/client/support/shellLayoutClass.js +32 -0
  38. package/test/bootstrapRuntime.test.js +98 -0
  39. package/test/errorActions.test.js +2 -1
  40. package/test/errorRuntime.test.js +30 -0
  41. package/test/linkResolver.test.js +23 -1
  42. package/test/menuLinkTarget.test.js +77 -0
  43. package/test/routeSwipeGesture.test.js +49 -0
  44. package/test/settingsPlacementContract.test.js +16 -6
  45. package/test/shellGestureSupport.test.js +46 -0
  46. package/test/surfaceContext.test.js +53 -0
  47. package/test/useShellPullToRefresh.test.js +123 -0
@@ -0,0 +1,260 @@
1
+ import { computed, onBeforeUnmount, onMounted, ref, unref } from "vue";
2
+ import { isPrimaryTouchPointer, isShellGestureIgnoredTarget } from "../support/gestureTargets.js";
3
+
4
+ const PULL_REFRESH_TRIGGER_DISTANCE = 72;
5
+ const PULL_REFRESH_MAX_DISTANCE = 112;
6
+ const PULL_REFRESH_IGNORED_SELECTORS = Object.freeze(["[data-shell-pull-refresh-ignore]"]);
7
+
8
+ function isAtPageTop() {
9
+ if (typeof window !== "object" || typeof document !== "object") {
10
+ return false;
11
+ }
12
+
13
+ const documentScrollTop = Number(document.documentElement?.scrollTop || 0);
14
+ const bodyScrollTop = Number(document.body?.scrollTop || 0);
15
+ return Math.max(Number(window.scrollY || 0), documentScrollTop, bodyScrollTop) <= 0;
16
+ }
17
+
18
+ // Compact-layout pull-to-refresh. Pointer events start the gesture where the
19
+ // browser supports them; touch events carry it on after the browser cancels the
20
+ // pointer to scroll, and start it where pointer events are missing.
21
+ function useShellPullToRefresh({ enabled, refreshRuntime = null } = {}) {
22
+ const pullDistance = ref(0);
23
+ const pullRefreshing = ref(false);
24
+ let activePull = null;
25
+
26
+ const pullProgress = computed(() =>
27
+ Math.min(100, Math.round((pullDistance.value / PULL_REFRESH_TRIGGER_DISTANCE) * 100))
28
+ );
29
+ const pullIndicatorVisible = computed(() =>
30
+ Boolean(unref(enabled) && (pullDistance.value > 0 || pullRefreshing.value))
31
+ );
32
+ const pullRefreshLabel = computed(() => {
33
+ if (pullRefreshing.value) {
34
+ return "Refreshing";
35
+ }
36
+ return pullProgress.value >= 100 ? "Release to refresh" : "Pull to refresh";
37
+ });
38
+ const pullRefreshStyle = computed(() => ({
39
+ "--shell-pull-refresh-distance": `${Math.round(Math.min(pullDistance.value, PULL_REFRESH_MAX_DISTANCE))}px`
40
+ }));
41
+
42
+ onMounted(() => {
43
+ if (typeof window !== "object") {
44
+ return;
45
+ }
46
+ window.addEventListener("pointerdown", handlePullPointerDown, { capture: true, passive: true });
47
+ window.addEventListener("pointermove", handlePullPointerMove, { capture: true, passive: false });
48
+ window.addEventListener("pointerup", handlePullPointerEnd, { capture: true, passive: true });
49
+ window.addEventListener("pointercancel", handlePullPointerCancel, { capture: true, passive: true });
50
+ window.addEventListener("touchstart", handlePullTouchStart, { capture: true, passive: true });
51
+ window.addEventListener("touchmove", handlePullTouchMove, { capture: true, passive: false });
52
+ window.addEventListener("touchend", handlePullTouchEnd, { capture: true, passive: true });
53
+ window.addEventListener("touchcancel", handlePullTouchCancel, { capture: true, passive: true });
54
+ });
55
+
56
+ onBeforeUnmount(() => {
57
+ if (typeof window !== "object") {
58
+ return;
59
+ }
60
+ window.removeEventListener("pointerdown", handlePullPointerDown, { capture: true });
61
+ window.removeEventListener("pointermove", handlePullPointerMove, { capture: true });
62
+ window.removeEventListener("pointerup", handlePullPointerEnd, { capture: true });
63
+ window.removeEventListener("pointercancel", handlePullPointerCancel, { capture: true });
64
+ window.removeEventListener("touchstart", handlePullTouchStart, { capture: true });
65
+ window.removeEventListener("touchmove", handlePullTouchMove, { capture: true });
66
+ window.removeEventListener("touchend", handlePullTouchEnd, { capture: true });
67
+ window.removeEventListener("touchcancel", handlePullTouchCancel, { capture: true });
68
+ });
69
+
70
+ function canRefresh() {
71
+ return Boolean(refreshRuntime && typeof refreshRuntime.refresh === "function");
72
+ }
73
+
74
+ function canStartFromTarget(target) {
75
+ return Boolean(
76
+ unref(enabled) &&
77
+ canRefresh() &&
78
+ !pullRefreshing.value &&
79
+ isAtPageTop() &&
80
+ !isShellGestureIgnoredTarget(target, { extraSelectors: PULL_REFRESH_IGNORED_SELECTORS })
81
+ );
82
+ }
83
+
84
+ function handlePullPointerDown(event) {
85
+ if (!isPrimaryTouchPointer(event) || !canStartFromTarget(event.target)) {
86
+ activePull = null;
87
+ return;
88
+ }
89
+
90
+ activePull = {
91
+ pointerId: event.pointerId,
92
+ touchIdentifier: null,
93
+ startX: event.clientX,
94
+ startY: event.clientY
95
+ };
96
+ }
97
+
98
+ function handlePullPointerMove(event) {
99
+ if (!activePull || event.pointerId !== activePull.pointerId) {
100
+ return;
101
+ }
102
+
103
+ updatePullGesture(event.clientX, event.clientY, event);
104
+ }
105
+
106
+ function handlePullPointerEnd(event) {
107
+ if (!activePull || event.pointerId !== activePull.pointerId) {
108
+ return;
109
+ }
110
+
111
+ finishPullGesture();
112
+ }
113
+
114
+ // The browser cancels the pointer when it takes over scrolling; touch events
115
+ // keep following the same finger.
116
+ function handlePullPointerCancel(event) {
117
+ if (!activePull || event.pointerId !== activePull.pointerId) {
118
+ return;
119
+ }
120
+
121
+ activePull.pointerId = null;
122
+ }
123
+
124
+ function handlePullTouchStart(event) {
125
+ if (activePull || event?.touches?.length !== 1 || !canStartFromTarget(event.target)) {
126
+ return;
127
+ }
128
+
129
+ const touch = event.touches[0];
130
+ activePull = {
131
+ pointerId: null,
132
+ touchIdentifier: touch.identifier,
133
+ startX: touch.clientX,
134
+ startY: touch.clientY
135
+ };
136
+ }
137
+
138
+ function handlePullTouchMove(event) {
139
+ const touch = findActiveTouch(event.touches);
140
+ if (!activePull || !touch) {
141
+ return;
142
+ }
143
+
144
+ updatePullGesture(touch.clientX, touch.clientY, event);
145
+ }
146
+
147
+ function handlePullTouchEnd(event) {
148
+ if (!activePull || !touchListIncludesActiveTouch(event.changedTouches)) {
149
+ return;
150
+ }
151
+
152
+ finishPullGesture();
153
+ }
154
+
155
+ function handlePullTouchCancel(event) {
156
+ if (activePull && touchListIncludesActiveTouch(event.changedTouches)) {
157
+ cancelPullRefresh();
158
+ }
159
+ }
160
+
161
+ function updatePullGesture(clientX, clientY, event) {
162
+ if (!activePull) {
163
+ return;
164
+ }
165
+
166
+ const deltaX = clientX - activePull.startX;
167
+ const deltaY = clientY - activePull.startY;
168
+ const absX = Math.abs(deltaX);
169
+
170
+ if (deltaY < -4 || (absX > 24 && absX > deltaY * 1.15)) {
171
+ cancelPullRefresh();
172
+ return;
173
+ }
174
+
175
+ if (deltaY <= 6 || !isAtPageTop()) {
176
+ return;
177
+ }
178
+
179
+ if (event?.cancelable) {
180
+ event.preventDefault();
181
+ }
182
+ pullDistance.value = Math.min(PULL_REFRESH_MAX_DISTANCE, Math.round(deltaY * 0.55));
183
+ }
184
+
185
+ function finishPullGesture() {
186
+ const shouldRefresh = pullDistance.value >= PULL_REFRESH_TRIGGER_DISTANCE;
187
+ activePull = null;
188
+
189
+ if (!shouldRefresh) {
190
+ pullDistance.value = 0;
191
+ return;
192
+ }
193
+
194
+ void refreshFromPullGesture();
195
+ }
196
+
197
+ function cancelPullRefresh() {
198
+ activePull = null;
199
+ if (!pullRefreshing.value) {
200
+ pullDistance.value = 0;
201
+ }
202
+ }
203
+
204
+ async function refreshFromPullGesture() {
205
+ if (!canRefresh() || pullRefreshing.value) {
206
+ pullDistance.value = 0;
207
+ return;
208
+ }
209
+
210
+ pullRefreshing.value = true;
211
+ pullDistance.value = PULL_REFRESH_TRIGGER_DISTANCE;
212
+ try {
213
+ await refreshRuntime.refresh("pull-to-refresh");
214
+ } finally {
215
+ pullRefreshing.value = false;
216
+ pullDistance.value = 0;
217
+ }
218
+ }
219
+
220
+ function findActiveTouch(touchList) {
221
+ if (!activePull || !touchList || touchList.length < 1) {
222
+ return null;
223
+ }
224
+
225
+ if (activePull.touchIdentifier === null && touchList.length === 1) {
226
+ return touchList[0];
227
+ }
228
+
229
+ for (const touch of touchList) {
230
+ if (touch.identifier === activePull.touchIdentifier) {
231
+ return touch;
232
+ }
233
+ }
234
+
235
+ return null;
236
+ }
237
+
238
+ function touchListIncludesActiveTouch(touchList) {
239
+ if (!activePull) {
240
+ return false;
241
+ }
242
+
243
+ if (activePull.touchIdentifier === null) {
244
+ return !touchList || touchList.length <= 1;
245
+ }
246
+
247
+ return Boolean(findActiveTouch(touchList));
248
+ }
249
+
250
+ return {
251
+ pullDistance,
252
+ pullRefreshing,
253
+ pullProgress,
254
+ pullIndicatorVisible,
255
+ pullRefreshLabel,
256
+ pullRefreshStyle
257
+ };
258
+ }
259
+
260
+ export { useShellPullToRefresh };
@@ -1,3 +1,3 @@
1
1
  export { createDefaultErrorPolicy } from "./policy.js";
2
- export { useShellWebErrorRuntime } from "./inject.js";
2
+ export { SHELL_WEB_ERROR_RUNTIME_KEY, useShellWebErrorRuntime } from "./inject.js";
3
3
  export { useShellErrorPresentationStore } from "../stores/useShellErrorPresentationStore.js";
@@ -2,6 +2,8 @@ import {
2
2
  inject
3
3
  } from "vue";
4
4
 
5
+ const SHELL_WEB_ERROR_RUNTIME_KEY = "jskit.shell-web.runtime.web-error.client";
6
+
5
7
  const EMPTY_ERROR_RUNTIME = Object.freeze({
6
8
  report() {
7
9
  return Object.freeze({
@@ -49,7 +51,7 @@ const EMPTY_ERROR_RUNTIME = Object.freeze({
49
51
  });
50
52
 
51
53
  function useShellWebErrorRuntime({ required = false } = {}) {
52
- const runtime = inject("jskit.shell-web.runtime.web-error.client", null);
54
+ const runtime = inject(SHELL_WEB_ERROR_RUNTIME_KEY, null);
53
55
  if (runtime && typeof runtime.report === "function") {
54
56
  return runtime;
55
57
  }
@@ -62,6 +64,7 @@ function useShellWebErrorRuntime({ required = false } = {}) {
62
64
  }
63
65
 
64
66
  export {
67
+ SHELL_WEB_ERROR_RUNTIME_KEY,
65
68
  EMPTY_ERROR_RUNTIME,
66
69
  useShellWebErrorRuntime
67
70
  };
@@ -1,10 +1,7 @@
1
1
  import {
2
- normalizeAction,
3
- normalizeAdditionalActions,
4
2
  normalizeChannel,
5
3
  normalizeErrorIntent,
6
- normalizeSeverity,
7
- normalizeText
4
+ normalizeSeverity
8
5
  } from "./normalize.js";
9
6
 
10
7
  function createDefaultErrorPolicy({
@@ -46,17 +43,11 @@ function createDefaultErrorPolicy({
46
43
  }
47
44
  }
48
45
 
49
- const message = normalizeText(event.userMessage || event.message, "Request failed.");
50
-
46
+ // The runtime owns every other default, including message, actions,
47
+ // presenter, dedupe and persistence.
51
48
  return Object.freeze({
52
49
  channel,
53
- message,
54
- severity: normalizeSeverity(event.severity, normalizedDefaultSeverity),
55
- presenterId: normalizeText(event.presenterId),
56
- action: normalizeAction(event.action),
57
- additionalActions: normalizeAdditionalActions(event.additionalActions),
58
- persist: channel !== "snackbar",
59
- dedupeKey: normalizeText(event.dedupeKey)
50
+ severity: normalizeSeverity(event.severity, normalizedDefaultSeverity)
60
51
  });
61
52
  };
62
53
  }
@@ -3,8 +3,7 @@ import { normalizeText } from "./normalize.js";
3
3
  function createStoreBackedPresenter({
4
4
  id,
5
5
  channel,
6
- store,
7
- defaultPersist = false
6
+ store
8
7
  } = {}) {
9
8
  const normalizedId = normalizeText(id);
10
9
  const normalizedChannel = normalizeText(channel).toLowerCase();
@@ -26,7 +25,6 @@ function createStoreBackedPresenter({
26
25
  present(payload = {}) {
27
26
  return store.present(normalizedChannel, {
28
27
  ...payload,
29
- persist: typeof payload.persist === "boolean" ? payload.persist : defaultPersist,
30
28
  presenterId: normalizedId
31
29
  });
32
30
  },
@@ -40,8 +38,7 @@ function createMaterialSnackbarPresenter({ store } = {}) {
40
38
  return createStoreBackedPresenter({
41
39
  id: "material.snackbar",
42
40
  channel: "snackbar",
43
- store,
44
- defaultPersist: false
41
+ store
45
42
  });
46
43
  }
47
44
 
@@ -49,8 +46,7 @@ function createMaterialBannerPresenter({ store } = {}) {
49
46
  return createStoreBackedPresenter({
50
47
  id: "material.banner",
51
48
  channel: "banner",
52
- store,
53
- defaultPersist: true
49
+ store
54
50
  });
55
51
  }
56
52
 
@@ -58,8 +54,7 @@ function createMaterialDialogPresenter({ store } = {}) {
58
54
  return createStoreBackedPresenter({
59
55
  id: "material.dialog",
60
56
  channel: "dialog",
61
- store,
62
- defaultPersist: true
57
+ store
63
58
  });
64
59
  }
65
60
 
@@ -80,7 +80,7 @@ function createErrorPresentationStore({
80
80
  channel: normalizedChannel,
81
81
  message: normalizeText(payload.message, "Request failed."),
82
82
  severity: normalizeSeverity(payload.severity, "error"),
83
- persist: typeof payload.persist === "boolean" ? payload.persist : normalizedChannel !== "snackbar",
83
+ persist: payload.persist === true,
84
84
  action: normalizeAction(payload.action),
85
85
  additionalActions: normalizeAdditionalActions(payload.additionalActions),
86
86
  presenterId: normalizeText(payload.presenterId),
@@ -6,26 +6,19 @@ import {
6
6
  resolveSurfaceDefinitionFromPlacementContext,
7
7
  resolveSurfaceRootPathFromPlacementContext
8
8
  } from "../placement/surfaceContext.js";
9
-
10
- function normalizeParamsMap(params = null) {
11
- if (!params || typeof params !== "object" || Array.isArray(params)) {
12
- return {};
13
- }
14
- return params;
15
- }
9
+ import { normalizeRouteParams } from "../support/routeParams.js";
16
10
 
17
11
  function materializeSurfaceRouteBase(routeBaseTemplate = "/", { params = {}, strictParams = true, surface = "" } = {}) {
18
- const normalizedParams = normalizeParamsMap(params);
12
+ const normalizedParams = normalizeRouteParams(params);
19
13
  const missingParams = new Set();
20
14
  const outputPath = String(routeBaseTemplate || "/").replace(/:([A-Za-z0-9_]+)/g, (_full, rawName) => {
21
15
  const paramName = String(rawName || "").trim();
22
16
  const paramValue = normalizedParams[paramName];
23
- const normalizedValue = String(paramValue ?? "").trim();
24
- if (!normalizedValue) {
17
+ if (!paramValue) {
25
18
  missingParams.add(paramName);
26
19
  return `:${paramName}`;
27
20
  }
28
- return encodeURIComponent(normalizedValue);
21
+ return encodeURIComponent(paramValue);
29
22
  });
30
23
 
31
24
  if (strictParams && missingParams.size > 0) {
@@ -94,23 +87,8 @@ function useShellLinkResolver({ surface = "" } = {}) {
94
87
  });
95
88
  }
96
89
 
97
- function toSurface(relativePath = "/", options = {}) {
98
- return resolve(relativePath, options);
99
- }
100
-
101
- function toWorkspace(relativePath = "/", options = {}) {
102
- return resolve(relativePath, options);
103
- }
104
-
105
- function toAuto(relativePath = "/", options = {}) {
106
- return resolve(relativePath, options);
107
- }
108
-
109
90
  return Object.freeze({
110
- resolve,
111
- toSurface,
112
- toWorkspace,
113
- toAuto
91
+ resolve
114
92
  });
115
93
  }
116
94
 
@@ -4,6 +4,7 @@ import { resolveScopedApiBasePath } from "@jskit-ai/kernel/shared/surface";
4
4
  import { resolveSurfaceDefinitionFromPlacementContext } from "../placement/index.js";
5
5
  import { useShellLinkResolver } from "./linkResolver.js";
6
6
  import { useSurfaceRouteContext } from "./useSurfaceRouteContext.js";
7
+ import { normalizeRouteParams } from "../support/routeParams.js";
7
8
 
8
9
  function normalizePathSuffix(value = "") {
9
10
  const raw = normalizeText(unref(value));
@@ -32,27 +33,6 @@ function resolveDefaultSurfaceIdFromPlacementContext(placementContext = null) {
32
33
  return resolveSurfaceId(placementContext?.surfaceConfig?.defaultSurfaceId, "");
33
34
  }
34
35
 
35
- function normalizeRouteParams(params = null) {
36
- if (!params || typeof params !== "object" || Array.isArray(params)) {
37
- return {};
38
- }
39
-
40
- const output = {};
41
- for (const [rawKey, rawValue] of Object.entries(params)) {
42
- const key = normalizeText(rawKey);
43
- if (!key) {
44
- continue;
45
- }
46
-
47
- const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
48
- const normalizedValue = normalizeText(value);
49
- if (normalizedValue) {
50
- output[key] = normalizedValue;
51
- }
52
- }
53
- return output;
54
- }
55
-
56
36
  function resolveRouteParams(baseParams = {}, overrideParams = null) {
57
37
  return {
58
38
  ...normalizeRouteParams(baseParams),
@@ -8,6 +8,7 @@ export {
8
8
  } from "./validators.js";
9
9
 
10
10
  export {
11
+ SHELL_WEB_PLACEMENT_RUNTIME_KEY,
11
12
  useWebPlacementContext
12
13
  } from "./inject.js";
13
14
 
@@ -17,6 +18,7 @@ export {
17
18
 
18
19
  export {
19
20
  readPlacementSurfaceConfig,
21
+ resolveAllowedOriginsFromPlacementContext,
20
22
  resolveSurfaceDefinitionFromPlacementContext,
21
23
  joinSurfacePath,
22
24
  resolveSurfaceIdFromPlacementPathname,
@@ -5,6 +5,8 @@ import {
5
5
  shallowRef
6
6
  } from "vue";
7
7
 
8
+ const SHELL_WEB_PLACEMENT_RUNTIME_KEY = "jskit.shell-web.runtime.web-placement.client";
9
+
8
10
  const EMPTY_WEB_PLACEMENT_RUNTIME = Object.freeze({
9
11
  getContext() {
10
12
  return Object.freeze({});
@@ -36,7 +38,7 @@ const EMPTY_WEB_PLACEMENT_CONTEXT = Object.freeze({
36
38
  });
37
39
 
38
40
  function useWebPlacementRuntime({ required = false } = {}) {
39
- const runtime = inject("jskit.shell-web.runtime.web-placement.client", null);
41
+ const runtime = inject(SHELL_WEB_PLACEMENT_RUNTIME_KEY, null);
40
42
  if (runtime && typeof runtime.getPlacements === "function") {
41
43
  return runtime;
42
44
  }
@@ -96,6 +98,7 @@ function useWebPlacementContext({ required = false } = {}) {
96
98
  }
97
99
 
98
100
  export {
101
+ SHELL_WEB_PLACEMENT_RUNTIME_KEY,
99
102
  EMPTY_WEB_PLACEMENT_RUNTIME,
100
103
  EMPTY_WEB_PLACEMENT_CONTEXT,
101
104
  useWebPlacementRuntime,
@@ -2,6 +2,7 @@ import { DEFAULT_DEBUG_DEPTH, explodePayload } from "./debug.js";
2
2
  import { createListenerSubscription } from "@jskit-ai/kernel/shared/support/listenerSet";
3
3
  import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
4
4
  import {
5
+ WEB_PLACEMENT_SURFACE_ANY,
5
6
  normalizePlacementLayoutClass,
6
7
  normalizePlacementTopologyDefinition
7
8
  } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
@@ -24,7 +25,6 @@ function ensureArray(value) {
24
25
 
25
26
  const PLACEMENT_DEBUG_PREFIX = "[placement-debug]";
26
27
  const PLACEMENT_DEBUG_FLAG = "__JSKIT_PLACEMENT_DEBUG__";
27
- const WEB_PLACEMENT_SURFACE_ANY = "*";
28
28
  const NOOP = () => {};
29
29
 
30
30
  function isPlacementDebugEnabled() {
@@ -480,9 +480,7 @@ function createWebPlacementRuntime({ components, contextContributors = [], logge
480
480
  if (!renderablePlacement) {
481
481
  continue;
482
482
  }
483
- const placementSurfaces = Array.isArray(renderablePlacement.surfaces)
484
- ? renderablePlacement.surfaces
485
- : [WEB_PLACEMENT_SURFACE_ANY];
483
+ const placementSurfaces = renderablePlacement.surfaces;
486
484
 
487
485
  if (!matchesSurface(placementSurfaces, normalizedSurface)) {
488
486
  debugLog("getPlacements:skip-surfaces", {
@@ -7,6 +7,7 @@ import {
7
7
  import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
8
8
  import { normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
9
9
  import { isExternalLinkTarget, splitPathQueryHash } from "@jskit-ai/kernel/shared/support/linkPath";
10
+ import { resolveAllowedOriginsFromSurfaceDefinitions } from "@jskit-ai/kernel/shared/support/returnToPath";
10
11
 
11
12
  const EMPTY_SURFACE_CONFIG = Object.freeze({
12
13
  tenancyMode: "",
@@ -119,6 +120,14 @@ function readPlacementSurfaceConfig(contextValue = null) {
119
120
  return normalizeSurfaceConfig(contextRecord.surfaceConfig);
120
121
  }
121
122
 
123
+ // Origins a return-to URL may point at: this page plus every surface origin.
124
+ function resolveAllowedOriginsFromPlacementContext(contextValue = null) {
125
+ const seedOrigins = typeof window === "object" && window?.location?.origin ? [window.location.origin] : [];
126
+ return resolveAllowedOriginsFromSurfaceDefinitions(readPlacementSurfaceConfig(contextValue).surfacesById, {
127
+ seedOrigins
128
+ });
129
+ }
130
+
122
131
  function resolveSurfaceDefinitionFromPlacementContext(contextValue = null, surfaceId = "") {
123
132
  const surfaceConfig = readPlacementSurfaceConfig(contextValue);
124
133
  const normalizedSurfaceId = normalizeSurfaceId(surfaceId);
@@ -253,14 +262,7 @@ function createPlacementSurfacePathHelpers(surfaceConfig = EMPTY_SURFACE_CONFIG)
253
262
 
254
263
  return createSurfacePathHelpers({
255
264
  defaultSurfaceId: resolvedDefaultSurfaceId,
256
- normalizeSurfaceId,
257
- resolveSurfaceRouteBase(surfaceId) {
258
- const normalizedSurfaceId = normalizeSurfaceId(surfaceId);
259
- return String(surfacesById[normalizedSurfaceId]?.routeBase || "/").trim() || "/";
260
- },
261
- listSurfaceDefinitions() {
262
- return Object.values(surfacesById);
263
- }
265
+ surfaceDefinitions: Object.values(surfacesById)
264
266
  });
265
267
  }
266
268
 
@@ -280,6 +282,7 @@ export {
280
282
  EMPTY_SURFACE_CONFIG,
281
283
  buildSurfaceConfigContext,
282
284
  readPlacementSurfaceConfig,
285
+ resolveAllowedOriginsFromPlacementContext,
283
286
  resolveSurfaceDefinitionFromPlacementContext,
284
287
  joinSurfacePath,
285
288
  resolveSurfaceIdFromPlacementPathname,