@jskit-ai/shell-web 0.1.32 → 0.1.34

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 +111 -33
  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/home/index.vue +12 -0
  17. package/templates/expected-existing/src/pages/home.vue +13 -0
  18. package/templates/src/components/ShellLayout.vue +11 -4
  19. package/templates/src/components/menus/MenuLinkItem.vue +30 -0
  20. package/templates/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
  21. package/templates/src/components/menus/TabLinkItem.vue +34 -0
  22. package/templates/src/pages/home/settings/index.vue +8 -8
  23. package/templates/src/pages/home/settings.vue +4 -1
  24. package/test/bootstrapClaimContract.test.js +66 -0
  25. package/test/linkItemScaffoldContract.test.js +209 -0
  26. package/test/outletMenuWidgetContract.test.js +33 -0
  27. package/test/placementRegistry.test.js +17 -6
  28. package/test/placementRuntime.test.js +59 -44
  29. package/test/settingsPlacementContract.test.js +16 -5
  30. package/templates/src/pages/console/index.vue +0 -24
  31. package/templates/src/pages/console.vue +0 -20
@@ -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.34",
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.35",
97
96
  "vuetify": "^4.0.0"
98
97
  },
99
98
  dev: {}
@@ -101,16 +100,78 @@ export default Object.freeze({
101
100
  packageJson: {
102
101
  scripts: {
103
102
  "dev:all": "vite",
104
- "dev:home": "VITE_SURFACE=home vite",
105
- "dev:console": "VITE_SURFACE=console vite"
103
+ "dev:home": "VITE_SURFACE=home vite"
106
104
  }
107
105
  },
108
106
  procfile: {},
109
- text: [],
107
+ text: [
108
+ {
109
+ op: "append-text",
110
+ file: "packages/main/src/client/providers/MainClientProvider.js",
111
+ position: "top",
112
+ skipIfContains: "import MenuLinkItem from \"/src/components/menus/MenuLinkItem.vue\";",
113
+ value: "import MenuLinkItem from \"/src/components/menus/MenuLinkItem.vue\";\n",
114
+ reason: "Bind app-owned shell menu link-item scaffold into local main client provider imports.",
115
+ category: "shell-web",
116
+ id: "shell-web-main-client-provider-menu-link-item-import"
117
+ },
118
+ {
119
+ op: "append-text",
120
+ file: "packages/main/src/client/providers/MainClientProvider.js",
121
+ position: "top",
122
+ skipIfContains: "import SurfaceAwareMenuLinkItem from \"/src/components/menus/SurfaceAwareMenuLinkItem.vue\";",
123
+ value: "import SurfaceAwareMenuLinkItem from \"/src/components/menus/SurfaceAwareMenuLinkItem.vue\";\n",
124
+ reason: "Bind app-owned shell surface-aware menu link-item scaffold into local main client provider imports.",
125
+ category: "shell-web",
126
+ id: "shell-web-main-client-provider-surface-aware-menu-link-item-import"
127
+ },
128
+ {
129
+ op: "append-text",
130
+ file: "packages/main/src/client/providers/MainClientProvider.js",
131
+ position: "top",
132
+ skipIfContains: "import TabLinkItem from \"/src/components/menus/TabLinkItem.vue\";",
133
+ value: "import TabLinkItem from \"/src/components/menus/TabLinkItem.vue\";\n",
134
+ reason: "Bind app-owned shell tab link-item scaffold into local main client provider imports.",
135
+ category: "shell-web",
136
+ id: "shell-web-main-client-provider-tab-link-item-import"
137
+ },
138
+ {
139
+ op: "append-text",
140
+ file: "packages/main/src/client/providers/MainClientProvider.js",
141
+ position: "bottom",
142
+ skipIfContains: "registerMainClientComponent(\"local.main.ui.menu-link-item\", () => MenuLinkItem);",
143
+ value: "\nregisterMainClientComponent(\"local.main.ui.menu-link-item\", () => MenuLinkItem);\n",
144
+ reason: "Bind app-owned shell menu link-item token into local main client provider registry.",
145
+ category: "shell-web",
146
+ id: "shell-web-main-client-provider-menu-link-item-register"
147
+ },
148
+ {
149
+ op: "append-text",
150
+ file: "packages/main/src/client/providers/MainClientProvider.js",
151
+ position: "bottom",
152
+ skipIfContains: "registerMainClientComponent(\"local.main.ui.surface-aware-menu-link-item\", () => SurfaceAwareMenuLinkItem);",
153
+ value: "\nregisterMainClientComponent(\"local.main.ui.surface-aware-menu-link-item\", () => SurfaceAwareMenuLinkItem);\n",
154
+ reason: "Bind app-owned shell surface-aware menu link-item token into local main client provider registry.",
155
+ category: "shell-web",
156
+ id: "shell-web-main-client-provider-surface-aware-menu-link-item-register"
157
+ },
158
+ {
159
+ op: "append-text",
160
+ file: "packages/main/src/client/providers/MainClientProvider.js",
161
+ position: "bottom",
162
+ skipIfContains: "registerMainClientComponent(\"local.main.ui.tab-link-item\", () => TabLinkItem);",
163
+ value: "\nregisterMainClientComponent(\"local.main.ui.tab-link-item\", () => TabLinkItem);\n",
164
+ reason: "Bind app-owned shell tab link-item token into local main client provider registry.",
165
+ category: "shell-web",
166
+ id: "shell-web-main-client-provider-tab-link-item-register"
167
+ }
168
+ ],
110
169
  files: [
111
170
  {
112
171
  from: "templates/src/App.vue",
113
172
  to: "src/App.vue",
173
+ ownership: "app",
174
+ expectedExistingFrom: "templates/expected-existing/src/App.vue",
114
175
  reason: "Install full-width shell app root with shell-web error host and edge-to-edge layout.",
115
176
  category: "shell-web",
116
177
  id: "shell-web-app-root"
@@ -118,13 +179,39 @@ export default Object.freeze({
118
179
  {
119
180
  from: "templates/src/components/ShellLayout.vue",
120
181
  to: "src/components/ShellLayout.vue",
182
+ ownership: "app",
121
183
  reason: "Install app-owned shell layout component so apps can customize structure and slots.",
122
184
  category: "shell-web",
123
185
  id: "shell-web-component-shell-layout"
124
186
  },
187
+ {
188
+ from: "templates/src/components/menus/MenuLinkItem.vue",
189
+ to: "src/components/menus/MenuLinkItem.vue",
190
+ ownership: "app",
191
+ reason: "Install app-owned shell menu link-item scaffold for local placement customization.",
192
+ category: "shell-web",
193
+ id: "shell-web-component-menu-link-item"
194
+ },
195
+ {
196
+ from: "templates/src/components/menus/SurfaceAwareMenuLinkItem.vue",
197
+ to: "src/components/menus/SurfaceAwareMenuLinkItem.vue",
198
+ ownership: "app",
199
+ reason: "Install app-owned surface-aware shell menu link-item scaffold for local placement customization.",
200
+ category: "shell-web",
201
+ id: "shell-web-component-surface-aware-menu-link-item"
202
+ },
203
+ {
204
+ from: "templates/src/components/menus/TabLinkItem.vue",
205
+ to: "src/components/menus/TabLinkItem.vue",
206
+ ownership: "app",
207
+ reason: "Install app-owned shell tab link-item scaffold for local placement customization.",
208
+ category: "shell-web",
209
+ id: "shell-web-component-tab-link-item"
210
+ },
125
211
  {
126
212
  from: "templates/src/error.js",
127
213
  to: "src/error.js",
214
+ ownership: "app",
128
215
  reason: "Install app-owned error runtime policy and presenter config scaffold.",
129
216
  category: "shell-web",
130
217
  id: "shell-web-error-config"
@@ -132,6 +219,7 @@ export default Object.freeze({
132
219
  {
133
220
  from: "templates/src/placement.js",
134
221
  to: "src/placement.js",
222
+ ownership: "app",
135
223
  reason: "Install app-owned placement registry scaffold used by shell-web placement runtime.",
136
224
  category: "shell-web",
137
225
  id: "shell-web-placement-registry"
@@ -140,6 +228,8 @@ export default Object.freeze({
140
228
  from: "templates/src/pages/home.vue",
141
229
  toSurface: "home",
142
230
  toSurfaceRoot: true,
231
+ ownership: "app",
232
+ expectedExistingFrom: "templates/expected-existing/src/pages/home.vue",
143
233
  reason: "Install shell-driven home wrapper page.",
144
234
  category: "shell-web",
145
235
  id: "shell-web-page-home-wrapper"
@@ -148,6 +238,8 @@ export default Object.freeze({
148
238
  from: "templates/src/pages/home/index.vue",
149
239
  toSurface: "home",
150
240
  toSurfacePath: "index.vue",
241
+ ownership: "app",
242
+ expectedExistingFrom: "templates/expected-existing/src/pages/home/index.vue",
151
243
  reason: "Install shell-driven home surface starter page.",
152
244
  category: "shell-web",
153
245
  id: "shell-web-page-home"
@@ -156,6 +248,7 @@ export default Object.freeze({
156
248
  from: "templates/src/pages/home/settings.vue",
157
249
  toSurface: "home",
158
250
  toSurfacePath: "settings.vue",
251
+ ownership: "app",
159
252
  reason: "Install shell-driven home settings shell route with section navigation.",
160
253
  category: "shell-web",
161
254
  id: "shell-web-page-home-settings-shell"
@@ -164,25 +257,10 @@ export default Object.freeze({
164
257
  from: "templates/src/pages/home/settings/index.vue",
165
258
  toSurface: "home",
166
259
  toSurfacePath: "settings/index.vue",
167
- reason: "Install shell-driven home settings landing page scaffold.",
260
+ ownership: "app",
261
+ reason: "Install shell-driven home settings index stub scaffold for app-owned landing or redirect behavior.",
168
262
  category: "shell-web",
169
263
  id: "shell-web-page-home-settings"
170
- },
171
- {
172
- from: "templates/src/pages/console.vue",
173
- toSurface: "console",
174
- toSurfaceRoot: true,
175
- reason: "Install shell-driven console wrapper page.",
176
- category: "shell-web",
177
- id: "shell-web-page-console-wrapper"
178
- },
179
- {
180
- from: "templates/src/pages/console/index.vue",
181
- toSurface: "console",
182
- toSurfacePath: "index.vue",
183
- reason: "Install shell-driven console page starter.",
184
- category: "shell-web",
185
- id: "shell-web-page-console"
186
264
  }
187
265
  ]
188
266
  }
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.34",
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.35",
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>