@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,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/shell-web",
4
- version: "0.1.64",
4
+ version: "0.1.66",
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/outlet-menu/error-host components and ShellWebClientProvider."
33
+ summary: "Exports shell layout/outlet/outlet-menu/route-transition/error-host components and ShellWebClientProvider."
34
34
  },
35
35
  {
36
36
  subpath: "./client/placement",
@@ -43,6 +43,10 @@ export default Object.freeze({
43
43
  {
44
44
  subpath: "./client/bootstrap",
45
45
  summary: "Exports the shared client bootstrap handler registry used to extend /api/bootstrap handling."
46
+ },
47
+ {
48
+ subpath: "./test/adaptiveShellSmoke",
49
+ summary: "Exports reusable Playwright smoke coverage for generated adaptive shell layouts."
46
50
  }
47
51
  ],
48
52
  containerTokens: {
@@ -51,8 +55,7 @@ export default Object.freeze({
51
55
  "runtime.web-placement.client",
52
56
  "runtime.web-bootstrap.client",
53
57
  "runtime.web-error.client",
54
- "runtime.web-error.presentation-store.client",
55
- "shell.web.query-client"
58
+ "runtime.web-error.presentation-store.client"
56
59
  ]
57
60
  }
58
61
  },
@@ -71,46 +74,213 @@ export default Object.freeze({
71
74
  },
72
75
  {
73
76
  target: "shell-layout:primary-menu",
74
- defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
77
+ surfaces: ["*"],
78
+ source: "src/client/components/ShellLayout.vue"
79
+ },
80
+ {
81
+ target: "shell-layout:primary-bottom-nav",
75
82
  surfaces: ["*"],
76
83
  source: "src/client/components/ShellLayout.vue"
77
84
  },
78
85
  {
79
86
  target: "shell-layout:secondary-menu",
80
- defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
87
+ surfaces: ["*"],
88
+ source: "src/client/components/ShellLayout.vue"
89
+ },
90
+ {
91
+ target: "shell-layout:supporting-bottom-sheet",
92
+ surfaces: ["*"],
93
+ source: "src/client/components/ShellLayout.vue"
94
+ },
95
+ {
96
+ target: "shell-layout:supporting-side-panel",
81
97
  surfaces: ["*"],
82
98
  source: "src/client/components/ShellLayout.vue"
83
99
  },
84
100
  {
85
101
  target: "home-settings:primary-menu",
86
- defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
87
102
  surfaces: ["home"],
88
103
  source: "templates/src/pages/home/settings.vue"
89
104
  }
90
105
  ],
106
+ topology: {
107
+ placements: [
108
+ {
109
+ id: "shell.primary-nav",
110
+ description: "Primary top-level navigation for the current surface.",
111
+ surfaces: ["*"],
112
+ default: true,
113
+ variants: {
114
+ compact: {
115
+ outlet: "shell-layout:primary-bottom-nav",
116
+ renderers: {
117
+ link: "local.main.ui.tab-link-item"
118
+ }
119
+ },
120
+ medium: {
121
+ outlet: "shell-layout:primary-menu",
122
+ renderers: {
123
+ link: "local.main.ui.surface-aware-menu-link-item"
124
+ }
125
+ },
126
+ expanded: {
127
+ outlet: "shell-layout:primary-menu",
128
+ renderers: {
129
+ link: "local.main.ui.surface-aware-menu-link-item"
130
+ }
131
+ }
132
+ }
133
+ },
134
+ {
135
+ id: "shell.status",
136
+ description: "Surface status, connection, and utility indicators.",
137
+ surfaces: ["*"],
138
+ variants: {
139
+ compact: {
140
+ outlet: "shell-layout:top-right"
141
+ },
142
+ medium: {
143
+ outlet: "shell-layout:top-right"
144
+ },
145
+ expanded: {
146
+ outlet: "shell-layout:top-right"
147
+ }
148
+ }
149
+ },
150
+ {
151
+ id: "shell.secondary-nav",
152
+ description: "Secondary navigation for lower-priority shell links.",
153
+ surfaces: ["*"],
154
+ variants: {
155
+ compact: {
156
+ outlet: "shell-layout:secondary-menu",
157
+ renderers: {
158
+ link: "local.main.ui.surface-aware-menu-link-item"
159
+ }
160
+ },
161
+ medium: {
162
+ outlet: "shell-layout:secondary-menu",
163
+ renderers: {
164
+ link: "local.main.ui.surface-aware-menu-link-item"
165
+ }
166
+ },
167
+ expanded: {
168
+ outlet: "shell-layout:secondary-menu",
169
+ renderers: {
170
+ link: "local.main.ui.surface-aware-menu-link-item"
171
+ }
172
+ }
173
+ }
174
+ },
175
+ {
176
+ id: "shell.identity",
177
+ description: "Current user, workspace, and surface identity controls.",
178
+ surfaces: ["*"],
179
+ variants: {
180
+ compact: {
181
+ outlet: "shell-layout:top-left"
182
+ },
183
+ medium: {
184
+ outlet: "shell-layout:top-left"
185
+ },
186
+ expanded: {
187
+ outlet: "shell-layout:top-left"
188
+ }
189
+ }
190
+ },
191
+ {
192
+ id: "shell.global-actions",
193
+ description: "Global surface actions that should stay outside primary navigation.",
194
+ surfaces: ["*"],
195
+ variants: {
196
+ compact: {
197
+ outlet: "shell-layout:top-right",
198
+ renderers: {
199
+ link: "local.main.ui.surface-aware-menu-link-item"
200
+ }
201
+ },
202
+ medium: {
203
+ outlet: "shell-layout:top-right",
204
+ renderers: {
205
+ link: "local.main.ui.surface-aware-menu-link-item"
206
+ }
207
+ },
208
+ expanded: {
209
+ outlet: "shell-layout:top-right",
210
+ renderers: {
211
+ link: "local.main.ui.surface-aware-menu-link-item"
212
+ }
213
+ }
214
+ }
215
+ },
216
+ {
217
+ id: "page.supporting-content",
218
+ description: "Supporting page content that opens as a bottom sheet on compact layouts and a side panel on wider layouts.",
219
+ surfaces: ["*"],
220
+ variants: {
221
+ compact: {
222
+ outlet: "shell-layout:supporting-bottom-sheet"
223
+ },
224
+ medium: {
225
+ outlet: "shell-layout:supporting-side-panel"
226
+ },
227
+ expanded: {
228
+ outlet: "shell-layout:supporting-side-panel"
229
+ }
230
+ }
231
+ },
232
+ {
233
+ id: "page.section-nav",
234
+ owner: "home-settings",
235
+ description: "Navigation between child pages in the home settings section.",
236
+ surfaces: ["home"],
237
+ variants: {
238
+ compact: {
239
+ outlet: "home-settings:primary-menu",
240
+ renderers: {
241
+ link: "local.main.ui.surface-aware-menu-link-item"
242
+ }
243
+ },
244
+ medium: {
245
+ outlet: "home-settings:primary-menu",
246
+ renderers: {
247
+ link: "local.main.ui.surface-aware-menu-link-item"
248
+ }
249
+ },
250
+ expanded: {
251
+ outlet: "home-settings:primary-menu",
252
+ renderers: {
253
+ link: "local.main.ui.surface-aware-menu-link-item"
254
+ }
255
+ }
256
+ }
257
+ }
258
+ ]
259
+ },
91
260
  contributions: [
92
261
  {
93
262
  id: "shell-web.home.menu.home",
94
- target: "shell-layout:primary-menu",
263
+ target: "shell.primary-nav",
264
+ kind: "link",
95
265
  surfaces: ["home"],
96
266
  order: 50,
97
- componentToken: "local.main.ui.surface-aware-menu-link-item",
98
267
  source: "templates/src/placement.js"
99
268
  },
100
269
  {
101
270
  id: "shell-web.home.menu.settings",
102
- target: "shell-layout:primary-menu",
271
+ target: "shell.primary-nav",
272
+ kind: "link",
103
273
  surfaces: ["home"],
104
274
  order: 100,
105
- componentToken: "local.main.ui.surface-aware-menu-link-item",
106
275
  source: "templates/src/placement.js"
107
276
  },
108
277
  {
109
278
  id: "shell-web.home.settings.general",
110
- target: "home-settings:primary-menu",
279
+ target: "page.section-nav",
280
+ owner: "home-settings",
281
+ kind: "link",
111
282
  surfaces: ["home"],
112
283
  order: 100,
113
- componentToken: "local.main.ui.surface-aware-menu-link-item",
114
284
  source: "templates/src/placement.js"
115
285
  }
116
286
  ]
@@ -121,9 +291,7 @@ export default Object.freeze({
121
291
  dependencies: {
122
292
  runtime: {
123
293
  "@mdi/js": "^7.4.47",
124
- "@tanstack/vue-query": "^5.90.5",
125
- "@jskit-ai/kernel": "0.1.65",
126
- "vuetify": "^4.0.0"
294
+ "@jskit-ai/kernel": "0.1.67"
127
295
  },
128
296
  dev: {}
129
297
  },
@@ -254,6 +422,14 @@ export default Object.freeze({
254
422
  category: "shell-web",
255
423
  id: "shell-web-placement-registry"
256
424
  },
425
+ {
426
+ from: "templates/src/placementTopology.js",
427
+ to: "src/placementTopology.js",
428
+ ownership: "app",
429
+ reason: "Install app-owned semantic placement topology used by shell-web placement runtime.",
430
+ category: "shell-web",
431
+ id: "shell-web-placement-topology"
432
+ },
257
433
  {
258
434
  from: "templates/src/pages/home.vue",
259
435
  toSurface: "home",
@@ -300,6 +476,14 @@ export default Object.freeze({
300
476
  reason: "Install shell-driven general settings child page with a tiny browser-local shell preference example.",
301
477
  category: "shell-web",
302
478
  id: "shell-web-page-home-settings-general"
479
+ },
480
+ {
481
+ from: "templates/tests/e2e/adaptive-shell.spec.ts",
482
+ to: "tests/e2e/adaptive-shell.spec.ts",
483
+ ownership: "app",
484
+ reason: "Install compact/medium/expanded Playwright smoke coverage for the adaptive shell.",
485
+ category: "shell-web",
486
+ id: "shell-web-test-adaptive-shell-smoke"
303
487
  }
304
488
  ]
305
489
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/shell-web",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -15,22 +15,23 @@
15
15
  "./client/components/ShellLayout": "./src/client/components/ShellLayout.vue",
16
16
  "./client/components/ShellOutlet": "./src/client/components/ShellOutlet.vue",
17
17
  "./client/components/ShellOutletMenuWidget": "./src/client/components/ShellOutletMenuWidget.vue",
18
+ "./client/components/ShellRouteTransition": "./src/client/components/ShellRouteTransition.vue",
18
19
  "./client/components/ShellErrorHost": "./src/client/components/ShellErrorHost.vue",
19
20
  "./client/components/ShellMenuLinkItem": "./src/client/components/ShellMenuLinkItem.vue",
20
21
  "./client/components/ShellSurfaceAwareMenuLinkItem": "./src/client/components/ShellSurfaceAwareMenuLinkItem.vue",
21
22
  "./client/components/ShellTabLinkItem": "./src/client/components/ShellTabLinkItem.vue",
22
23
  "./client/composables/useShellLayoutState": "./src/client/composables/useShellLayoutState.js",
23
- "./client/providers/ShellWebClientProvider": "./src/client/providers/ShellWebClientProvider.js"
24
+ "./client/providers/ShellWebClientProvider": "./src/client/providers/ShellWebClientProvider.js",
25
+ "./test/adaptiveShellSmoke": "./src/test/adaptiveShellSmoke.js"
24
26
  },
25
27
  "dependencies": {
26
28
  "@mdi/js": "^7.4.47",
27
- "@tanstack/vue-query": "^5.90.5",
28
- "@jskit-ai/kernel": "0.1.65",
29
- "pinia": "^3.0.4",
30
- "vuetify": "^4.0.0"
29
+ "@jskit-ai/kernel": "0.1.67"
31
30
  },
32
31
  "peerDependencies": {
32
+ "pinia": "^3.0.4",
33
33
  "vue": "^3.5.13",
34
- "vue-router": "^5.0.4"
34
+ "vue-router": "^5.0.4",
35
+ "vuetify": "^4.0.0"
35
36
  }
36
37
  }
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed } from "vue";
2
+ import { computed, ref, watch } from "vue";
3
3
  import {
4
4
  useShellWebErrorRuntime
5
5
  } from "../error/inject.js";
@@ -8,9 +8,57 @@ import { useShellErrorPresentationStore } from "../stores/useShellErrorPresentat
8
8
  const runtime = useShellWebErrorRuntime();
9
9
  const store = useShellErrorPresentationStore();
10
10
 
11
- const snackbarEntry = computed(() => store.channels.snackbar[0] || null);
11
+ const snackbarEntries = computed(() => store.channels.snackbar || []);
12
12
  const bannerEntries = computed(() => store.channels.banner || []);
13
13
  const dialogEntry = computed(() => store.channels.dialog[0] || null);
14
+ const displayedSnackbarEntry = ref(null);
15
+ const snackbarOpen = ref(false);
16
+
17
+ function isSameSnackbarEntry(left = null, right = null) {
18
+ return Boolean(left?.id && right?.id && left.id === right.id);
19
+ }
20
+
21
+ function hasSnackbarEntry(entry = null) {
22
+ if (!entry?.id) {
23
+ return false;
24
+ }
25
+
26
+ return snackbarEntries.value.some((candidate) => isSameSnackbarEntry(candidate, entry));
27
+ }
28
+
29
+ function openNextSnackbarEntry() {
30
+ const nextEntry = snackbarEntries.value[0] || null;
31
+ if (!nextEntry) {
32
+ displayedSnackbarEntry.value = null;
33
+ snackbarOpen.value = false;
34
+ return;
35
+ }
36
+
37
+ displayedSnackbarEntry.value = nextEntry;
38
+ snackbarOpen.value = true;
39
+ }
40
+
41
+ watch(
42
+ snackbarEntries,
43
+ (entries) => {
44
+ const currentEntry = displayedSnackbarEntry.value;
45
+ if (!currentEntry) {
46
+ openNextSnackbarEntry();
47
+ return;
48
+ }
49
+
50
+ const matchingEntry = entries.find((entry) => isSameSnackbarEntry(entry, currentEntry)) || null;
51
+ if (matchingEntry) {
52
+ displayedSnackbarEntry.value = matchingEntry;
53
+ return;
54
+ }
55
+
56
+ snackbarOpen.value = false;
57
+ },
58
+ {
59
+ immediate: true
60
+ }
61
+ );
14
62
 
15
63
  function resolveSeverityColor(severity = "error") {
16
64
  const normalized = String(severity || "error").trim().toLowerCase();
@@ -26,6 +74,20 @@ function resolveSeverityColor(severity = "error") {
26
74
  return "error";
27
75
  }
28
76
 
77
+ function resolveSeverityIcon(severity = "error") {
78
+ const normalized = String(severity || "error").trim().toLowerCase();
79
+ if (normalized === "info") {
80
+ return "mdi-information-outline";
81
+ }
82
+ if (normalized === "success") {
83
+ return "mdi-check-circle-outline";
84
+ }
85
+ if (normalized === "warning") {
86
+ return "mdi-alert-outline";
87
+ }
88
+ return "mdi-alert-outline";
89
+ }
90
+
29
91
  function resolveTimeout(entry) {
30
92
  if (!entry) {
31
93
  return -1;
@@ -56,8 +118,8 @@ function runAction(entry) {
56
118
  source: "shell-web.error-host.action",
57
119
  message: "Error action failed.",
58
120
  cause: error,
59
- severity: "error",
60
- channel: "dialog"
121
+ intent: "blocking",
122
+ severity: "error"
61
123
  });
62
124
  }
63
125
 
@@ -67,8 +129,9 @@ function runAction(entry) {
67
129
  }
68
130
 
69
131
  function onSnackbarModelValue(nextValue) {
70
- if (nextValue === false && snackbarEntry.value) {
71
- dismiss(snackbarEntry.value);
132
+ if (nextValue === false && displayedSnackbarEntry.value) {
133
+ snackbarOpen.value = false;
134
+ dismiss(displayedSnackbarEntry.value);
72
135
  }
73
136
  }
74
137
 
@@ -77,6 +140,14 @@ function onDialogModelValue(nextValue) {
77
140
  dismiss(dialogEntry.value);
78
141
  }
79
142
  }
143
+
144
+ function onSnackbarAfterLeave() {
145
+ if (hasSnackbarEntry(displayedSnackbarEntry.value)) {
146
+ return;
147
+ }
148
+
149
+ openNextSnackbarEntry();
150
+ }
80
151
  </script>
81
152
 
82
153
  <template>
@@ -87,6 +158,7 @@ function onDialogModelValue(nextValue) {
87
158
  v-for="entry in bannerEntries"
88
159
  :key="entry.id"
89
160
  :type="resolveSeverityColor(entry.severity)"
161
+ :icon="resolveSeverityIcon(entry.severity)"
90
162
  variant="elevated"
91
163
  density="comfortable"
92
164
  rounded="lg"
@@ -113,28 +185,29 @@ function onDialogModelValue(nextValue) {
113
185
  </div>
114
186
 
115
187
  <v-snackbar
116
- :model-value="Boolean(snackbarEntry)"
188
+ :model-value="snackbarOpen"
117
189
  location="bottom end"
118
- :timeout="resolveTimeout(snackbarEntry)"
119
- :color="resolveSeverityColor(snackbarEntry?.severity)"
190
+ :timeout="resolveTimeout(displayedSnackbarEntry)"
191
+ :color="displayedSnackbarEntry ? resolveSeverityColor(displayedSnackbarEntry.severity) : undefined"
120
192
  @update:model-value="onSnackbarModelValue"
193
+ @after-leave="onSnackbarAfterLeave"
121
194
  >
122
- <span v-if="snackbarEntry">{{ snackbarEntry.message }}</span>
195
+ <span v-if="displayedSnackbarEntry">{{ displayedSnackbarEntry.message }}</span>
123
196
 
124
197
  <template #actions>
125
198
  <v-btn
126
- v-if="snackbarEntry?.action"
199
+ v-if="displayedSnackbarEntry?.action"
127
200
  variant="text"
128
201
  size="small"
129
- @click="runAction(snackbarEntry)"
202
+ @click="runAction(displayedSnackbarEntry)"
130
203
  >
131
- {{ snackbarEntry.action.label }}
204
+ {{ displayedSnackbarEntry.action.label }}
132
205
  </v-btn>
133
206
  <v-btn
134
- v-if="snackbarEntry"
207
+ v-if="displayedSnackbarEntry"
135
208
  variant="text"
136
209
  size="small"
137
- @click="dismiss(snackbarEntry)"
210
+ @click="dismiss(displayedSnackbarEntry)"
138
211
  >
139
212
  Dismiss
140
213
  </v-btn>