@jskit-ai/shell-web 0.1.32 → 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.
Files changed (31) hide show
  1. package/package.descriptor.mjs +114 -15
  2. package/package.json +8 -2
  3. package/src/client/components/ShellLayout.vue +11 -4
  4. package/src/client/components/ShellMenuLinkItem.vue +71 -0
  5. package/src/client/components/ShellOutlet.vue +10 -7
  6. package/src/client/components/ShellOutletMenuWidget.vue +57 -0
  7. package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +116 -0
  8. package/src/client/components/ShellTabLinkItem.vue +128 -0
  9. package/src/client/index.js +4 -0
  10. package/src/client/lib/menuIcons.js +210 -0
  11. package/src/client/placement/runtime.js +22 -22
  12. package/src/client/placement/validators.js +19 -49
  13. package/src/client/support/menuLinkTarget.js +97 -0
  14. package/src/server/support/localLinkItemScaffolds.js +80 -0
  15. package/templates/expected-existing/src/App.vue +13 -0
  16. package/templates/expected-existing/src/pages/console/index.vue +12 -0
  17. package/templates/expected-existing/src/pages/console.vue +13 -0
  18. package/templates/expected-existing/src/pages/home/index.vue +12 -0
  19. package/templates/expected-existing/src/pages/home.vue +13 -0
  20. package/templates/src/components/ShellLayout.vue +11 -4
  21. package/templates/src/components/menus/MenuLinkItem.vue +30 -0
  22. package/templates/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
  23. package/templates/src/components/menus/TabLinkItem.vue +34 -0
  24. package/templates/src/pages/home/settings/index.vue +8 -8
  25. package/templates/src/pages/home/settings.vue +4 -1
  26. package/test/bootstrapClaimContract.test.js +88 -0
  27. package/test/linkItemScaffoldContract.test.js +209 -0
  28. package/test/outletMenuWidgetContract.test.js +33 -0
  29. package/test/placementRegistry.test.js +17 -6
  30. package/test/placementRuntime.test.js +59 -44
  31. package/test/settingsPlacementContract.test.js +16 -5
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/shell-web",
4
- version: "0.1.32",
4
+ version: "0.1.33",
5
5
  kind: "runtime",
6
6
  description: "Web shell layout runtime with outlet-based placement contributions.",
7
7
  dependsOn: [],
