@jskit-ai/shell-web 0.1.64 → 0.1.66

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 (37) hide show
  1. package/package.descriptor.mjs +200 -16
  2. package/package.json +8 -7
  3. package/src/client/components/ShellErrorHost.vue +88 -15
  4. package/src/client/components/ShellLayout.vue +551 -50
  5. package/src/client/components/ShellOutlet.vue +34 -4
  6. package/src/client/components/ShellOutletMenuWidget.vue +1 -8
  7. package/src/client/components/ShellRouteTransition.vue +480 -0
  8. package/src/client/components/ShellTabLinkItem.vue +22 -6
  9. package/src/client/composables/useShellLayoutState.js +12 -1
  10. package/src/client/error/normalize.js +17 -0
  11. package/src/client/error/policy.js +25 -11
  12. package/src/client/error/runtime.js +2 -0
  13. package/src/client/index.js +1 -0
  14. package/src/client/placement/index.js +5 -0
  15. package/src/client/placement/runtime.js +149 -16
  16. package/src/client/placement/validators.js +36 -8
  17. package/src/client/providers/ShellWebClientProvider.js +189 -24
  18. package/src/client/stores/useShellLayoutStore.js +21 -1
  19. package/src/test/adaptiveShellSmoke.js +121 -0
  20. package/templates/expected-existing/src/pages/home/index.vue +40 -10
  21. package/templates/src/components/ShellLayout.vue +10 -90
  22. package/templates/src/components/menus/TabLinkItem.vue +4 -0
  23. package/templates/src/error.js +7 -1
  24. package/templates/src/pages/home/index.vue +64 -23
  25. package/templates/src/pages/home/settings/general/index.vue +12 -9
  26. package/templates/src/pages/home/settings.vue +68 -24
  27. package/templates/src/placement.js +7 -6
  28. package/templates/src/placementTopology.js +149 -0
  29. package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
  30. package/test/errorRuntime.test.js +42 -0
  31. package/test/linkItemScaffoldContract.test.js +9 -2
  32. package/test/outletMenuWidgetContract.test.js +2 -2
  33. package/test/placementRegistry.test.js +3 -3
  34. package/test/placementRuntime.test.js +144 -14
  35. package/test/provider.test.js +97 -5
  36. package/test/settingsPlacementContract.test.js +234 -20
  37. package/test/useShellLayoutState.test.js +19 -0
@@ -1,7 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { createPinia } from "pinia";
4
- import { ShellWebClientProvider } from "../src/client/providers/ShellWebClientProvider.js";
4
+ import {
5
+ ShellWebClientProvider,
6
+ resolveAppPlacementTopologyExport
7
+ } from "../src/client/providers/ShellWebClientProvider.js";
5
8
  import { useShellErrorPresentationStore } from "../src/client/stores/useShellErrorPresentationStore.js";
6
9
  const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
7
10
 
@@ -14,7 +17,7 @@ function setClientAppConfig(source = {}) {
14
17
  return normalized;
15
18
  }
16
19
 
17
- function createAppDouble({ surfaceRuntime = null } = {}) {
20
+ function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
18
21
  const singletons = new Map();
19
22
  const singletonInstances = new Map();
20
23
  const provided = [];
@@ -61,6 +64,9 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
61
64
  if (token === "jskit.client.surface.runtime") {
62
65
  return Boolean(surfaceRuntime);
63
66
  }
67
+ if (token === "jskit.client.query-client") {
68
+ return Boolean(queryClient);
69
+ }
64
70
  return singletons.has(token) || singletonInstances.has(token);
65
71
  },
66
72
  make(token) {
@@ -73,6 +79,9 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
73
79
  if (token === "jskit.client.surface.runtime" && surfaceRuntime) {
74
80
  return surfaceRuntime;
75
81
  }
82
+ if (token === "jskit.client.query-client" && queryClient) {
83
+ return queryClient;
84
+ }
76
85
  if (singletonInstances.has(token)) {
77
86
  return singletonInstances.get(token);
78
87
  }
@@ -125,6 +134,32 @@ async function withFetchImplementation(fetchImplementation, callback) {
125
134
  }
126
135
  }
