@jskit-ai/shell-web 0.1.65 → 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 (29) hide show
  1. package/package.descriptor.mjs +74 -9
  2. package/package.json +8 -7
  3. package/src/client/components/ShellErrorHost.vue +88 -15
  4. package/src/client/components/ShellLayout.vue +551 -46
  5. package/src/client/components/ShellRouteTransition.vue +480 -0
  6. package/src/client/components/ShellTabLinkItem.vue +22 -6
  7. package/src/client/composables/useShellLayoutState.js +12 -1
  8. package/src/client/error/normalize.js +17 -0
  9. package/src/client/error/policy.js +25 -11
  10. package/src/client/error/runtime.js +2 -0
  11. package/src/client/index.js +1 -0
  12. package/src/client/providers/ShellWebClientProvider.js +163 -39
  13. package/src/client/stores/useShellLayoutStore.js +21 -1
  14. package/src/test/adaptiveShellSmoke.js +121 -0
  15. package/templates/expected-existing/src/pages/home/index.vue +40 -10
  16. package/templates/src/components/ShellLayout.vue +10 -86
  17. package/templates/src/components/menus/TabLinkItem.vue +4 -0
  18. package/templates/src/error.js +7 -1
  19. package/templates/src/pages/home/index.vue +64 -23
  20. package/templates/src/pages/home/settings/general/index.vue +12 -9
  21. package/templates/src/pages/home/settings.vue +68 -21
  22. package/templates/src/placementTopology.js +43 -2
  23. package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
  24. package/test/errorRuntime.test.js +42 -0
  25. package/test/linkItemScaffoldContract.test.js +9 -2
  26. package/test/placementRuntime.test.js +37 -0
  27. package/test/provider.test.js +97 -5
  28. package/test/settingsPlacementContract.test.js +205 -8
  29. package/test/useShellLayoutState.test.js +19 -0
@@ -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));
@@ -47,10 +48,121 @@ test("shell-web home settings template exposes surface-derived settings outlets"
47
48
  const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings.vue"), "utf8");
48
49
 
49
50
  assert.match(source, /target="home-settings:primary-menu"/);
51
+ assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-shell/);
52
+ assert.match(source, /--generated-ui-screen-title-size/);
50
53
  assert.doesNotMatch(source, /default-link-component-token/);
51
54
  assert.match(source, /<RouterView \/>/);
52
55
  });
53
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
+
54
166
  test("shell-web settings landing page redirects to the starter child page", async () => {
55
167
  const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "index.vue"), "utf8");
56
168
 
@@ -59,20 +171,23 @@ test("shell-web settings landing page redirects to the starter child page", asyn
59
171
  assert.match(source, /redirectToChild\("general"\)/);
60
172
  });
61
173
 
62
- 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 () => {
63
175
  const source = await readFile(
64
176
  path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "general", "index.vue"),
65
177
  "utf8"
66
178
  );
67
179
 
68
180
  assert.match(source, /useShellLayoutState/);
181
+ assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-general-screen/);
69
182
  assert.match(source, /drawerDefaultOpen/);
70
183
  assert.match(source, /setDrawerDefaultOpen/);
71
- assert.match(source, /Open navigation drawer by default/);
72
- 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/);
73
188
  });
74
189
 
75
- 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 () => {
76
191
  const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "placement.js"), "utf8");
77
192
 
78
193
  assert.match(source, /id: "shell-web\.home\.menu\.home"/);
@@ -92,7 +207,52 @@ test("shell-web placement template seeds default Home and Settings drawer naviga
92
207
  assert.doesNotMatch(source, /to: "\.\/general"/);
93
208
  });
94
209
 
95
- 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
+
96
256
  assert.deepEqual(
97
257
  readOutlets("home-settings:primary-menu"),
98
258
  [
@@ -142,7 +302,28 @@ test("shell-web descriptor metadata advertises home settings outlets, default dr
142
302
  );
143
303
 
144
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
+ );
145
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
+ );
146
327
 
147
328
  assert.deepEqual(findFileMutation("shell-web-page-home-settings-shell"), {
148
329
  from: "templates/src/pages/home/settings.vue",
@@ -173,13 +354,29 @@ test("shell-web descriptor metadata advertises home settings outlets, default dr
173
354
  category: "shell-web",
174
355
  id: "shell-web-page-home-settings-general"
175
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
+ });
176
366
  });
177
367
 
178
- 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 () => {
179
369
  const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "index.vue"), "utf8");
180
370
 
181
- assert.match(source, /Use the navigation drawer to move around the shell\./);
182
- 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/);
183
380
  assert.doesNotMatch(source, /\/console/);
184
381
  assert.doesNotMatch(source, /\/auth\/signout/);
185
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
+ });