@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
@@ -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.65",
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
  },
@@ -74,11 +77,26 @@ export default Object.freeze({
74
77
  surfaces: ["*"],
75
78
  source: "src/client/components/ShellLayout.vue"
76
79
  },
80
+ {
81
+ target: "shell-layout:primary-bottom-nav",
82
+ surfaces: ["*"],
83
+ source: "src/client/components/ShellLayout.vue"
84
+ },
77
85
  {
78
86
  target: "shell-layout:secondary-menu",
79
87
  surfaces: ["*"],
80
88
  source: "src/client/components/ShellLayout.vue"
81
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",
97
+ surfaces: ["*"],
98
+ source: "src/client/components/ShellLayout.vue"
99
+ },
82
100
  {
83
101
  target: "home-settings:primary-menu",
84
102
  surfaces: ["home"],
@@ -94,9 +112,9 @@ export default Object.freeze({
94
112
  default: true,
95
113
  variants: {
96
114
  compact: {
97
- outlet: "shell-layout:primary-menu",
115
+ outlet: "shell-layout:primary-bottom-nav",
98
116
  renderers: {
99
- link: "local.main.ui.surface-aware-menu-link-item"
117
+ link: "local.main.ui.tab-link-item"
100
118
  }
101
119
  },
102
120
  medium: {
@@ -170,6 +188,47 @@ export default Object.freeze({
170
188
  }
171
189
  }
172
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
+ },
173
232
  {
174
233
  id: "page.section-nav",
175
234
  owner: "home-settings",
@@ -232,9 +291,7 @@ export default Object.freeze({
232
291
  dependencies: {
233
292
  runtime: {
234
293
  "@mdi/js": "^7.4.47",
235
- "@tanstack/vue-query": "^5.90.5",
236
- "@jskit-ai/kernel": "0.1.66",
237
- "vuetify": "^4.0.0"
294
+ "@jskit-ai/kernel": "0.1.67"
238
295
  },
239
296
  dev: {}
240
297
  },
@@ -419,6 +476,14 @@ export default Object.freeze({
419
476
  reason: "Install shell-driven general settings child page with a tiny browser-local shell preference example.",
420
477
  category: "shell-web",
421
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"
422
487
  }
423
488
  ]
424
489
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/shell-web",
3
- "version": "0.1.65",
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.66",
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>