@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.
- package/package.descriptor.mjs +74 -9
- package/package.json +8 -7
- package/src/client/components/ShellErrorHost.vue +88 -15
- package/src/client/components/ShellLayout.vue +551 -46
- package/src/client/components/ShellRouteTransition.vue +480 -0
- package/src/client/components/ShellTabLinkItem.vue +22 -6
- package/src/client/composables/useShellLayoutState.js +12 -1
- package/src/client/error/normalize.js +17 -0
- package/src/client/error/policy.js +25 -11
- package/src/client/error/runtime.js +2 -0
- package/src/client/index.js +1 -0
- package/src/client/providers/ShellWebClientProvider.js +163 -39
- package/src/client/stores/useShellLayoutStore.js +21 -1
- package/src/test/adaptiveShellSmoke.js +121 -0
- package/templates/expected-existing/src/pages/home/index.vue +40 -10
- package/templates/src/components/ShellLayout.vue +10 -86
- package/templates/src/components/menus/TabLinkItem.vue +4 -0
- package/templates/src/error.js +7 -1
- package/templates/src/pages/home/index.vue +64 -23
- package/templates/src/pages/home/settings/general/index.vue +12 -9
- package/templates/src/pages/home/settings.vue +68 -21
- package/templates/src/placementTopology.js +43 -2
- package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
- package/test/errorRuntime.test.js +42 -0
- package/test/linkItemScaffoldContract.test.js +9 -2
- package/test/placementRuntime.test.js +37 -0
- package/test/provider.test.js +97 -5
- package/test/settingsPlacementContract.test.js +205 -8
- package/test/useShellLayoutState.test.js +19 -0
package/package.descriptor.mjs
CHANGED
|
@@ -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.
|
|
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-
|
|
115
|
+
outlet: "shell-layout:primary-bottom-nav",
|
|
98
116
|
renderers: {
|
|
99
|
-
link: "local.main.ui.
|
|
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
|
-
"@
|
|
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.
|
|
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
|
-
"@
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
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 &&
|
|
71
|
-
|
|
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="
|
|
188
|
+
:model-value="snackbarOpen"
|
|
117
189
|
location="bottom end"
|
|
118
|
-
:timeout="resolveTimeout(
|
|
119
|
-
:color="resolveSeverityColor(
|
|
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="
|
|
195
|
+
<span v-if="displayedSnackbarEntry">{{ displayedSnackbarEntry.message }}</span>
|
|
123
196
|
|
|
124
197
|
<template #actions>
|
|
125
198
|
<v-btn
|
|
126
|
-
v-if="
|
|
199
|
+
v-if="displayedSnackbarEntry?.action"
|
|
127
200
|
variant="text"
|
|
128
201
|
size="small"
|
|
129
|
-
@click="runAction(
|
|
202
|
+
@click="runAction(displayedSnackbarEntry)"
|
|
130
203
|
>
|
|
131
|
-
{{
|
|
204
|
+
{{ displayedSnackbarEntry.action.label }}
|
|
132
205
|
</v-btn>
|
|
133
206
|
<v-btn
|
|
134
|
-
v-if="
|
|
207
|
+
v-if="displayedSnackbarEntry"
|
|
135
208
|
variant="text"
|
|
136
209
|
size="small"
|
|
137
|
-
@click="dismiss(
|
|
210
|
+
@click="dismiss(displayedSnackbarEntry)"
|
|
138
211
|
>
|
|
139
212
|
Dismiss
|
|
140
213
|
</v-btn>
|