@jskit-ai/shell-web 0.1.31 → 0.1.33
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.
- package/package.descriptor.mjs +114 -15
- package/package.json +8 -2
- package/src/client/components/ShellLayout.vue +11 -4
- package/src/client/components/ShellMenuLinkItem.vue +71 -0
- package/src/client/components/ShellOutlet.vue +10 -7
- package/src/client/components/ShellOutletMenuWidget.vue +57 -0
- package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +116 -0
- package/src/client/components/ShellTabLinkItem.vue +128 -0
- package/src/client/index.js +4 -0
- package/src/client/lib/menuIcons.js +210 -0
- package/src/client/placement/runtime.js +22 -22
- package/src/client/placement/validators.js +19 -49
- package/src/client/support/menuLinkTarget.js +97 -0
- package/src/server/support/localLinkItemScaffolds.js +80 -0
- package/templates/expected-existing/src/App.vue +13 -0
- package/templates/expected-existing/src/pages/console/index.vue +12 -0
- package/templates/expected-existing/src/pages/console.vue +13 -0
- package/templates/expected-existing/src/pages/home/index.vue +12 -0
- package/templates/expected-existing/src/pages/home.vue +13 -0
- package/templates/src/components/ShellLayout.vue +11 -4
- package/templates/src/components/menus/MenuLinkItem.vue +30 -0
- package/templates/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
- package/templates/src/components/menus/TabLinkItem.vue +34 -0
- package/templates/src/pages/home/settings/index.vue +8 -8
- package/templates/src/pages/home/settings.vue +4 -1
- package/test/bootstrapClaimContract.test.js +88 -0
- package/test/linkItemScaffoldContract.test.js +209 -0
- package/test/outletMenuWidgetContract.test.js +33 -0
- package/test/placementRegistry.test.js +17 -6
- package/test/placementRuntime.test.js +59 -44
- package/test/settingsPlacementContract.test.js +16 -5
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useRoute } from "vue-router";
|
|
4
|
+
import { resolveShellLinkPath } from "../navigation/linkResolver.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveSurfaceIdFromPlacementPathname,
|
|
7
|
+
resolveSurfaceNavigationTargetFromPlacementContext,
|
|
8
|
+
useWebPlacementContext
|
|
9
|
+
} from "../placement/index.js";
|
|
10
|
+
import {
|
|
11
|
+
normalizeMenuLinkPathname,
|
|
12
|
+
resolveMenuLinkTarget
|
|
13
|
+
} from "../support/menuLinkTarget.js";
|
|
14
|
+
|
|
15
|
+
const props = defineProps({
|
|
16
|
+
label: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: ""
|
|
19
|
+
},
|
|
20
|
+
to: {
|
|
21
|
+
type: String,
|
|
22
|
+
default: ""
|
|
23
|
+
},
|
|
24
|
+
surface: {
|
|
25
|
+
type: String,
|
|
26
|
+
default: ""
|
|
27
|
+
},
|
|
28
|
+
workspaceSuffix: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: "/"
|
|
31
|
+
},
|
|
32
|
+
nonWorkspaceSuffix: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: "/"
|
|
35
|
+
},
|
|
36
|
+
disabled: {
|
|
37
|
+
type: Boolean,
|
|
38
|
+
default: false
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const route = useRoute();
|
|
43
|
+
const { context: placementContext } = useWebPlacementContext();
|
|
44
|
+
|
|
45
|
+
const currentSurfaceId = computed(() => {
|
|
46
|
+
return resolveSurfaceIdFromPlacementPathname(
|
|
47
|
+
placementContext.value,
|
|
48
|
+
String(route?.path || route?.fullPath || "").trim()
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const resolvedTo = computed(() => {
|
|
53
|
+
return resolveMenuLinkTarget({
|
|
54
|
+
to: props.to,
|
|
55
|
+
surface: props.surface,
|
|
56
|
+
currentSurfaceId: currentSurfaceId.value,
|
|
57
|
+
placementContext: placementContext.value,
|
|
58
|
+
workspaceSuffix: props.workspaceSuffix,
|
|
59
|
+
nonWorkspaceSuffix: props.nonWorkspaceSuffix,
|
|
60
|
+
routeParams: route.params || {},
|
|
61
|
+
resolvePagePath(relativePath, options = {}) {
|
|
62
|
+
return resolveShellLinkPath({
|
|
63
|
+
context: placementContext.value,
|
|
64
|
+
surface: options.surface,
|
|
65
|
+
relativePath,
|
|
66
|
+
params: route.params || {},
|
|
67
|
+
strictParams: options.strictParams !== false
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const resolvedTarget = computed(() => {
|
|
74
|
+
const target = String(resolvedTo.value || "").trim();
|
|
75
|
+
if (!target) {
|
|
76
|
+
return {
|
|
77
|
+
href: "",
|
|
78
|
+
sameOrigin: true
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
|
|
83
|
+
path: target
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
href: navigationTarget.href,
|
|
87
|
+
sameOrigin: navigationTarget.sameOrigin
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const isActive = computed(() => {
|
|
92
|
+
if (!resolvedTarget.value.sameOrigin) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const targetPath = normalizeMenuLinkPathname(resolvedTarget.value.href);
|
|
97
|
+
const currentPath = normalizeMenuLinkPathname(route.fullPath || route.path || "");
|
|
98
|
+
if (!targetPath || !currentPath) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
|
|
103
|
+
});
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<v-btn
|
|
108
|
+
v-if="resolvedTarget.href"
|
|
109
|
+
class="tab-link-item"
|
|
110
|
+
variant="text"
|
|
111
|
+
size="small"
|
|
112
|
+
:to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
|
|
113
|
+
:href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
|
|
114
|
+
:active="isActive"
|
|
115
|
+
:disabled="disabled"
|
|
116
|
+
color="primary"
|
|
117
|
+
>
|
|
118
|
+
{{ label || "Tab" }}
|
|
119
|
+
</v-btn>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<style scoped>
|
|
123
|
+
.tab-link-item {
|
|
124
|
+
text-transform: none;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
border-radius: 999px;
|
|
127
|
+
}
|
|
128
|
+
</style>
|
package/src/client/index.js
CHANGED
|
@@ -8,7 +8,11 @@ export {
|
|
|
8
8
|
|
|
9
9
|
export { default as ShellLayout } from "./components/ShellLayout.vue";
|
|
10
10
|
export { default as ShellOutlet } from "./components/ShellOutlet.vue";
|
|
11
|
+
export { default as ShellOutletMenuWidget } from "./components/ShellOutletMenuWidget.vue";
|
|
11
12
|
export { default as ShellErrorHost } from "./components/ShellErrorHost.vue";
|
|
13
|
+
export { default as ShellMenuLinkItem } from "./components/ShellMenuLinkItem.vue";
|
|
14
|
+
export { default as ShellSurfaceAwareMenuLinkItem } from "./components/ShellSurfaceAwareMenuLinkItem.vue";
|
|
15
|
+
export { default as ShellTabLinkItem } from "./components/ShellTabLinkItem.vue";
|
|
12
16
|
export { useShellLayoutState } from "./composables/useShellLayoutState.js";
|
|
13
17
|
|
|
14
18
|
const clientProviders = Object.freeze([ShellWebClientProvider]);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as mdiIcons from "@mdi/js";
|
|
2
|
+
import {
|
|
3
|
+
mdiAccountCircleOutline,
|
|
4
|
+
mdiAccountCogOutline,
|
|
5
|
+
mdiAccountGroupOutline,
|
|
6
|
+
mdiArrowRightCircleOutline,
|
|
7
|
+
mdiClipboardListOutline,
|
|
8
|
+
mdiCogOutline,
|
|
9
|
+
mdiConsoleNetworkOutline,
|
|
10
|
+
mdiFolderOutline,
|
|
11
|
+
mdiHomeVariantOutline,
|
|
12
|
+
mdiLogin,
|
|
13
|
+
mdiLogout,
|
|
14
|
+
mdiRobotOutline,
|
|
15
|
+
mdiShieldCrownOutline,
|
|
16
|
+
mdiViewDashboardOutline,
|
|
17
|
+
mdiViewListOutline
|
|
18
|
+
} from "@mdi/js";
|
|
19
|
+
import { isExternalLinkTarget, splitPathQueryHash } from "@jskit-ai/kernel/shared/support/linkPath";
|
|
20
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
21
|
+
import { normalizePathname as normalizeKernelPathname } from "@jskit-ai/kernel/shared/surface/paths";
|
|
22
|
+
|
|
23
|
+
const SURFACE_SWITCH_ICON_BY_ID = Object.freeze({
|
|
24
|
+
home: mdiHomeVariantOutline,
|
|
25
|
+
app: mdiViewDashboardOutline,
|
|
26
|
+
admin: mdiShieldCrownOutline,
|
|
27
|
+
console: mdiConsoleNetworkOutline
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function resolveExplicitIconValue(explicitIcon = "") {
|
|
31
|
+
const normalizedExplicitIcon = normalizeText(explicitIcon);
|
|
32
|
+
if (!normalizedExplicitIcon) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!normalizedExplicitIcon.startsWith("mdi-")) {
|
|
37
|
+
return normalizedExplicitIcon;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const iconKey = normalizedExplicitIcon
|
|
41
|
+
.slice("mdi-".length)
|
|
42
|
+
.split("-")
|
|
43
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
44
|
+
.join("");
|
|
45
|
+
const exportName = `mdi${iconKey}`;
|
|
46
|
+
const resolvedIcon = mdiIcons[exportName];
|
|
47
|
+
return typeof resolvedIcon === "string" && resolvedIcon ? resolvedIcon : normalizedExplicitIcon;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizePathname(value = "") {
|
|
51
|
+
const normalizedValue = normalizeText(value);
|
|
52
|
+
if (!normalizedValue) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isExternalLinkTarget(normalizedValue)) {
|
|
57
|
+
const isHttpTarget = normalizedValue.startsWith("http://") || normalizedValue.startsWith("https://");
|
|
58
|
+
if (!isHttpTarget) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let parsedPathname = "";
|
|
63
|
+
try {
|
|
64
|
+
parsedPathname = String(new URL(normalizedValue).pathname || "");
|
|
65
|
+
} catch {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const normalizedPathname = normalizeText(parsedPathname);
|
|
70
|
+
if (!normalizedPathname) {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
return normalizeKernelPathname(normalizedPathname).toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { pathname } = splitPathQueryHash(normalizedValue);
|
|
77
|
+
const normalizedPathname = normalizeText(pathname);
|
|
78
|
+
if (!normalizedPathname) {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return normalizeKernelPathname(normalizedPathname).toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveSurfaceSwitchIdFromLabel(label = "") {
|
|
86
|
+
const normalizedLabel = normalizeText(label).toLowerCase();
|
|
87
|
+
if (!normalizedLabel.startsWith("go to ")) {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
return normalizeText(normalizedLabel.slice("go to ".length));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveSurfaceSwitchIcon(surfaceId = "", explicitIcon = "") {
|
|
94
|
+
const resolvedExplicitIcon = resolveExplicitIconValue(explicitIcon);
|
|
95
|
+
if (resolvedExplicitIcon) {
|
|
96
|
+
return resolvedExplicitIcon;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const normalizedSurfaceId = normalizeText(surfaceId).toLowerCase();
|
|
100
|
+
return SURFACE_SWITCH_ICON_BY_ID[normalizedSurfaceId] || mdiArrowRightCircleOutline;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveMenuLinkIcon({ icon = "", label = "", to = "" } = {}) {
|
|
104
|
+
const resolvedExplicitIcon = resolveExplicitIconValue(icon);
|
|
105
|
+
if (resolvedExplicitIcon) {
|
|
106
|
+
return resolvedExplicitIcon;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const normalizedLabel = normalizeText(label).toLowerCase();
|
|
110
|
+
const normalizedPathname = normalizePathname(to);
|
|
111
|
+
if (!normalizedLabel && !normalizedPathname) {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const surfaceSwitchSurfaceId = resolveSurfaceSwitchIdFromLabel(normalizedLabel);
|
|
116
|
+
if (surfaceSwitchSurfaceId) {
|
|
117
|
+
return resolveSurfaceSwitchIcon(surfaceSwitchSurfaceId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
normalizedLabel.includes("sign in") ||
|
|
122
|
+
normalizedPathname.includes("/auth/login")
|
|
123
|
+
) {
|
|
124
|
+
return mdiLogin;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
normalizedLabel.includes("sign out") ||
|
|
129
|
+
normalizedPathname.includes("/auth/signout")
|
|
130
|
+
) {
|
|
131
|
+
return mdiLogout;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
normalizedLabel.includes("account") ||
|
|
136
|
+
normalizedPathname.includes("/account")
|
|
137
|
+
) {
|
|
138
|
+
if (
|
|
139
|
+
normalizedLabel.includes("settings") ||
|
|
140
|
+
normalizedPathname.includes("/settings") ||
|
|
141
|
+
normalizedPathname === "/account"
|
|
142
|
+
) {
|
|
143
|
+
return mdiAccountCogOutline;
|
|
144
|
+
}
|
|
145
|
+
return mdiAccountCircleOutline;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
normalizedLabel.includes("members") ||
|
|
150
|
+
normalizedLabel.includes("team") ||
|
|
151
|
+
normalizedPathname.includes("/members")
|
|
152
|
+
) {
|
|
153
|
+
return mdiAccountGroupOutline;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (normalizedLabel.includes("assistant") || normalizedPathname.includes("/assistant")) {
|
|
157
|
+
return mdiRobotOutline;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
normalizedLabel.includes("console") ||
|
|
162
|
+
normalizedPathname.startsWith("/console")
|
|
163
|
+
) {
|
|
164
|
+
return mdiConsoleNetworkOutline;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
normalizedLabel.includes("admin") ||
|
|
169
|
+
normalizedPathname.includes("/admin")
|
|
170
|
+
) {
|
|
171
|
+
return mdiShieldCrownOutline;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (normalizedLabel.includes("settings") || normalizedPathname.includes("/settings")) {
|
|
175
|
+
return mdiCogOutline;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
normalizedLabel.includes("home") ||
|
|
180
|
+
normalizedPathname === "/"
|
|
181
|
+
) {
|
|
182
|
+
return mdiHomeVariantOutline;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
normalizedLabel.includes("workspace") ||
|
|
187
|
+
normalizedLabel.includes("dashboard") ||
|
|
188
|
+
normalizedPathname.includes("/w/")
|
|
189
|
+
) {
|
|
190
|
+
return mdiViewDashboardOutline;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (normalizedPathname) {
|
|
194
|
+
const segments = normalizedPathname.split("/").filter(Boolean);
|
|
195
|
+
if (segments.length === 1) {
|
|
196
|
+
return mdiFolderOutline;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (normalizedLabel.includes("list")) {
|
|
201
|
+
return mdiClipboardListOutline;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return mdiViewListOutline;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export {
|
|
208
|
+
resolveMenuLinkIcon,
|
|
209
|
+
resolveSurfaceSwitchIcon
|
|
210
|
+
};
|
|
@@ -4,8 +4,7 @@ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
|
4
4
|
import {
|
|
5
5
|
isRenderableComponent,
|
|
6
6
|
normalizePlacementDefinition,
|
|
7
|
-
|
|
8
|
-
normalizePlacementPosition,
|
|
7
|
+
normalizePlacementTarget,
|
|
9
8
|
normalizeSurface
|
|
10
9
|
} from "./validators.js";
|
|
11
10
|
|
|
@@ -86,13 +85,19 @@ function normalizePlacementList(placements, context = {}) {
|
|
|
86
85
|
byId.set(placement.id, placement);
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
return [...byId.values()]
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
return [...byId.values()]
|
|
89
|
+
.map((placement, index) => ({
|
|
90
|
+
placement,
|
|
91
|
+
index
|
|
92
|
+
}))
|
|
93
|
+
.sort((left, right) => {
|
|
94
|
+
const orderCompare = left.placement.order - right.placement.order;
|
|
95
|
+
if (orderCompare !== 0) {
|
|
96
|
+
return orderCompare;
|
|
97
|
+
}
|
|
98
|
+
return left.index - right.index;
|
|
99
|
+
})
|
|
100
|
+
.map((entry) => entry.placement);
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
function matchesSurface(placementSurfaces, requestedSurface) {
|
|
@@ -303,10 +308,9 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
|
|
|
303
308
|
return revision;
|
|
304
309
|
}
|
|
305
310
|
|
|
306
|
-
function getPlacements({ surface = WEB_PLACEMENT_SURFACE_ANY,
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
if (!normalizedHost || !normalizedPosition) {
|
|
311
|
+
function getPlacements({ surface = WEB_PLACEMENT_SURFACE_ANY, target = "", context = {} } = {}) {
|
|
312
|
+
const normalizedTarget = normalizePlacementTarget(target, { strict: false });
|
|
313
|
+
if (!normalizedTarget) {
|
|
310
314
|
return Object.freeze([]);
|
|
311
315
|
}
|
|
312
316
|
|
|
@@ -318,8 +322,7 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
|
|
|
318
322
|
{
|
|
319
323
|
app,
|
|
320
324
|
surface: normalizedSurface,
|
|
321
|
-
|
|
322
|
-
position: normalizedPosition,
|
|
325
|
+
target: normalizedTarget,
|
|
323
326
|
context: {
|
|
324
327
|
...contextFromRuntime,
|
|
325
328
|
...baseContext
|
|
@@ -333,14 +336,12 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
|
|
|
333
336
|
...baseContext,
|
|
334
337
|
app,
|
|
335
338
|
surface: normalizedSurface,
|
|
336
|
-
|
|
337
|
-
position: normalizedPosition
|
|
339
|
+
target: normalizedTarget
|
|
338
340
|
};
|
|
339
341
|
|
|
340
342
|
debugLog("getPlacements:start", {
|
|
341
343
|
surface: normalizedSurface,
|
|
342
|
-
|
|
343
|
-
position: normalizedPosition,
|
|
344
|
+
target: normalizedTarget,
|
|
344
345
|
contextKeys: Object.keys(baseContext),
|
|
345
346
|
sharedContextKeys: Object.keys(contextFromRuntime),
|
|
346
347
|
placementCount: placementDefinitions.length
|
|
@@ -348,7 +349,7 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
|
|
|
348
349
|
|
|
349
350
|
const matches = [];
|
|
350
351
|
for (const placement of placementDefinitions) {
|
|
351
|
-
if (placement.
|
|
352
|
+
if (placement.target !== normalizedTarget) {
|
|
352
353
|
continue;
|
|
353
354
|
}
|
|
354
355
|
const placementSurfaces = Array.isArray(placement.surfaces)
|
|
@@ -403,8 +404,7 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
|
|
|
403
404
|
|
|
404
405
|
debugLog("getPlacements:done", {
|
|
405
406
|
surface: normalizedSurface,
|
|
406
|
-
|
|
407
|
-
position: normalizedPosition,
|
|
407
|
+
target: normalizedTarget,
|
|
408
408
|
resultIds: matches.map((entry) => entry.id)
|
|
409
409
|
});
|
|
410
410
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRecord, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeShellOutletTargetId } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
|
|
2
3
|
|
|
3
4
|
const WEB_PLACEMENT_SURFACE_ANY = "*";
|
|
4
5
|
|
|
@@ -24,10 +25,6 @@ function isValidSurfaceIdToken(value = "") {
|
|
|
24
25
|
return /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(value);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function isValidPlacementHostOrPositionToken(value = "") {
|
|
28
|
-
return /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(value);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
28
|
function normalizePlacementSurface(value, { strict = false, source = "placement" } = {}) {
|
|
32
29
|
const normalized = normalizeText(value).toLowerCase();
|
|
33
30
|
if (!normalized) {
|
|
@@ -59,40 +56,16 @@ function toInteger(value, fallback = 1000) {
|
|
|
59
56
|
return Math.trunc(numeric);
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
function
|
|
63
|
-
const normalized =
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
throw new TypeError(`${source} requires host.`);
|
|
67
|
-
}
|
|
68
|
-
return "";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!isValidPlacementHostOrPositionToken(normalized)) {
|
|
72
|
-
if (strict) {
|
|
73
|
-
throw new TypeError(`${source} host "${normalized}" is invalid.`);
|
|
74
|
-
}
|
|
75
|
-
return "";
|
|
76
|
-
}
|
|
77
|
-
return normalized;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function normalizePlacementPosition(value, { strict = false, source = "placement" } = {}) {
|
|
81
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
82
|
-
if (!normalized) {
|
|
83
|
-
if (strict) {
|
|
84
|
-
throw new TypeError(`${source} requires position.`);
|
|
85
|
-
}
|
|
86
|
-
return "";
|
|
59
|
+
function normalizePlacementTarget(value, { strict = false, source = "placement" } = {}) {
|
|
60
|
+
const normalized = normalizeShellOutletTargetId(String(value || "").toLowerCase());
|
|
61
|
+
if (normalized) {
|
|
62
|
+
return normalized;
|
|
87
63
|
}
|
|
88
64
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
throw new TypeError(`${source} position "${normalized}" is invalid.`);
|
|
92
|
-
}
|
|
93
|
-
return "";
|
|
65
|
+
if (strict) {
|
|
66
|
+
throw new TypeError(`${source} requires target in "host:position" format.`);
|
|
94
67
|
}
|
|
95
|
-
return
|
|
68
|
+
return "";
|
|
96
69
|
}
|
|
97
70
|
|
|
98
71
|
function normalizePlacementSurfaces(value, { strict = false, source = "placement" } = {}) {
|
|
@@ -136,27 +109,26 @@ function normalizePlacementDefinition(value, { strict = false, source = "placeme
|
|
|
136
109
|
return null;
|
|
137
110
|
}
|
|
138
111
|
|
|
139
|
-
|
|
140
|
-
if (!id) {
|
|
112
|
+
if (Object.hasOwn(value, "host") || Object.hasOwn(value, "position")) {
|
|
141
113
|
if (strict) {
|
|
142
|
-
throw new TypeError(`${source}
|
|
114
|
+
throw new TypeError(`${source} must use "target" only. Legacy "host" and "position" fields are not supported.`);
|
|
143
115
|
}
|
|
144
116
|
return null;
|
|
145
117
|
}
|
|
146
118
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
119
|
+
const id = normalizeText(value.id);
|
|
120
|
+
if (!id) {
|
|
121
|
+
if (strict) {
|
|
122
|
+
throw new TypeError(`${source} requires id.`);
|
|
123
|
+
}
|
|
152
124
|
return null;
|
|
153
125
|
}
|
|
154
126
|
|
|
155
|
-
const
|
|
127
|
+
const target = normalizePlacementTarget(value.target, {
|
|
156
128
|
strict,
|
|
157
129
|
source: `${source} "${id}"`
|
|
158
130
|
});
|
|
159
|
-
if (!
|
|
131
|
+
if (!target) {
|
|
160
132
|
return null;
|
|
161
133
|
}
|
|
162
134
|
|
|
@@ -177,8 +149,7 @@ function normalizePlacementDefinition(value, { strict = false, source = "placeme
|
|
|
177
149
|
|
|
178
150
|
return Object.freeze({
|
|
179
151
|
id,
|
|
180
|
-
|
|
181
|
-
position,
|
|
152
|
+
target,
|
|
182
153
|
surfaces,
|
|
183
154
|
order: toInteger(value.order, 1000),
|
|
184
155
|
componentToken,
|
|
@@ -200,8 +171,7 @@ export {
|
|
|
200
171
|
isRenderableComponent,
|
|
201
172
|
normalizeSurface,
|
|
202
173
|
normalizePlacementSurface,
|
|
203
|
-
|
|
204
|
-
normalizePlacementPosition,
|
|
174
|
+
normalizePlacementTarget,
|
|
205
175
|
normalizePlacementSurfaces,
|
|
206
176
|
normalizePlacementDefinition,
|
|
207
177
|
definePlacement
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { resolveSurfaceDefinitionFromPlacementContext } from "../placement/surfaceContext.js";
|
|
3
|
+
|
|
4
|
+
function normalizeMenuLinkPathname(pathname = "") {
|
|
5
|
+
const source = String(pathname || "").trim();
|
|
6
|
+
if (!source) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const queryIndex = source.indexOf("?");
|
|
11
|
+
const hashIndex = source.indexOf("#");
|
|
12
|
+
const cutoff =
|
|
13
|
+
queryIndex < 0
|
|
14
|
+
? hashIndex
|
|
15
|
+
: hashIndex < 0
|
|
16
|
+
? queryIndex
|
|
17
|
+
: Math.min(queryIndex, hashIndex);
|
|
18
|
+
|
|
19
|
+
return cutoff < 0 ? source : source.slice(0, cutoff);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveMenuLinkSurfaceId(surface = "", fallbackSurfaceId = "") {
|
|
23
|
+
const explicitSurface = normalizeText(surface).toLowerCase();
|
|
24
|
+
if (explicitSurface && explicitSurface !== "*") {
|
|
25
|
+
return explicitSurface;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return normalizeText(fallbackSurfaceId).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function interpolateBracketParams(pathTemplate = "", params = {}) {
|
|
32
|
+
const source = String(pathTemplate || "").trim();
|
|
33
|
+
if (!source) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return source.replace(/\[([^\]]+)\]/g, (_match, rawKey) => {
|
|
38
|
+
const key = String(rawKey || "").trim();
|
|
39
|
+
if (!key) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const value = params?.[key];
|
|
44
|
+
return value == null ? `[${key}]` : encodeURIComponent(String(value));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRelativeMenuLinkTarget(target = "") {
|
|
49
|
+
const normalizedTarget = normalizeText(target);
|
|
50
|
+
return normalizedTarget.startsWith("./") || normalizedTarget.startsWith("../");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function surfaceRequiresWorkspaceFromPlacementContext(contextValue = null, surfaceId = "") {
|
|
54
|
+
return Boolean(resolveSurfaceDefinitionFromPlacementContext(contextValue, surfaceId)?.requiresWorkspace);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveMenuLinkTarget({
|
|
58
|
+
to = "",
|
|
59
|
+
surface = "",
|
|
60
|
+
currentSurfaceId = "",
|
|
61
|
+
placementContext = null,
|
|
62
|
+
workspaceSuffix = "/",
|
|
63
|
+
nonWorkspaceSuffix = "/",
|
|
64
|
+
routeParams = {},
|
|
65
|
+
resolvePagePath = null
|
|
66
|
+
} = {}) {
|
|
67
|
+
const explicitTarget = normalizeText(to);
|
|
68
|
+
const targetSurfaceId = resolveMenuLinkSurfaceId(surface, currentSurfaceId);
|
|
69
|
+
const workspaceRequired = surfaceRequiresWorkspaceFromPlacementContext(placementContext, targetSurfaceId);
|
|
70
|
+
const suffixTemplate = normalizeText(workspaceRequired ? workspaceSuffix : nonWorkspaceSuffix) || "/";
|
|
71
|
+
const interpolatedSuffix = interpolateBracketParams(suffixTemplate, routeParams);
|
|
72
|
+
const resolvedSuffixTarget =
|
|
73
|
+
typeof resolvePagePath === "function" &&
|
|
74
|
+
targetSurfaceId &&
|
|
75
|
+
interpolatedSuffix &&
|
|
76
|
+
!interpolatedSuffix.includes("[")
|
|
77
|
+
? normalizeText(resolvePagePath(interpolatedSuffix, {
|
|
78
|
+
surface: targetSurfaceId,
|
|
79
|
+
mode: "auto"
|
|
80
|
+
}))
|
|
81
|
+
: "";
|
|
82
|
+
|
|
83
|
+
if (!explicitTarget) {
|
|
84
|
+
return resolvedSuffixTarget;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (isRelativeMenuLinkTarget(explicitTarget)) {
|
|
88
|
+
return resolvedSuffixTarget;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return explicitTarget;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
normalizeMenuLinkPathname,
|
|
96
|
+
resolveMenuLinkTarget
|
|
97
|
+
};
|