@@ -30,7 +30,7 @@ export default Object.freeze({
30
30
  surfaces: [
31
31
  {
32
32
  subpath: "./client",
33
- summary: "Exports shell layout/outlet/error-host components and ShellWebClientProvider."
33
+ summary: "Exports shell layout/outlet/outlet-menu/error-host components and ShellWebClientProvider."
34
34
  },
35
35
  {
36
36
  subpath: "./client/placement",
@@ -55,32 +55,30 @@ export default Object.freeze({
55
55
  placements: {
56
56
  outlets: [
57
57
  {
58
- host: "shell-layout",
59
- position: "top-left",
58
+ target: "shell-layout:top-left",
60
59
  surfaces: ["*"],
61
60
  source: "src/client/components/ShellLayout.vue"
62
61
  },
63
62
  {
64
- host: "shell-layout",
65
- position: "top-right",
63
+ target: "shell-layout:top-right",
66
64
  surfaces: ["*"],
67
65
  source: "src/client/components/ShellLayout.vue"
68
66
  },
69
67
  {
70
- host: "shell-layout",
71
- position: "primary-menu",
68
+ target: "shell-layout:primary-menu",
69
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
72
70
  surfaces: ["*"],
73
71
  source: "src/client/components/ShellLayout.vue"
74
72
  },
75
73
  {
76
- host: "shell-layout",
77
- position: "secondary-menu",
74
+ target: "shell-layout:secondary-menu",
75
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
78
76
  surfaces: ["*"],
79
77
  source: "src/client/components/ShellLayout.vue"
80
78
  },
81
79
  {
82
- host: "home-settings",
83
- position: "primary-menu",
80
+ target: "home-settings:primary-menu",
81
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
84
82
  surfaces: ["home"],
85
83
  source: "templates/src/pages/home/settings.vue"
86
84
  }
@@ -92,8 +90,9 @@ export default Object.freeze({
92
90
  mutations: {
93
91
  dependencies: {
94
92
  runtime: {
93
+ "@mdi/js": "^7.4.47",
95
94
  "@tanstack/vue-query": "^5.90.5",
96
- "@jskit-ai/kernel": "0.1.33",
95
+ "@jskit-ai/kernel": "0.1.34",
97
96
  "vuetify": "^4.0.0"
98
97
  },
99
98
  dev: {}
@@ -106,11 +105,74 @@ export default Object.freeze({
106
105
  }
107
106
  },
108
107
  procfile: {},
109
- text: [],
108
+ text: [
109
+ {
110
+ op: "append-text",
111
+ file: "packages/main/src/client/providers/MainClientProvider.js",
112
+ position: "top",
113
+ skipIfContains: "import MenuLinkItem from \"/src/components/menus/MenuLinkItem.vue\";",
114
+ value: "import MenuLinkItem from \"/src/components/menus/MenuLinkItem.vue\";\n",
115
+ reason: "Bind app-owned shell menu link-item scaffold into local main client provider imports.",
116
+ category: "shell-web",
117
+ id: "shell-web-main-client-provider-menu-link-item-import"
118
+ },
119
+ {
120
+ op: "append-text",
121
+ file: "packages/main/src/client/providers/MainClientProvider.js",
122
+ position: "top",
123
+ skipIfContains: "import SurfaceAwareMenuLinkItem from \"/src/components/menus/SurfaceAwareMenuLinkItem.vue\";",
124
+ value: "import SurfaceAwareMenuLinkItem from \"/src/components/menus/SurfaceAwareMenuLinkItem.vue\";\n",
125
+ reason: "Bind app-owned shell surface-aware menu link-item scaffold into local main client provider imports.",
126
+ category: "shell-web",
127
+ id: "shell-web-main-client-provider-surface-aware-menu-link-item-import"
128
+ },
129
+ {
130
+ op: "append-text",
131
+ file: "packages/main/src/client/providers/MainClientProvider.js",
132
+ position: "top",
133
+ skipIfContains: "import TabLinkItem from \"/src/components/menus/TabLinkItem.vue\";",
134
+ value: "import TabLinkItem from \"/src/components/menus/TabLinkItem.vue\";\n",
135
+ reason: "Bind app-owned shell tab link-item scaffold into local main client provider imports.",
136
+ category: "shell-web",
137
+ id: "shell-web-main-client-provider-tab-link-item-import"
138
+ },
139
+ {
140
+ op: "append-text",
141
+ file: "packages/main/src/client/providers/MainClientProvider.js",
142
+ position: "bottom",
143
+ skipIfContains: "registerMainClientComponent(\"local.main.ui.menu-link-item\", () => MenuLinkItem);",
144
+ value: "\nregisterMainClientComponent(\"local.main.ui.menu-link-item\", () => MenuLinkItem);\n",
145
+ reason: "Bind app-owned shell menu link-item token into local main client provider registry.",
146
+ category: "shell-web",
147
+ id: "shell-web-main-client-provider-menu-link-item-register"
148
+ },
149
+ {
150
+ op: "append-text",
151
+ file: "packages/main/src/client/providers/MainClientProvider.js",
152
+ position: "bottom",
153
+ skipIfContains: "registerMainClientComponent(\"local.main.ui.surface-aware-menu-link-item\", () => SurfaceAwareMenuLinkItem);",
154
+ value: "\nregisterMainClientComponent(\"local.main.ui.surface-aware-menu-link-item\", () => SurfaceAwareMenuLinkItem);\n",
155
+ reason: "Bind app-owned shell surface-aware menu link-item token into local main client provider registry.",
156
+ category: "shell-web",
157
+ id: "shell-web-main-client-provider-surface-aware-menu-link-item-register"
158
+ },
159
+ {
160
+ op: "append-text",
161
+ file: "packages/main/src/client/providers/MainClientProvider.js",
162
+ position: "bottom",
163
+ skipIfContains: "registerMainClientComponent(\"local.main.ui.tab-link-item\", () => TabLinkItem);",
164
+ value: "\nregisterMainClientComponent(\"local.main.ui.tab-link-item\", () => TabLinkItem);\n",
165
+ reason: "Bind app-owned shell tab link-item token into local main client provider registry.",
166
+ category: "shell-web",
167
+ id: "shell-web-main-client-provider-tab-link-item-register"
168
+ }
169
+ ],
110
170
  files: [
111
171
  {
112
172
  from: "templates/src/App.vue",
113
173
  to: "src/App.vue",
174
+ ownership: "app",
175
+ expectedExistingFrom: "templates/expected-existing/src/App.vue",
114
176
  reason: "Install full-width shell app root with shell-web error host and edge-to-edge layout.",
115
177
  category: "shell-web",
116
178
  id: "shell-web-app-root"
@@ -118,13 +180,39 @@ export default Object.freeze({
118
180
  {
119
181
  from: "templates/src/components/ShellLayout.vue",
120
182
  to: "src/components/ShellLayout.vue",
183
+ ownership: "app",
121
184
  reason: "Install app-owned shell layout component so apps can customize structure and slots.",
122
185
  category: "shell-web",
123
186
  id: "shell-web-component-shell-layout"
124
187
  },
188
+ {
189
+ from: "templates/src/components/menus/MenuLinkItem.vue",
190
+ to: "src/components/menus/MenuLinkItem.vue",
191
+ ownership: "app",
192
+ reason: "Install app-owned shell menu link-item scaffold for local placement customization.",
193
+ category: "shell-web",
194
+ id: "shell-web-component-menu-link-item"
195
+ },
196
+ {
197
+ from: "templates/src/components/menus/SurfaceAwareMenuLinkItem.vue",
198
+ to: "src/components/menus/SurfaceAwareMenuLinkItem.vue",
199
+ ownership: "app",
200
+ reason: "Install app-owned surface-aware shell menu link-item scaffold for local placement customization.",
201
+ category: "shell-web",
202
+ id: "shell-web-component-surface-aware-menu-link-item"
203
+ },
204
+ {
205
+ from: "templates/src/components/menus/TabLinkItem.vue",
206
+ to: "src/components/menus/TabLinkItem.vue",
207
+ ownership: "app",
208
+ reason: "Install app-owned shell tab link-item scaffold for local placement customization.",
209
+ category: "shell-web",
210
+ id: "shell-web-component-tab-link-item"
211
+ },
125
212
  {
126
213
  from: "templates/src/error.js",
127
214
  to: "src/error.js",
215
+ ownership: "app",
128
216
  reason: "Install app-owned error runtime policy and presenter config scaffold.",
129
217
  category: "shell-web",
130
218
  id: "shell-web-error-config"
@@ -132,6 +220,7 @@ export default Object.freeze({
132
220
  {
133
221
  from: "templates/src/placement.js",
134
222
  to: "src/placement.js",
223
+ ownership: "app",
135
224
  reason: "Install app-owned placement registry scaffold used by shell-web placement runtime.",
136
225
  category: "shell-web",
137
226
  id: "shell-web-placement-registry"
@@ -140,6 +229,8 @@ export default Object.freeze({
140
229
  from: "templates/src/pages/home.vue",
141
230
  toSurface: "home",
142
231
  toSurfaceRoot: true,
232
+ ownership: "app",
233
+ expectedExistingFrom: "templates/expected-existing/src/pages/home.vue",
143
234
  reason: "Install shell-driven home wrapper page.",
144
235
  category: "shell-web",
145
236
  id: "shell-web-page-home-wrapper"
@@ -148,6 +239,8 @@ export default Object.freeze({
148
239
  from: "templates/src/pages/home/index.vue",
149
240
  toSurface: "home",
150
241
  toSurfacePath: "index.vue",
242
+ ownership: "app",
243
+ expectedExistingFrom: "templates/expected-existing/src/pages/home/index.vue",
151
244
  reason: "Install shell-driven home surface starter page.",
152
245
  category: "shell-web",
153
246
  id: "shell-web-page-home"
@@ -156,6 +249,7 @@ export default Object.freeze({
156
249
  from: "templates/src/pages/home/settings.vue",
157
250
  toSurface: "home",
158
251
  toSurfacePath: "settings.vue",
252
+ ownership: "app",
159
253
  reason: "Install shell-driven home settings shell route with section navigation.",
160
254
  category: "shell-web",
161
255
  id: "shell-web-page-home-settings-shell"
@@ -164,7 +258,8 @@ export default Object.freeze({
164
258
  from: "templates/src/pages/home/settings/index.vue",
165
259
  toSurface: "home",
166
260
  toSurfacePath: "settings/index.vue",
167
- reason: "Install shell-driven home settings landing page scaffold.",
261
+ ownership: "app",
262
+ reason: "Install shell-driven home settings index stub scaffold for app-owned landing or redirect behavior.",
168
263
  category: "shell-web",
169
264
  id: "shell-web-page-home-settings"
170
265
  },
@@ -172,6 +267,8 @@ export default Object.freeze({
172
267
  from: "templates/src/pages/console.vue",
173
268
  toSurface: "console",
174
269
  toSurfaceRoot: true,
270
+ ownership: "app",
271
+ expectedExistingFrom: "templates/expected-existing/src/pages/console.vue",
175
272
  reason: "Install shell-driven console wrapper page.",
176
273
  category: "shell-web",
177
274
  id: "shell-web-page-console-wrapper"
@@ -180,6 +277,8 @@ export default Object.freeze({
180
277
  from: "templates/src/pages/console/index.vue",
181
278
  toSurface: "console",
182
279
  toSurfacePath: "index.vue",
280
+ ownership: "app",
281
+ expectedExistingFrom: "templates/expected-existing/src/pages/console/index.vue",
183
282
  reason: "Install shell-driven console page starter.",
184
283
  category: "shell-web",
185
284
  id: "shell-web-page-console"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/shell-web",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -9,16 +9,22 @@
9
9
  "./client": "./src/client/index.js",
10
10
  "./client/error": "./src/client/error/index.js",
11
11
  "./client/placement": "./src/client/placement/index.js",
12
+ "./server/support/localLinkItemScaffolds": "./src/server/support/localLinkItemScaffolds.js",
12
13
  "./client/navigation/linkResolver": "./src/client/navigation/linkResolver.js",
13
14
  "./client/components/ShellLayout": "./src/client/components/ShellLayout.vue",
14
15
  "./client/components/ShellOutlet": "./src/client/components/ShellOutlet.vue",
16
+ "./client/components/ShellOutletMenuWidget": "./src/client/components/ShellOutletMenuWidget.vue",
15
17
  "./client/components/ShellErrorHost": "./src/client/components/ShellErrorHost.vue",
18
+ "./client/components/ShellMenuLinkItem": "./src/client/components/ShellMenuLinkItem.vue",
19
+ "./client/components/ShellSurfaceAwareMenuLinkItem": "./src/client/components/ShellSurfaceAwareMenuLinkItem.vue",
20
+ "./client/components/ShellTabLinkItem": "./src/client/components/ShellTabLinkItem.vue",
16
21
  "./client/composables/useShellLayoutState": "./src/client/composables/useShellLayoutState.js",
17
22
  "./client/providers/ShellWebClientProvider": "./src/client/providers/ShellWebClientProvider.js"
18
23
  },
19
24
  "dependencies": {
25
+ "@mdi/js": "^7.4.47",
20
26
  "@tanstack/vue-query": "^5.90.5",
21
- "@jskit-ai/kernel": "0.1.33",
27
+ "@jskit-ai/kernel": "0.1.34",
22
28
  "vuetify": "^4.0.0"
23
29
  }
24
30
  }
@@ -32,7 +32,7 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
32
32
  <slot name="top-left" :surface="resolvedSurface">
33
33
  <div class="d-flex align-center ga-2">
34
34
  <v-chip color="primary" size="small" label>{{ resolvedSurfaceLabel }}</v-chip>
35
- <ShellOutlet host="shell-layout" position="top-left" />
35
+ <ShellOutlet target="shell-layout:top-left" />
36
36
  </div>
37
37
  </slot>
38
38
 
@@ -40,7 +40,7 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
40
40
 
41
41
  <slot name="top-right" :surface="resolvedSurface">
42
42
  <div class="d-flex align-center ga-2">
43
- <ShellOutlet host="shell-layout" position="top-right" />
43
+ <ShellOutlet target="shell-layout:top-right" />
44
44
  </div>
45
45
  </slot>
46
46
  </v-app-bar>
@@ -49,9 +49,16 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
49
49
  <slot name="menu" :surface="resolvedSurface">
50
50
  <v-list nav density="comfortable" class="pt-2">
51
51
  <v-list-subheader class="text-uppercase text-caption">{{ resolvedSurfaceLabel }}</v-list-subheader>
52
- <ShellOutlet host="shell-layout" position="primary-menu" default />
52
+ <ShellOutlet
53
+ target="shell-layout:primary-menu"
54
+ default
55
+ default-link-component-token="local.main.ui.surface-aware-menu-link-item"
56
+ />
53
57
  <v-divider class="my-2" />
54
- <ShellOutlet host="shell-layout" position="secondary-menu" />
58
+ <ShellOutlet
59
+ target="shell-layout:secondary-menu"
60
+ default-link-component-token="local.main.ui.surface-aware-menu-link-item"
61
+ />
55
62
  </v-list>
56
63
  </slot>
57
64
  </v-navigation-drawer>
@@ -0,0 +1,71 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import {
4
+ resolveSurfaceNavigationTargetFromPlacementContext,
5
+ useWebPlacementContext
6
+ } from "../placement/index.js";
7
+ import { resolveMenuLinkIcon } from "../lib/menuIcons.js";
8
+
9
+ const props = defineProps({
10
+ label: {
11
+ type: String,
12
+ default: ""
13
+ },
14
+ to: {
15
+ type: String,
16
+ default: ""
17
+ },
18
+ icon: {
19
+ type: String,
20
+ default: ""
21
+ },
22
+ disabled: {
23
+ type: Boolean,
24
+ default: false
25
+ },
26
+ exact: {
27
+ type: Boolean,
28
+ default: false
29
+ }
30
+ });
31
+
32
+ const { context: placementContext } = useWebPlacementContext();
33
+
34
+ const resolvedTarget = computed(() => {
35
+ const target = String(props.to || "").trim();
36
+ if (!target) {
37
+ return {
38
+ href: "",
39
+ sameOrigin: true
40
+ };
41
+ }
42
+
43
+ const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
44
+ path: target
45
+ });
46
+ return {
47
+ href: navigationTarget.href,
48
+ sameOrigin: navigationTarget.sameOrigin
49
+ };
50
+ });
51
+
52
+ const resolvedIcon = computed(() =>
53
+ resolveMenuLinkIcon({
54
+ icon: props.icon,
55
+ label: props.label,
56
+ to: resolvedTarget.value.href || props.to
57
+ })
58
+ );
59
+ </script>
60
+
61
+ <template>
62
+ <v-list-item
63
+ v-if="resolvedTarget.href"
64
+ :title="props.label"
65
+ :to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
66
+ :href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
67
+ :prepend-icon="resolvedIcon || undefined"
68
+ :disabled="props.disabled"
69
+ :exact="props.exact"
70
+ />
71
+ </template>
@@ -14,11 +14,7 @@ import {
14
14
  } from "../placement/surfaceContext.js";
15
15
 
16
16
  const props = defineProps({
17
- host: {
18
- type: String,
19
- default: ""
20
- },
21
- position: {
17
+ target: {
22
18
  type: String,
23
19
  default: ""
24
20
  },
@@ -29,6 +25,10 @@ const props = defineProps({
29
25
  context: {
30
26
  type: Object,
31
27
  default: () => ({})
28
+ },
29
+ defaultLinkComponentToken: {
30
+ type: String,
31
+ default: ""
32
32
  }
33
33
  });
34
34
 
@@ -78,12 +78,15 @@ const resolvedSurface = computed(() => {
78
78
  return "*";
79
79
  });
80
80
 
81
+ const resolvedTargetId = computed(() => {
82
+ return String(props.target || "").trim();
83
+ });
84
+
81
85
  const placements = computed(() => {
82
86
  void revision.value;
83
87
  return placementRuntime.getPlacements({
84
88
  surface: resolvedSurface.value,
85
- host: props.host,
86
- position: props.position,
89
+ target: resolvedTargetId.value,
87
90
  context: props.context
88
91
  });
89
92
  });
@@ -0,0 +1,57 @@
1
+ <script setup>
2
+ import { mdiCogOutline } from "@mdi/js";
3
+ import ShellOutlet from "./ShellOutlet.vue";
4
+
5
+ const props = defineProps({
6
+ target: {
7
+ type: String,
8
+ required: true
9
+ },
10
+ defaultLinkComponentToken: {
11
+ type: String,
12
+ default: ""
13
+ },
14
+ icon: {
15
+ type: String,
16
+ default: mdiCogOutline
17
+ },
18
+ ariaLabel: {
19
+ type: String,
20
+ default: "Tools"
21
+ },
22
+ location: {
23
+ type: String,
24
+ default: "bottom end"
25
+ },
26
+ offset: {
27
+ type: [Number, String],
28
+ default: 10
29
+ },
30
+ minWidth: {
31
+ type: [Number, String],
32
+ default: 220
33
+ }
34
+ });
35
+ </script>
36
+
37
+ <template>
38
+ <v-menu :location="props.location" :offset="props.offset" eager>
39
+ <template #activator="{ props: activatorProps }">
40
+ <v-btn
41
+ v-bind="activatorProps"
42
+ icon
43
+ variant="text"
44
+ :aria-label="props.ariaLabel"
45
+ >
46
+ <v-icon :icon="props.icon" />
47
+ </v-btn>
48
+ </template>
49
+
50
+ <v-list :min-width="props.minWidth" density="comfortable" class="py-1">
51
+ <ShellOutlet
52
+ :target="props.target"
53
+ :default-link-component-token="props.defaultLinkComponentToken"
54
+ />
55
+ </v-list>
56
+ </v-menu>
57
+ </template>
@@ -0,0 +1,116 @@
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 { resolveMenuLinkIcon } from "../lib/menuIcons.js";
11
+ import { resolveMenuLinkTarget } from "../support/menuLinkTarget.js";
12
+
13
+ const props = defineProps({
14
+ label: {
15
+ type: String,
16
+ default: ""
17
+ },
18
+ to: {
19
+ type: String,
20
+ default: ""
21
+ },
22
+ icon: {
23
+ type: String,
24
+ default: ""
25
+ },
26
+ surface: {
27
+ type: String,
28
+ default: ""
29
+ },
30
+ workspaceSuffix: {
31
+ type: String,
32
+ default: "/"
33
+ },
34
+ nonWorkspaceSuffix: {
35
+ type: String,
36
+ default: "/"
37
+ },
38
+ disabled: {
39
+ type: Boolean,
40
+ default: false
41
+ },
42
+ exact: {
43
+ type: Boolean,
44
+ default: false
45
+ }
46
+ });
47
+
48
+ const route = useRoute();
49
+ const { context: placementContext } = useWebPlacementContext();
50
+
51
+ const currentSurfaceId = computed(() => {
52
+ return resolveSurfaceIdFromPlacementPathname(
53
+ placementContext.value,
54
+ String(route?.path || route?.fullPath || "").trim()
55
+ );
56
+ });
57
+
58
+ const resolvedTo = computed(() => {
59
+ return resolveMenuLinkTarget({
60
+ to: props.to,
61
+ surface: props.surface,
62
+ currentSurfaceId: currentSurfaceId.value,
63
+ placementContext: placementContext.value,
64
+ workspaceSuffix: props.workspaceSuffix,
65
+ nonWorkspaceSuffix: props.nonWorkspaceSuffix,
66
+ routeParams: route.params || {},
67
+ resolvePagePath(relativePath, options = {}) {
68
+ return resolveShellLinkPath({
69
+ context: placementContext.value,
70
+ surface: options.surface,
71
+ relativePath,
72
+ params: route.params || {},
73
+ strictParams: options.strictParams !== false
74
+ });
75
+ }
76
+ });
77
+ });
78
+
79
+ const resolvedTarget = computed(() => {
80
+ const target = String(resolvedTo.value || "").trim();
81
+ if (!target) {
82
+ return {
83
+ href: "",
84
+ sameOrigin: true
85
+ };
86
+ }
87
+
88
+ const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
89
+ path: target
90
+ });
91
+ return {
92
+ href: navigationTarget.href,
93
+ sameOrigin: navigationTarget.sameOrigin
94
+ };
95
+ });
96
+
97
+ const resolvedIcon = computed(() =>
98
+ resolveMenuLinkIcon({
99
+ icon: props.icon,
100
+ label: props.label,
101
+ to: resolvedTarget.value.href || resolvedTo.value
102
+ })
103
+ );
104
+ </script>
105
+
106
+ <template>
107
+ <v-list-item
108
+ v-if="resolvedTarget.href"
109
+ :title="props.label"
110
+ :to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
111
+ :href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
112
+ :prepend-icon="resolvedIcon || undefined"
113
+ :disabled="props.disabled"
114
+ :exact="props.exact"
115
+ />
116
+ </template>