127
136
 
137
+ test("shell web client provider preserves append-only placement topology object exports", () => {
138
+ const warnings = [];
139
+ const topology = {
140
+ placements: [
141
+ {
142
+ id: "shell.primary-nav",
143
+ surfaces: ["*"],
144
+ variants: {
145
+ compact: { outlet: "shell-layout:primary-menu" },
146
+ medium: { outlet: "shell-layout:primary-menu" },
147
+ expanded: { outlet: "shell-layout:primary-menu" }
148
+ }
149
+ }
150
+ ]
151
+ };
152
+
153
+ const resolved = resolveAppPlacementTopologyExport(topology, {
154
+ warn(payload, message) {
155
+ warnings.push({ payload, message });
156
+ }
157
+ });
158
+
159
+ assert.equal(resolved, topology);
160
+ assert.deepEqual(warnings, []);
161
+ });
162
+
128
163
  test("shell web client provider binds runtime and injects it into Vue app", async () => {
129
164
  await withFetchStub({ surfaceAccess: {} }, async () => {
130
165
  const app = createAppDouble();
@@ -134,15 +169,15 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
134
169
  assert.equal(app.singletons.has("runtime.web-placement.client"), true);
135
170
  assert.equal(app.singletons.has("runtime.web-error.client"), true);
136
171
  assert.equal(app.singletons.has("runtime.web-error.presentation-store.client"), true);
172
+ assert.equal(app.singletons.has("runtime.web-refresh.client"), true);
137
173
 
138
174
  await provider.boot(app);
139
- assert.equal(app.plugins.length, 1);
140
- assert.equal(typeof app.plugins[0].plugin.install, "function");
141
- assert.equal(typeof app.plugins[0].options?.queryClient, "object");
175
+ assert.equal(app.plugins.length, 0);
142
176
 
143
177
  const providedByKey = new Map(app.provided.map((entry) => [entry.key, entry.value]));
144
178
 
145
179
  assert.equal(providedByKey.has("jskit.shell-web.runtime.web-placement.client"), true);
180
+ assert.equal(providedByKey.has("jskit.shell-web.runtime.web-refresh.client"), true);
146
181
  assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.client"), true);
147
182
  assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.presentation-store.client"), true);
148
183
 
@@ -156,6 +191,9 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
156
191
  assert.equal(typeof errorRuntime.report, "function");
157
192
  assert.equal(typeof errorRuntime.configure, "function");
158
193
 
194
+ const refreshRuntime = providedByKey.get("jskit.shell-web.runtime.web-refresh.client");
195
+ assert.equal(typeof refreshRuntime.refresh, "function");
196
+
159
197
  const errorStore = providedByKey.get("jskit.shell-web.runtime.web-error.presentation-store.client");
160
198
  assert.equal(typeof errorStore.getState, "function");
161
199
  assert.equal(typeof errorStore.present, "function");
@@ -168,6 +206,60 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
168
206
  });
169
207
  });
170
208
 
209
+ test("shell refresh runtime refreshes bootstrap and active queries by default", async () => {
210
+ await withFetchStub({ surfaceAccess: { home: true } }, async () => {
211
+ const refetchCalls = [];
212
+ const queryClient = {
213
+ async refetchQueries(filters = {}, options = {}) {
214
+ refetchCalls.push({ filters, options });
215
+ }
216
+ };
217
+ const app = createAppDouble({ queryClient });
218
+ const provider = new ShellWebClientProvider();
219
+ provider.register(app);
220
+
221
+ const refreshRuntime = app.make("runtime.web-refresh.client");
222
+ const result = await refreshRuntime.refresh("test-refresh");
223
+
224
+ assert.equal(result.reason, "test-refresh");
225
+ assert.equal(result.bootstrapRefreshed, true);
226
+ assert.equal(result.queriesRefetched, true);
227
+ assert.equal(refetchCalls.length, 1);
228
+ assert.equal(refetchCalls[0].filters.type, "active");
229
+ assert.equal(refetchCalls[0].options.throwOnError, false);
230
+ assert.equal(refetchCalls[0].filters.predicate({}), true);
231
+ assert.equal(refetchCalls[0].filters.predicate({ meta: {} }), true);
232
+ assert.equal(refetchCalls[0].filters.predicate({ meta: { jskit: { refreshOnPull: true } } }), true);
233
+ assert.equal(refetchCalls[0].filters.predicate({ meta: { jskitRefresh: "pull" } }), true);
234
+ assert.equal(refetchCalls[0].filters.predicate({ meta: { jskit: { refreshOnPull: false } } }), false);
235
+ assert.equal(refetchCalls[0].filters.predicate({ meta: { jskitRefresh: false } }), false);
236
+ });
237
+ });
238
+
239
+ test("shell refresh runtime reports recoverable retry errors as banners", async () => {
240
+ await withFetchStub({ surfaceAccess: { home: true } }, async () => {
241
+ const queryClient = {
242
+ async refetchQueries() {
243
+ throw new Error("Network unavailable");
244
+ }
245
+ };
246
+ const app = createAppDouble({ queryClient });
247
+ const provider = new ShellWebClientProvider();
248
+ provider.register(app);
249
+
250
+ const refreshRuntime = app.make("runtime.web-refresh.client");
251
+ const result = await refreshRuntime.refresh("test-refresh");
252
+
253
+ assert.equal(result.error instanceof Error, true);
254
+
255
+ const errorStore = app.make("runtime.web-error.presentation-store.client");
256
+ const state = errorStore.getState();
257
+ assert.equal(state.channels.banner.length, 1);
258
+ assert.equal(state.channels.banner[0].message, "Unable to refresh. Check the connection and try again.");
259
+ assert.equal(state.channels.banner[0].action.label, "Retry");
260
+ });
261
+ });
262
+
171
263
  test("shell web client provider resolves surface config from client app config", async () => {
172
264
  setClientAppConfig({
173
265
  tenancyMode: "workspaces",
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import test from "node:test";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { assertGeneratedUiSourceContract } from "@jskit-ai/kernel/shared/support/generatedUiContract";
6
7
  import descriptor from "../package.descriptor.mjs";
7
8
 
8
9
  const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
@@ -24,6 +25,18 @@ function readContributions(target = "") {
24
25
  : [];
25
26
  }
26
27
 
28
+ function readTopology(id = "", owner = "") {
29
+ const placements = descriptor?.metadata?.ui?.placements?.topology?.placements;
30
+ const normalizedId = String(id || "").trim();
31
+ const normalizedOwner = String(owner || "").trim();
32
+ return Array.isArray(placements)
33
+ ? placements.filter((entry) =>
34
+ String(entry?.id || "").trim() === normalizedId &&
35
+ String(entry?.owner || "").trim() === normalizedOwner
36
+ )
37
+ : [];
38
+ }
39
+
27
40
  function findFileMutation(id) {
28
41
  const files = descriptor?.mutations?.files;
29
42
  return Array.isArray(files)
@@ -35,10 +48,121 @@ test("shell-web home settings template exposes surface-derived settings outlets"
35
48
  const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings.vue"), "utf8");
36
49
 
37
50
  assert.match(source, /target="home-settings:primary-menu"/);
38
- assert.match(source, /default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item"/);
51
+ assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-shell/);
52
+ assert.match(source, /--generated-ui-screen-title-size/);
53
+ assert.doesNotMatch(source, /default-link-component-token/);
39
54
  assert.match(source, /<RouterView \/>/);
40
55
  });
41
56
 
57
+ test("shell-web shell layout registers navigation at the app layout level", async () => {
58
+ const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "ShellLayout.vue"), "utf8");
59
+
60
+ assert.doesNotMatch(source, /<v-layout\b/);
61
+ assert.doesNotMatch(source, /overflow-hidden/);
62
+ assert.doesNotMatch(source, /min-height:\s*72vh/);
63
+ assert.match(source, /ShellRouteTransition/);
64
+ assert.match(source, /<ShellRouteTransition>[\s\S]*<slot \/>[\s\S]*<\/ShellRouteTransition>/);
65
+ assert.match(source, /data-testid="jskit-shell-app-bar"/);
66
+ assert.match(source, /:density="isCompactLayout \? 'compact' : 'comfortable'"/);
67
+ assert.match(source, /shell-layout__surface-label/);
68
+ assert.doesNotMatch(source, /shell-layout__surface-chip/);
69
+ assert.doesNotMatch(source, /<v-chip[^>]*resolvedSurfaceLabel/);
70
+ assert.match(source, /shell-layout__top-right[\s\S]*max-width:\s*min\(45vw, 18rem\)/);
71
+ assert.match(source, /<v-bottom-navigation[\s\S]*target="shell-layout:primary-bottom-nav"/);
72
+ assert.match(source, /<v-bottom-sheet[\s\S]*target="shell-layout:supporting-bottom-sheet"/);
73
+ assert.match(source, /target="shell-layout:supporting-side-panel"/);
74
+ assert.match(source, /data-testid="jskit-shell-supporting-bottom-sheet"/);
75
+ assert.match(source, /data-testid="jskit-shell-supporting-side-panel"/);
76
+ assert.match(source, /inject\("jskit\.shell-web\.runtime\.web-refresh\.client", null\)/);
77
+ assert.match(source, /window\.addEventListener\("pointerdown", handlePullPointerDown/);
78
+ assert.match(source, /window\.addEventListener\("touchmove", handlePullTouchMove/);
79
+ assert.match(source, /refreshRuntime\.refresh\("pull-to-refresh"\)/);
80
+ assert.match(source, /data-testid="jskit-shell-pull-refresh"/);
81
+ assert.match(source, /target="shell-layout:primary-menu"[\s\S]*default/);
82
+ assert.doesNotMatch(source, /target="shell-layout:primary-bottom-nav"[\s\S]*default/);
83
+ assert.match(source, /data-testid="jskit-shell-drawer"/);
84
+ assert.match(source, /data-testid="jskit-shell-bottom-nav"/);
85
+ assert.match(source, /padding:\s*0\.75rem 1rem calc\(1rem \+ env\(safe-area-inset-bottom, 0px\)\)/);
86
+
87
+ const template = await readFile(path.join(PACKAGE_DIR, "templates", "src", "components", "ShellLayout.vue"), "utf8");
88
+
89
+ assert.match(template, /PackageShellLayout from "@jskit-ai\/shell-web\/client\/components\/ShellLayout"/);
90
+ assert.match(template, /h\(PackageShellLayout, attrs, slots\)/);
91
+ assert.doesNotMatch(template, /ShellOutlet|ShellRouteTransition|useShellLayoutState|pointerdown|v-navigation-drawer|v-bottom-navigation/);
92
+ });
93
+
94
+ test("shell-web error host uses one explicit close affordance for banner errors", async () => {
95
+ const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "ShellErrorHost.vue"), "utf8");
96
+
97
+ assert.match(source, /function resolveSeverityIcon/);
98
+ assert.match(source, /mdi-alert-outline/);
99
+ assert.match(source, /:icon="resolveSeverityIcon\(entry\.severity\)"/);
100
+ assert.match(source, /closable[\s\S]*@click:close="dismiss\(entry\)"/);
101
+ assert.doesNotMatch(source, /mdi-close/);
102
+ });
103
+
104
+ test("shell-web error host keeps snackbar color stable while closing", async () => {
105
+ const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "ShellErrorHost.vue"), "utf8");
106
+
107
+ assert.match(source, /displayedSnackbarEntry/);
108
+ assert.match(source, /@after-leave="onSnackbarAfterLeave"/);
109
+ assert.match(source, /:color="displayedSnackbarEntry \? resolveSeverityColor\(displayedSnackbarEntry\.severity\) : undefined"/);
110
+ assert.doesNotMatch(source, /resolveSeverityColor\(snackbarEntry\?\.severity\)/);
111
+ });
112
+
113
+ test("shell-web error template uses intent-driven default presentation", async () => {
114
+ const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "error.js"), "utf8");
115
+
116
+ assert.match(source, /resourceLoadChannel:\s*"silent"/);
117
+ assert.match(source, /actionFeedbackChannel:\s*"snackbar"/);
118
+ assert.match(source, /appRecoverableChannel:\s*"banner"/);
119
+ assert.match(source, /blockingChannel:\s*"dialog"/);
120
+ });
121
+
122
+ test("shell-web installs generated adaptive shell Playwright smoke coverage", async () => {
123
+ const source = await readFile(path.join(PACKAGE_DIR, "templates", "tests", "e2e", "adaptive-shell.spec.ts"), "utf8");
124
+ const helperSource = await readFile(path.join(PACKAGE_DIR, "src", "test", "adaptiveShellSmoke.js"), "utf8");
125
+
126
+ assertGeneratedUiSourceContract(helperSource, {
127
+ profile: "responsive-smoke",
128
+ sourceName: "adaptiveShellSmoke.js"
129
+ });
130
+ assert.match(source, /runAdaptiveShellSmoke/);
131
+ assert.match(source, /@jskit-ai\/shell-web\/test\/adaptiveShellSmoke/);
132
+ assert.match(helperSource, /generated adaptive shell smoke/);
133
+ assert.match(helperSource, /390/);
134
+ assert.match(helperSource, /768/);
135
+ assert.match(helperSource, /1280/);
136
+ assert.match(helperSource, /jskit-shell-bottom-nav/);
137
+ assert.match(helperSource, /jskit-shell-drawer/);
138
+ assert.match(helperSource, /scrollWidth/);
139
+ assert.match(helperSource, /toBeGreaterThanOrEqual\(48\)/);
140
+
141
+ const packageJson = JSON.parse(await readFile(path.join(PACKAGE_DIR, "package.json"), "utf8"));
142
+ assert.equal(packageJson?.exports?.["./test/adaptiveShellSmoke"], "./src/test/adaptiveShellSmoke.js");
143
+ });
144
+
145
+ test("shell-web route transition keeps mobile route motion placement-driven", async () => {
146
+ const source = await readFile(
147
+ path.join(PACKAGE_DIR, "src", "client", "components", "ShellRouteTransition.vue"),
148
+ "utf8"
149
+ );
150
+
151
+ assert.match(source, /target:\s*\{[\s\S]*default:\s*"shell-layout:primary-bottom-nav"/);
152
+ assert.match(source, /semanticTarget:\s*\{[\s\S]*default:\s*"shell\.primary-nav"/);
153
+ assert.match(source, /placementRuntime\s*\.\s*getPlacements/);
154
+ assert.match(source, /useRouter\(\)/);
155
+ assert.match(source, /swipeEnabled:\s*\{[\s\S]*default:\s*true/);
156
+ assert.match(source, /window\.addEventListener\("pointerdown", handlePointerDown/);
157
+ assert.match(source, /document\.documentElement\.classList\.toggle\("shell-route-swipe-enabled", enabled\)/);
158
+ assert.match(source, /navigateBySwipe\(deltaX < 0 \? 1 : -1\)/);
159
+ assert.match(source, /router\.push\(nextEntry\.href\)/);
160
+ assert.match(source, /isSwipeIgnoredTarget/);
161
+ assert.match(source, /touch-action:\s*pan-y/);
162
+ assert.match(source, /transitionDirection\.value = nextIndex > previousIndex \? "forward" : "reverse"/);
163
+ assert.match(source, /prefers-reduced-motion:\s*reduce/);
164
+ });
165
+
42
166
  test("shell-web settings landing page redirects to the starter child page", async () => {
43
167
  const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "index.vue"), "utf8");
44
168
 
@@ -47,24 +171,28 @@ test("shell-web settings landing page redirects to the starter child page", asyn
47
171
  assert.match(source, /redirectToChild\("general"\)/);
48
172
  });
49
173
 
50
- test("shell-web settings general child page exposes a tiny browser-local drawer preference", async () => {
174
+ test("shell-web settings general child page exposes an adaptive drawer preference", async () => {
51
175
  const source = await readFile(
52
176
  path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "general", "index.vue"),
53
177
  "utf8"
54
178
  );
55
179
 
56
180
  assert.match(source, /useShellLayoutState/);
181
+ assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-general-screen/);
57
182
  assert.match(source, /drawerDefaultOpen/);
58
183
  assert.match(source, /setDrawerDefaultOpen/);
59
- assert.match(source, /Open navigation drawer by default/);
60
- assert.match(source, /live in this browser only/);
184
+ assert.match(source, /Phone layouts keep primary navigation in the bottom bar/);
185
+ assert.match(source, /Open drawer by default on wider screens/);
186
+ assert.match(source, /min-height:\s*48px/);
187
+ assert.doesNotMatch(source, /live in this browser only|tiny example|starter settings/);
61
188
  });
62
189
 
63
- test("shell-web placement template seeds default Home and Settings drawer navigation", async () => {
190
+ test("shell-web placement template seeds default Home and Settings adaptive navigation", async () => {
64
191
  const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "placement.js"), "utf8");
65
192
 
66
193
  assert.match(source, /id: "shell-web\.home\.menu\.home"/);
67
- assert.match(source, /target: "shell-layout:primary-menu"/);
194
+ assert.match(source, /target: "shell\.primary-nav"/);
195
+ assert.match(source, /kind: "link"/);
68
196
  assert.match(source, /surfaces: \["home"\]/);
69
197
  assert.match(source, /label: "Home"/);
70
198
  assert.match(source, /unscopedSuffix: "\/"/);
@@ -72,19 +200,64 @@ test("shell-web placement template seeds default Home and Settings drawer naviga
72
200
  assert.match(source, /label: "Settings"/);
73
201
  assert.match(source, /unscopedSuffix: "\/settings"/);
74
202
  assert.match(source, /id: "shell-web\.home\.settings\.general"/);
75
- assert.match(source, /target: "home-settings:primary-menu"/);
203
+ assert.match(source, /target: "page\.section-nav"/);
204
+ assert.match(source, /owner: "home-settings"/);
76
205
  assert.match(source, /label: "General"/);
77
206
  assert.match(source, /unscopedSuffix: "\/settings\/general"/);
78
207
  assert.doesNotMatch(source, /to: "\.\/general"/);
79
208
  });
80
209
 
81
- test("shell-web descriptor metadata advertises home settings outlets, default drawer links, and installs the scaffold page", () => {
210
+ test("shell-web placement topology seeds global actions as a semantic shell placement", async () => {
211
+ const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "placementTopology.js"), "utf8");
212
+
213
+ assert.match(source, /id: "shell\.global-actions"/);
214
+ assert.match(source, /description: "Global surface actions that should stay outside primary navigation\."/);
215
+ assert.match(source, /outlet: "shell-layout:top-right"/);
216
+ assert.match(source, /renderers: menuLinkRenderers/);
217
+ assert.match(source, /id: "page\.supporting-content"/);
218
+ assert.match(source, /outlet: "shell-layout:supporting-bottom-sheet"/);
219
+ assert.match(source, /outlet: "shell-layout:supporting-side-panel"/);
220
+ });
221
+
222
+ test("shell-web descriptor metadata advertises adaptive shell outlets, default links, and installs the scaffold page", () => {
223
+ assert.deepEqual(
224
+ readOutlets("shell-layout:primary-bottom-nav"),
225
+ [
226
+ {
227
+ target: "shell-layout:primary-bottom-nav",
228
+ surfaces: ["*"],
229
+ source: "src/client/components/ShellLayout.vue"
230
+ }
231
+ ]
232
+ );
233
+
234
+ assert.deepEqual(
235
+ readOutlets("shell-layout:supporting-bottom-sheet"),
236
+ [
237
+ {
238
+ target: "shell-layout:supporting-bottom-sheet",
239
+ surfaces: ["*"],
240
+ source: "src/client/components/ShellLayout.vue"
241
+ }
242
+ ]
243
+ );
244
+
245
+ assert.deepEqual(
246
+ readOutlets("shell-layout:supporting-side-panel"),
247
+ [
248
+ {
249
+ target: "shell-layout:supporting-side-panel",
250
+ surfaces: ["*"],
251
+ source: "src/client/components/ShellLayout.vue"
252
+ }
253
+ ]
254
+ );
255
+
82
256
  assert.deepEqual(
83
257
  readOutlets("home-settings:primary-menu"),
84
258
  [
85
259
  {
86
260
  target: "home-settings:primary-menu",
87
- defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
88
261
  surfaces: ["home"],
89
262
  source: "templates/src/pages/home/settings.vue"
90
263
  }
@@ -92,41 +265,66 @@ test("shell-web descriptor metadata advertises home settings outlets, default dr
92
265
  );
93
266
 
94
267
  assert.deepEqual(
95
- readContributions("shell-layout:primary-menu"),
268
+ readContributions("shell.primary-nav"),
96
269
  [
97
270
  {
98
271
  id: "shell-web.home.menu.home",
99
- target: "shell-layout:primary-menu",
272
+ target: "shell.primary-nav",
273
+ kind: "link",
100
274
  surfaces: ["home"],
101
275
  order: 50,
102
- componentToken: "local.main.ui.surface-aware-menu-link-item",
103
276
  source: "templates/src/placement.js"
104
277
  },
105
278
  {
106
279
  id: "shell-web.home.menu.settings",
107
- target: "shell-layout:primary-menu",
280
+ target: "shell.primary-nav",
281
+ kind: "link",
108
282
  surfaces: ["home"],
109
283
  order: 100,
110
- componentToken: "local.main.ui.surface-aware-menu-link-item",
111
284
  source: "templates/src/placement.js"
112
285
  }
113
286
  ]
114
287
  );
115
288
 
116
289
  assert.deepEqual(
117
- readContributions("home-settings:primary-menu"),
290
+ readContributions("page.section-nav"),
118
291
  [
119
292
  {
120
293
  id: "shell-web.home.settings.general",
121
- target: "home-settings:primary-menu",
294
+ target: "page.section-nav",
295
+ owner: "home-settings",
296
+ kind: "link",
122
297
  surfaces: ["home"],
123
298
  order: 100,
124
- componentToken: "local.main.ui.surface-aware-menu-link-item",
125
299
  source: "templates/src/placement.js"
126
300
  }
127
301
  ]
128
302
  );
129
303
 
304
+ assert.equal(readTopology("shell.primary-nav").length, 1);
305
+ assert.equal(readTopology("shell.primary-nav")[0]?.variants?.compact?.outlet, "shell-layout:primary-bottom-nav");
306
+ assert.equal(
307
+ readTopology("shell.primary-nav")[0]?.variants?.compact?.renderers?.link,
308
+ "local.main.ui.tab-link-item"
309
+ );
310
+ assert.equal(readTopology("shell.primary-nav")[0]?.variants?.medium?.outlet, "shell-layout:primary-menu");
311
+ assert.equal(readTopology("shell.global-actions").length, 1);
312
+ assert.equal(readTopology("shell.global-actions")[0]?.variants?.compact?.outlet, "shell-layout:top-right");
313
+ assert.equal(
314
+ readTopology("shell.global-actions")[0]?.variants?.compact?.renderers?.link,
315
+ "local.main.ui.surface-aware-menu-link-item"
316
+ );
317
+ assert.equal(readTopology("page.section-nav", "home-settings").length, 1);
318
+ assert.equal(readTopology("page.supporting-content").length, 1);
319
+ assert.equal(
320
+ readTopology("page.supporting-content")[0]?.variants?.compact?.outlet,
321
+ "shell-layout:supporting-bottom-sheet"
322
+ );
323
+ assert.equal(
324
+ readTopology("page.supporting-content")[0]?.variants?.expanded?.outlet,
325
+ "shell-layout:supporting-side-panel"
326
+ );
327
+
130
328
  assert.deepEqual(findFileMutation("shell-web-page-home-settings-shell"), {
131
329
  from: "templates/src/pages/home/settings.vue",
132
330
  toSurface: "home",
@@ -156,13 +354,29 @@ test("shell-web descriptor metadata advertises home settings outlets, default dr
156
354
  category: "shell-web",
157
355
  id: "shell-web-page-home-settings-general"
158
356
  });
357
+
358
+ assert.deepEqual(findFileMutation("shell-web-test-adaptive-shell-smoke"), {
359
+ from: "templates/tests/e2e/adaptive-shell.spec.ts",
360
+ to: "tests/e2e/adaptive-shell.spec.ts",
361
+ ownership: "app",
362
+ reason: "Install compact/medium/expanded Playwright smoke coverage for the adaptive shell.",
363
+ category: "shell-web",
364
+ id: "shell-web-test-adaptive-shell-smoke"
365
+ });
159
366
  });
160
367
 
161
- test("shell-web home starter page relies on drawer navigation instead of dead feature buttons", async () => {
368
+ test("shell-web home starter page relies on adaptive shell navigation instead of dead feature buttons", async () => {
162
369
  const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "index.vue"), "utf8");
163
370
 
164
- assert.match(source, /Use the navigation drawer to move around the shell\./);
165
- assert.doesNotMatch(source, /\/home\/settings/);
371
+ assertGeneratedUiSourceContract(source, {
372
+ profile: "shell-home",
373
+ sourceName: "shell-web home/index.vue"
374
+ });
375
+ assert.match(source, /generated-ui-screen generated-ui-screen--app home-surface-screen/);
376
+ assert.match(source, /--generated-ui-screen-title-size/);
377
+ assert.match(source, /Core services are available\./);
378
+ assert.match(source, /to="\/home\/settings\/general"/);
379
+ assert.doesNotMatch(source, /Use bottom navigation|Replace this content|Main public surface/);
166
380
  assert.doesNotMatch(source, /\/console/);
167
381
  assert.doesNotMatch(source, /\/auth\/signout/);
168
382
  });
@@ -65,3 +65,22 @@ test("shell layout store keeps drawer state and default preference in sync", ()
65
65
  store.setDrawerOpen(false);
66
66
  assert.equal(store.drawerOpen, false);
67
67
  });
68
+
69
+ test("shell layout store keeps supporting content closed until explicitly opened", () => {
70
+ const pinia = createPinia();
71
+ const store = useShellLayoutStore(pinia);
72
+
73
+ assert.equal(store.supportingContentOpen, false);
74
+ assert.equal(store.supportingContentTitle, "");
75
+
76
+ store.openSupportingContent({ title: "Customer details" });
77
+ assert.equal(store.supportingContentOpen, true);
78
+ assert.equal(store.supportingContentTitle, "Customer details");
79
+
80
+ store.setSupportingContentOpen(false);
81
+ assert.equal(store.supportingContentOpen, false);
82
+ assert.equal(store.supportingContentTitle, "Customer details");
83
+
84
+ store.closeSupportingContent();
85
+ assert.equal(store.supportingContentOpen, false);
86
+ });