@jskit-ai/shell-web 0.1.35 → 0.1.37
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 +38 -4
- package/package.json +3 -2
- package/src/client/components/ShellErrorHost.vue +6 -9
- package/src/client/composables/shellLayoutDrawerPreference.js +43 -0
- package/src/client/composables/useShellLayoutState.js +8 -3
- package/src/client/error/index.js +1 -0
- package/src/client/error/inject.js +2 -73
- package/src/client/error/presentationDefaults.js +31 -0
- package/src/client/index.js +2 -0
- package/src/client/providers/ShellWebClientProvider.js +8 -1
- package/src/client/stores/useShellErrorPresentationStore.js +96 -0
- package/src/client/stores/useShellLayoutStore.js +34 -0
- package/templates/src/components/ShellLayout.vue +1 -1
- package/templates/src/pages/home/index.vue +1 -5
- package/templates/src/pages/home/settings/general/index.vue +37 -0
- package/templates/src/pages/home/settings/index.vue +3 -6
- package/templates/src/placement.js +44 -0
- package/test/errorStore.test.js +15 -0
- package/test/provider.test.js +21 -1
- package/test/settingsPlacementContract.test.js +98 -9
- package/test/useShellLayoutState.test.js +67 -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.37",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Web shell layout runtime with outlet-based placement contributions.",
|
|
7
7
|
dependsOn: [],
|
|
@@ -83,7 +83,32 @@ export default Object.freeze({
|
|
|
83
83
|
source: "templates/src/pages/home/settings.vue"
|
|
84
84
|
}
|
|
85
85
|
],
|
|
86
|
-
contributions: [
|
|
86
|
+
contributions: [
|
|
87
|
+
{
|
|
88
|
+
id: "shell-web.home.menu.home",
|
|
89
|
+
target: "shell-layout:primary-menu",
|
|
90
|
+
surfaces: ["*"],
|
|
91
|
+
order: 50,
|
|
92
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
93
|
+
source: "templates/src/placement.js"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "shell-web.home.menu.settings",
|
|
97
|
+
target: "shell-layout:primary-menu",
|
|
98
|
+
surfaces: ["home"],
|
|
99
|
+
order: 100,
|
|
100
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
101
|
+
source: "templates/src/placement.js"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "shell-web.home.settings.general",
|
|
105
|
+
target: "home-settings:primary-menu",
|
|
106
|
+
surfaces: ["home"],
|
|
107
|
+
order: 100,
|
|
108
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
109
|
+
source: "templates/src/placement.js"
|
|
110
|
+
}
|
|
111
|
+
]
|
|
87
112
|
}
|
|
88
113
|
}
|
|
89
114
|
},
|
|
@@ -92,7 +117,7 @@ export default Object.freeze({
|
|
|
92
117
|
runtime: {
|
|
93
118
|
"@mdi/js": "^7.4.47",
|
|
94
119
|
"@tanstack/vue-query": "^5.90.5",
|
|
95
|
-
"@jskit-ai/kernel": "0.1.
|
|
120
|
+
"@jskit-ai/kernel": "0.1.38",
|
|
96
121
|
"vuetify": "^4.0.0"
|
|
97
122
|
},
|
|
98
123
|
dev: {}
|
|
@@ -258,9 +283,18 @@ export default Object.freeze({
|
|
|
258
283
|
toSurface: "home",
|
|
259
284
|
toSurfacePath: "settings/index.vue",
|
|
260
285
|
ownership: "app",
|
|
261
|
-
reason: "Install shell-driven home settings
|
|
286
|
+
reason: "Install shell-driven home settings redirect so the starter settings shell lands on a real child page.",
|
|
262
287
|
category: "shell-web",
|
|
263
288
|
id: "shell-web-page-home-settings"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
from: "templates/src/pages/home/settings/general/index.vue",
|
|
292
|
+
toSurface: "home",
|
|
293
|
+
toSurfacePath: "settings/general/index.vue",
|
|
294
|
+
ownership: "app",
|
|
295
|
+
reason: "Install shell-driven general settings child page with a tiny browser-local shell preference example.",
|
|
296
|
+
category: "shell-web",
|
|
297
|
+
id: "shell-web-page-home-settings-general"
|
|
264
298
|
}
|
|
265
299
|
]
|
|
266
300
|
}
|
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.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@mdi/js": "^7.4.47",
|
|
26
26
|
"@tanstack/vue-query": "^5.90.5",
|
|
27
|
-
"@jskit-ai/kernel": "0.1.
|
|
27
|
+
"@jskit-ai/kernel": "0.1.38",
|
|
28
|
+
"pinia": "^3.0.4",
|
|
28
29
|
"vuetify": "^4.0.0"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import {
|
|
4
|
-
useShellWebErrorPresentationState,
|
|
5
4
|
useShellWebErrorRuntime
|
|
6
5
|
} from "../error/inject.js";
|
|
6
|
+
import { useShellErrorPresentationStore } from "../stores/useShellErrorPresentationStore.js";
|
|
7
7
|
|
|
8
8
|
const runtime = useShellWebErrorRuntime();
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const snackbarEntry = computed(() => state.value.channels.snackbar[0] || null);
|
|
15
|
-
const bannerEntries = computed(() => state.value.channels.banner || []);
|
|
16
|
-
const dialogEntry = computed(() => state.value.channels.dialog[0] || null);
|
|
9
|
+
const store = useShellErrorPresentationStore();
|
|
10
|
+
|
|
11
|
+
const snackbarEntry = computed(() => store.channels.snackbar[0] || null);
|
|
12
|
+
const bannerEntries = computed(() => store.channels.banner || []);
|
|
13
|
+
const dialogEntry = computed(() => store.channels.dialog[0] || null);
|
|
17
14
|
|
|
18
15
|
function resolveSeverityColor(severity = "error") {
|
|
19
16
|
const normalized = String(severity || "error").trim().toLowerCase();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY = "jskit.shell-web.drawer-default-open";
|
|
2
|
+
|
|
3
|
+
function readDrawerDefaultOpenPreference({
|
|
4
|
+
storage = typeof window === "object" ? window?.localStorage : null
|
|
5
|
+
} = {}) {
|
|
6
|
+
if (!storage || typeof storage.getItem !== "function") {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const storedValue = String(storage.getItem(SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY) || "").trim().toLowerCase();
|
|
12
|
+
if (storedValue === "false") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (storedValue === "true") {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeDrawerDefaultOpenPreference(open, {
|
|
26
|
+
storage = typeof window === "object" ? window?.localStorage : null
|
|
27
|
+
} = {}) {
|
|
28
|
+
if (!storage || typeof storage.setItem !== "function") {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
storage.setItem(SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY, open ? "true" : "false");
|
|
34
|
+
} catch {
|
|
35
|
+
// Ignore localStorage write failures in unsupported or locked-down environments.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY,
|
|
41
|
+
readDrawerDefaultOpenPreference,
|
|
42
|
+
writeDrawerDefaultOpenPreference
|
|
43
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { computed
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { storeToRefs } from "pinia";
|
|
2
3
|
import { useRoute } from "vue-router";
|
|
3
4
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface";
|
|
4
5
|
import { useWebPlacementContext } from "../placement/inject.js";
|
|
6
|
+
import { useShellLayoutStore } from "../stores/useShellLayoutStore.js";
|
|
5
7
|
import {
|
|
6
8
|
readPlacementSurfaceConfig,
|
|
7
9
|
resolveSurfaceDefinitionFromPlacementContext,
|
|
@@ -22,6 +24,8 @@ function toSurfaceLabel(surfaceId = "") {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function useShellLayoutState(props = {}) {
|
|
27
|
+
const shellLayoutStore = useShellLayoutStore();
|
|
28
|
+
const { drawerDefaultOpen, drawerOpen } = storeToRefs(shellLayoutStore);
|
|
25
29
|
let route = null;
|
|
26
30
|
try {
|
|
27
31
|
route = useRoute();
|
|
@@ -30,10 +34,9 @@ function useShellLayoutState(props = {}) {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
const { context: placementContext } = useWebPlacementContext();
|
|
33
|
-
const drawerOpen = ref(true);
|
|
34
37
|
|
|
35
38
|
function toggleDrawer() {
|
|
36
|
-
|
|
39
|
+
shellLayoutStore.toggleDrawer();
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
const resolvedSurface = computed(() => {
|
|
@@ -78,7 +81,9 @@ function useShellLayoutState(props = {}) {
|
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
return Object.freeze({
|
|
84
|
+
drawerDefaultOpen,
|
|
81
85
|
drawerOpen,
|
|
86
|
+
setDrawerDefaultOpen: shellLayoutStore.setDrawerDefaultOpen,
|
|
82
87
|
toggleDrawer,
|
|
83
88
|
resolvedSurface,
|
|
84
89
|
resolvedSurfaceLabel
|
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
inject
|
|
3
|
-
onBeforeUnmount,
|
|
4
|
-
onMounted,
|
|
5
|
-
shallowRef
|
|
2
|
+
inject
|
|
6
3
|
} from "vue";
|
|
7
4
|
|
|
8
|
-
const EMPTY_PRESENTATION_STATE = Object.freeze({
|
|
9
|
-
revision: 0,
|
|
10
|
-
channels: Object.freeze({
|
|
11
|
-
snackbar: Object.freeze([]),
|
|
12
|
-
banner: Object.freeze([]),
|
|
13
|
-
dialog: Object.freeze([])
|
|
14
|
-
})
|
|
15
|
-
});
|
|
16
|
-
|
|
17
5
|
const EMPTY_ERROR_RUNTIME = Object.freeze({
|
|
18
6
|
report() {
|
|
19
7
|
return Object.freeze({
|
|
@@ -60,24 +48,6 @@ const EMPTY_ERROR_RUNTIME = Object.freeze({
|
|
|
60
48
|
}
|
|
61
49
|
});
|
|
62
50
|
|
|
63
|
-
const EMPTY_PRESENTATION_STORE = Object.freeze({
|
|
64
|
-
getState() {
|
|
65
|
-
return EMPTY_PRESENTATION_STATE;
|
|
66
|
-
},
|
|
67
|
-
subscribe() {
|
|
68
|
-
return () => {};
|
|
69
|
-
},
|
|
70
|
-
present() {
|
|
71
|
-
throw new Error("Shell web error presentation store is not available.");
|
|
72
|
-
},
|
|
73
|
-
dismiss() {
|
|
74
|
-
return 0;
|
|
75
|
-
},
|
|
76
|
-
clear() {
|
|
77
|
-
return 0;
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
51
|
function useShellWebErrorRuntime({ required = false } = {}) {
|
|
82
52
|
const runtime = inject("jskit.shell-web.runtime.web-error.client", null);
|
|
83
53
|
if (runtime && typeof runtime.report === "function") {
|
|
@@ -91,48 +61,7 @@ function useShellWebErrorRuntime({ required = false } = {}) {
|
|
|
91
61
|
return EMPTY_ERROR_RUNTIME;
|
|
92
62
|
}
|
|
93
63
|
|
|
94
|
-
function useShellWebErrorPresentationStore({ required = false } = {}) {
|
|
95
|
-
const store = inject("jskit.shell-web.runtime.web-error.presentation-store.client", null);
|
|
96
|
-
if (store && typeof store.getState === "function" && typeof store.subscribe === "function") {
|
|
97
|
-
return store;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (required) {
|
|
101
|
-
throw new Error("Shell web error presentation store is not available in Vue injection context.");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return EMPTY_PRESENTATION_STORE;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function useShellWebErrorPresentationState({ required = false } = {}) {
|
|
108
|
-
const store = useShellWebErrorPresentationStore({ required });
|
|
109
|
-
const state = shallowRef(store.getState());
|
|
110
|
-
let unsubscribe = null;
|
|
111
|
-
|
|
112
|
-
onMounted(() => {
|
|
113
|
-
unsubscribe = store.subscribe((nextState) => {
|
|
114
|
-
state.value = nextState;
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
onBeforeUnmount(() => {
|
|
119
|
-
if (typeof unsubscribe === "function") {
|
|
120
|
-
unsubscribe();
|
|
121
|
-
unsubscribe = null;
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
return Object.freeze({
|
|
126
|
-
state,
|
|
127
|
-
store
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
64
|
export {
|
|
132
65
|
EMPTY_ERROR_RUNTIME,
|
|
133
|
-
|
|
134
|
-
EMPTY_PRESENTATION_STATE,
|
|
135
|
-
useShellWebErrorRuntime,
|
|
136
|
-
useShellWebErrorPresentationStore,
|
|
137
|
-
useShellWebErrorPresentationState
|
|
66
|
+
useShellWebErrorRuntime
|
|
138
67
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const EMPTY_PRESENTATION_STATE = Object.freeze({
|
|
2
|
+
revision: 0,
|
|
3
|
+
channels: Object.freeze({
|
|
4
|
+
snackbar: Object.freeze([]),
|
|
5
|
+
banner: Object.freeze([]),
|
|
6
|
+
dialog: Object.freeze([])
|
|
7
|
+
})
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const EMPTY_PRESENTATION_STORE = Object.freeze({
|
|
11
|
+
getState() {
|
|
12
|
+
return EMPTY_PRESENTATION_STATE;
|
|
13
|
+
},
|
|
14
|
+
subscribe() {
|
|
15
|
+
return () => {};
|
|
16
|
+
},
|
|
17
|
+
present() {
|
|
18
|
+
throw new Error("Shell web error presentation store is not available.");
|
|
19
|
+
},
|
|
20
|
+
dismiss() {
|
|
21
|
+
return 0;
|
|
22
|
+
},
|
|
23
|
+
clear() {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
EMPTY_PRESENTATION_STATE,
|
|
30
|
+
EMPTY_PRESENTATION_STORE
|
|
31
|
+
};
|
package/src/client/index.js
CHANGED
|
@@ -14,6 +14,8 @@ export { default as ShellMenuLinkItem } from "./components/ShellMenuLinkItem.vue
|
|
|
14
14
|
export { default as ShellSurfaceAwareMenuLinkItem } from "./components/ShellSurfaceAwareMenuLinkItem.vue";
|
|
15
15
|
export { default as ShellTabLinkItem } from "./components/ShellTabLinkItem.vue";
|
|
16
16
|
export { useShellLayoutState } from "./composables/useShellLayoutState.js";
|
|
17
|
+
export { useShellLayoutStore } from "./stores/useShellLayoutStore.js";
|
|
18
|
+
export { useShellErrorPresentationStore } from "./stores/useShellErrorPresentationStore.js";
|
|
17
19
|
|
|
18
20
|
const clientProviders = Object.freeze([ShellWebClientProvider]);
|
|
19
21
|
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
createErrorPresentationStore
|
|
23
23
|
} from "../error/store.js";
|
|
24
24
|
import { createWebPlacementRuntime } from "../placement/runtime.js";
|
|
25
|
+
import { useShellErrorPresentationStore } from "../stores/useShellErrorPresentationStore.js";
|
|
25
26
|
import { buildSurfaceConfigContext } from "../placement/surfaceContext.js";
|
|
26
27
|
|
|
27
28
|
// Keep this constant for diagnostics, but keep import() below as a literal string so Vite can statically analyze it.
|
|
@@ -292,6 +293,12 @@ class ShellWebClientProvider {
|
|
|
292
293
|
if (!vueApp || typeof vueApp.provide !== "function" || typeof vueApp.use !== "function") {
|
|
293
294
|
return;
|
|
294
295
|
}
|
|
296
|
+
const pinia = app.make("jskit.client.pinia");
|
|
297
|
+
if (!pinia) {
|
|
298
|
+
throw new Error("ShellWebClientProvider requires Pinia installed in the client app.");
|
|
299
|
+
}
|
|
300
|
+
const errorPresentationStore = app.make("runtime.web-error.presentation-store.client");
|
|
301
|
+
useShellErrorPresentationStore(pinia).attachRuntimeStore(errorPresentationStore);
|
|
295
302
|
|
|
296
303
|
vueApp.use(VueQueryPlugin, {
|
|
297
304
|
queryClient: app.make("shell.web.query-client")
|
|
@@ -300,7 +307,7 @@ class ShellWebClientProvider {
|
|
|
300
307
|
vueApp.provide("jskit.shell-web.runtime.web-error.client", errorRuntime);
|
|
301
308
|
vueApp.provide(
|
|
302
309
|
"jskit.shell-web.runtime.web-error.presentation-store.client",
|
|
303
|
-
|
|
310
|
+
errorPresentationStore
|
|
304
311
|
);
|
|
305
312
|
|
|
306
313
|
installVueErrorBridge(vueApp, errorRuntime, logger);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { computed, markRaw, shallowRef } from "vue";
|
|
2
|
+
import { defineStore } from "pinia";
|
|
3
|
+
import { EMPTY_PRESENTATION_STATE, EMPTY_PRESENTATION_STORE } from "../error/presentationDefaults.js";
|
|
4
|
+
|
|
5
|
+
function normalizePresentationState(nextState) {
|
|
6
|
+
if (!nextState || typeof nextState !== "object") {
|
|
7
|
+
return EMPTY_PRESENTATION_STATE;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return nextState;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPresentationRuntimeStore(value) {
|
|
14
|
+
return Boolean(
|
|
15
|
+
value &&
|
|
16
|
+
typeof value.getState === "function" &&
|
|
17
|
+
typeof value.subscribe === "function" &&
|
|
18
|
+
typeof value.present === "function" &&
|
|
19
|
+
typeof value.dismiss === "function" &&
|
|
20
|
+
typeof value.clear === "function"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const useShellErrorPresentationStore = defineStore("jskit.shell-web.error-presentation", () => {
|
|
25
|
+
const runtimeStore = shallowRef(markRaw(EMPTY_PRESENTATION_STORE));
|
|
26
|
+
const presentationState = shallowRef(EMPTY_PRESENTATION_STATE);
|
|
27
|
+
let unsubscribe = null;
|
|
28
|
+
|
|
29
|
+
function setPresentationState(nextState) {
|
|
30
|
+
presentationState.value = normalizePresentationState(nextState);
|
|
31
|
+
return presentationState.value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function detachRuntimeStore() {
|
|
35
|
+
if (typeof unsubscribe === "function") {
|
|
36
|
+
unsubscribe();
|
|
37
|
+
unsubscribe = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function attachRuntimeStore(nextRuntimeStore = EMPTY_PRESENTATION_STORE) {
|
|
42
|
+
if (!isPresentationRuntimeStore(nextRuntimeStore)) {
|
|
43
|
+
throw new TypeError("useShellErrorPresentationStore.attachRuntimeStore requires an error presentation store.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (runtimeStore.value === nextRuntimeStore) {
|
|
47
|
+
setPresentationState(nextRuntimeStore.getState());
|
|
48
|
+
return runtimeStore.value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
detachRuntimeStore();
|
|
52
|
+
runtimeStore.value = markRaw(nextRuntimeStore);
|
|
53
|
+
setPresentationState(nextRuntimeStore.getState());
|
|
54
|
+
unsubscribe = nextRuntimeStore.subscribe((nextState) => {
|
|
55
|
+
setPresentationState(nextState);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return runtimeStore.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getState() {
|
|
62
|
+
return presentationState.value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function subscribe(listener) {
|
|
66
|
+
return runtimeStore.value.subscribe(listener);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function present(channel, payload = {}) {
|
|
70
|
+
return runtimeStore.value.present(channel, payload);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function dismiss(channel, presentationId = "") {
|
|
74
|
+
return runtimeStore.value.dismiss(channel, presentationId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function clear(channel = "") {
|
|
78
|
+
return runtimeStore.value.clear(channel);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const revision = computed(() => Number(presentationState.value.revision || 0));
|
|
82
|
+
const channels = computed(() => presentationState.value.channels || EMPTY_PRESENTATION_STATE.channels);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
runtimeStore,
|
|
86
|
+
presentationState,
|
|
87
|
+
revision,
|
|
88
|
+
channels,
|
|
89
|
+
attachRuntimeStore,
|
|
90
|
+
getState,
|
|
91
|
+
subscribe,
|
|
92
|
+
present,
|
|
93
|
+
dismiss,
|
|
94
|
+
clear
|
|
95
|
+
};
|
|
96
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import { defineStore } from "pinia";
|
|
3
|
+
import {
|
|
4
|
+
readDrawerDefaultOpenPreference,
|
|
5
|
+
writeDrawerDefaultOpenPreference
|
|
6
|
+
} from "../composables/shellLayoutDrawerPreference.js";
|
|
7
|
+
|
|
8
|
+
export const useShellLayoutStore = defineStore("jskit.shell-web.layout", () => {
|
|
9
|
+
const drawerDefaultOpen = ref(readDrawerDefaultOpenPreference());
|
|
10
|
+
const drawerOpen = ref(drawerDefaultOpen.value);
|
|
11
|
+
|
|
12
|
+
function setDrawerDefaultOpen(open) {
|
|
13
|
+
const normalized = Boolean(open);
|
|
14
|
+
drawerDefaultOpen.value = normalized;
|
|
15
|
+
drawerOpen.value = normalized;
|
|
16
|
+
writeDrawerDefaultOpenPreference(normalized);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function setDrawerOpen(open) {
|
|
20
|
+
drawerOpen.value = Boolean(open);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toggleDrawer() {
|
|
24
|
+
drawerOpen.value = !drawerOpen.value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
drawerDefaultOpen,
|
|
29
|
+
drawerOpen,
|
|
30
|
+
setDrawerDefaultOpen,
|
|
31
|
+
setDrawerOpen,
|
|
32
|
+
toggleDrawer
|
|
33
|
+
};
|
|
34
|
+
});
|
|
@@ -48,7 +48,7 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
|
|
|
48
48
|
<v-navigation-drawer v-model="drawerOpen" border class="bg-surface" :width="248">
|
|
49
49
|
<slot name="menu" :surface="resolvedSurface">
|
|
50
50
|
<v-list nav density="comfortable" class="pt-2">
|
|
51
|
-
<v-list-subheader class="text-uppercase text-caption">
|
|
51
|
+
<v-list-subheader class="text-uppercase text-caption">Navigation</v-list-subheader>
|
|
52
52
|
<ShellOutlet
|
|
53
53
|
target="shell-layout:primary-menu"
|
|
54
54
|
default
|
|
@@ -45,11 +45,7 @@ const health = computed(() => {
|
|
|
45
45
|
<p class="text-medium-emphasis mb-0">
|
|
46
46
|
This is your primary landing page. Replace this content with your actual product home.
|
|
47
47
|
</p>
|
|
48
|
-
<
|
|
49
|
-
<v-btn color="primary" variant="flat" to="/home/settings">Open settings</v-btn>
|
|
50
|
-
<v-btn color="primary" variant="flat" to="/console">Open console surface</v-btn>
|
|
51
|
-
<v-btn color="secondary" variant="outlined" to="/auth/signout">Sign out</v-btn>
|
|
52
|
-
</div>
|
|
48
|
+
<p class="text-body-2 text-medium-emphasis mb-0">Use the navigation drawer to move around the shell.</p>
|
|
53
49
|
</v-card-text>
|
|
54
50
|
</v-card>
|
|
55
51
|
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useShellLayoutState } from "@jskit-ai/shell-web/client/composables/useShellLayoutState";
|
|
4
|
+
|
|
5
|
+
const { drawerDefaultOpen, setDrawerDefaultOpen } = useShellLayoutState();
|
|
6
|
+
|
|
7
|
+
const drawerDefaultOpenModel = computed({
|
|
8
|
+
get() {
|
|
9
|
+
return drawerDefaultOpen.value;
|
|
10
|
+
},
|
|
11
|
+
set(value) {
|
|
12
|
+
setDrawerDefaultOpen(Boolean(value));
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<section class="d-flex flex-column ga-4">
|
|
19
|
+
<div>
|
|
20
|
+
<h2 class="text-h6 mb-2">General</h2>
|
|
21
|
+
<p class="text-body-2 text-medium-emphasis mb-0">These starter settings live in this browser only.</p>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<v-switch
|
|
25
|
+
v-model="drawerDefaultOpenModel"
|
|
26
|
+
color="primary"
|
|
27
|
+
inset
|
|
28
|
+
hide-details="auto"
|
|
29
|
+
label="Open navigation drawer by default"
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
33
|
+
This tiny example exists to prove that shell-level settings can work without auth or a database. Real apps will
|
|
34
|
+
usually replace it.
|
|
35
|
+
</p>
|
|
36
|
+
</section>
|
|
37
|
+
</template>
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// });
|
|
2
|
+
definePage({
|
|
3
|
+
redirect: (to) => `${String(to.path || "").replace(/\/$/, "")}/general`
|
|
4
|
+
});
|
|
6
5
|
</script>
|
|
7
|
-
|
|
8
|
-
<template />
|
|
@@ -10,3 +10,47 @@ export { addPlacement };
|
|
|
10
10
|
export default function getPlacements() {
|
|
11
11
|
return registry.build();
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
addPlacement({
|
|
15
|
+
id: "shell-web.home.menu.home",
|
|
16
|
+
target: "shell-layout:primary-menu",
|
|
17
|
+
surfaces: ["*"],
|
|
18
|
+
order: 50,
|
|
19
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
20
|
+
props: {
|
|
21
|
+
label: "Home",
|
|
22
|
+
surface: "home",
|
|
23
|
+
workspaceSuffix: "/",
|
|
24
|
+
nonWorkspaceSuffix: "/",
|
|
25
|
+
exact: true
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
addPlacement({
|
|
30
|
+
id: "shell-web.home.menu.settings",
|
|
31
|
+
target: "shell-layout:primary-menu",
|
|
32
|
+
surfaces: ["home"],
|
|
33
|
+
order: 100,
|
|
34
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
35
|
+
props: {
|
|
36
|
+
label: "Settings",
|
|
37
|
+
surface: "home",
|
|
38
|
+
workspaceSuffix: "/settings",
|
|
39
|
+
nonWorkspaceSuffix: "/settings"
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
addPlacement({
|
|
44
|
+
id: "shell-web.home.settings.general",
|
|
45
|
+
target: "home-settings:primary-menu",
|
|
46
|
+
surfaces: ["home"],
|
|
47
|
+
order: 100,
|
|
48
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
49
|
+
props: {
|
|
50
|
+
label: "General",
|
|
51
|
+
surface: "home",
|
|
52
|
+
workspaceSuffix: "/settings/general",
|
|
53
|
+
nonWorkspaceSuffix: "/settings/general",
|
|
54
|
+
to: "./general"
|
|
55
|
+
}
|
|
56
|
+
});
|
package/test/errorStore.test.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { createPinia } from "pinia";
|
|
3
4
|
import { createErrorPresentationStore } from "../src/client/error/store.js";
|
|
5
|
+
import { useShellErrorPresentationStore } from "../src/client/stores/useShellErrorPresentationStore.js";
|
|
4
6
|
|
|
5
7
|
test("error presentation store keeps banner channel singleton", () => {
|
|
6
8
|
const store = createErrorPresentationStore({ now: () => 1000 });
|
|
@@ -24,3 +26,16 @@ test("error presentation store still queues snackbar channel entries", () => {
|
|
|
24
26
|
assert.equal(state.channels.snackbar[0].message, "One");
|
|
25
27
|
assert.equal(state.channels.snackbar[1].message, "Two");
|
|
26
28
|
});
|
|
29
|
+
|
|
30
|
+
test("shell error presentation Pinia store mirrors runtime presentation state", () => {
|
|
31
|
+
const pinia = createPinia();
|
|
32
|
+
const runtimeStore = createErrorPresentationStore({ now: () => 1000 });
|
|
33
|
+
const store = useShellErrorPresentationStore(pinia);
|
|
34
|
+
|
|
35
|
+
store.attachRuntimeStore(runtimeStore);
|
|
36
|
+
runtimeStore.present("snackbar", { message: "Hello" });
|
|
37
|
+
|
|
38
|
+
assert.equal(store.channels.snackbar.length, 1);
|
|
39
|
+
assert.equal(store.channels.snackbar[0].message, "Hello");
|
|
40
|
+
assert.equal(store.getState().channels.snackbar[0].message, "Hello");
|
|
41
|
+
});
|
package/test/provider.test.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { createPinia } from "pinia";
|
|
3
4
|
import { ShellWebClientProvider } from "../src/client/providers/ShellWebClientProvider.js";
|
|
5
|
+
import { useShellErrorPresentationStore } from "../src/client/stores/useShellErrorPresentationStore.js";
|
|
4
6
|
const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
|
|
5
7
|
|
|
6
8
|
function setClientAppConfig(source = {}) {
|
|
@@ -17,9 +19,14 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
|
|
|
17
19
|
const singletonInstances = new Map();
|
|
18
20
|
const provided = [];
|
|
19
21
|
const plugins = [];
|
|
22
|
+
const pinia = createPinia();
|
|
20
23
|
|
|
21
24
|
const vueApp = {
|
|
22
|
-
config: {
|
|
25
|
+
config: {
|
|
26
|
+
globalProperties: {
|
|
27
|
+
$pinia: pinia
|
|
28
|
+
}
|
|
29
|
+
},
|
|
23
30
|
use(plugin, options) {
|
|
24
31
|
plugins.push({ plugin, options });
|
|
25
32
|
return this;
|
|
@@ -33,6 +40,7 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
|
|
|
33
40
|
singletons,
|
|
34
41
|
provided,
|
|
35
42
|
plugins,
|
|
43
|
+
pinia,
|
|
36
44
|
vueApp,
|
|
37
45
|
singleton(token, factory) {
|
|
38
46
|
singletons.set(token, factory);
|
|
@@ -41,6 +49,9 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
|
|
|
41
49
|
if (token === "jskit.client.vue.app") {
|
|
42
50
|
return true;
|
|
43
51
|
}
|
|
52
|
+
if (token === "jskit.client.pinia") {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
44
55
|
if (token === "jskit.client.surface.runtime") {
|
|
45
56
|
return Boolean(surfaceRuntime);
|
|
46
57
|
}
|
|
@@ -50,6 +61,9 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
|
|
|
50
61
|
if (token === "jskit.client.vue.app") {
|
|
51
62
|
return vueApp;
|
|
52
63
|
}
|
|
64
|
+
if (token === "jskit.client.pinia") {
|
|
65
|
+
return pinia;
|
|
66
|
+
}
|
|
53
67
|
if (token === "jskit.client.surface.runtime" && surfaceRuntime) {
|
|
54
68
|
return surfaceRuntime;
|
|
55
69
|
}
|
|
@@ -103,6 +117,12 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
103
117
|
const errorStore = providedByKey.get("jskit.shell-web.runtime.web-error.presentation-store.client");
|
|
104
118
|
assert.equal(typeof errorStore.getState, "function");
|
|
105
119
|
assert.equal(typeof errorStore.present, "function");
|
|
120
|
+
|
|
121
|
+
const errorPresentationStore = useShellErrorPresentationStore(app.pinia);
|
|
122
|
+
assert.equal(errorPresentationStore.revision, 0);
|
|
123
|
+
assert.equal(typeof errorPresentationStore.present, "function");
|
|
124
|
+
errorStore.present("banner", { message: "Hello" });
|
|
125
|
+
assert.equal(errorPresentationStore.channels.banner[0].message, "Hello");
|
|
106
126
|
});
|
|
107
127
|
|
|
108
128
|
test("shell web client provider resolves surface config from client app config", async () => {
|
|
@@ -8,10 +8,19 @@ import descriptor from "../package.descriptor.mjs";
|
|
|
8
8
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
10
10
|
|
|
11
|
-
function
|
|
11
|
+
function readOutlets(target = "") {
|
|
12
12
|
const outlets = descriptor?.metadata?.ui?.placements?.outlets;
|
|
13
|
+
const normalizedTarget = String(target || "").trim();
|
|
13
14
|
return Array.isArray(outlets)
|
|
14
|
-
? outlets.filter((entry) => String(entry?.target || "").trim() ===
|
|
15
|
+
? outlets.filter((entry) => String(entry?.target || "").trim() === normalizedTarget)
|
|
16
|
+
: [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readContributions(target = "") {
|
|
20
|
+
const contributions = descriptor?.metadata?.ui?.placements?.contributions;
|
|
21
|
+
const normalizedTarget = String(target || "").trim();
|
|
22
|
+
return Array.isArray(contributions)
|
|
23
|
+
? contributions.filter((entry) => String(entry?.target || "").trim() === normalizedTarget)
|
|
15
24
|
: [];
|
|
16
25
|
}
|
|
17
26
|
|
|
@@ -30,16 +39,47 @@ test("shell-web home settings template exposes surface-derived settings outlets"
|
|
|
30
39
|
assert.match(source, /<RouterView \/>/);
|
|
31
40
|
});
|
|
32
41
|
|
|
33
|
-
test("shell-web
|
|
42
|
+
test("shell-web settings landing page redirects to the starter child page", async () => {
|
|
34
43
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "index.vue"), "utf8");
|
|
35
44
|
|
|
36
45
|
assert.match(source, /definePage/);
|
|
37
|
-
assert.match(source, /
|
|
46
|
+
assert.match(source, /redirect:/);
|
|
47
|
+
assert.match(source, /\/general/);
|
|
38
48
|
});
|
|
39
49
|
|
|
40
|
-
test("shell-web
|
|
50
|
+
test("shell-web settings general child page exposes a tiny browser-local drawer preference", async () => {
|
|
51
|
+
const source = await readFile(
|
|
52
|
+
path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "general", "index.vue"),
|
|
53
|
+
"utf8"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
assert.match(source, /useShellLayoutState/);
|
|
57
|
+
assert.match(source, /drawerDefaultOpen/);
|
|
58
|
+
assert.match(source, /setDrawerDefaultOpen/);
|
|
59
|
+
assert.match(source, /Open navigation drawer by default/);
|
|
60
|
+
assert.match(source, /live in this browser only/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("shell-web placement template seeds default Home and Settings drawer navigation", async () => {
|
|
64
|
+
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "placement.js"), "utf8");
|
|
65
|
+
|
|
66
|
+
assert.match(source, /id: "shell-web\.home\.menu\.home"/);
|
|
67
|
+
assert.match(source, /target: "shell-layout:primary-menu"/);
|
|
68
|
+
assert.match(source, /label: "Home"/);
|
|
69
|
+
assert.match(source, /nonWorkspaceSuffix: "\/"/);
|
|
70
|
+
assert.match(source, /id: "shell-web\.home\.menu\.settings"/);
|
|
71
|
+
assert.match(source, /label: "Settings"/);
|
|
72
|
+
assert.match(source, /nonWorkspaceSuffix: "\/settings"/);
|
|
73
|
+
assert.match(source, /id: "shell-web\.home\.settings\.general"/);
|
|
74
|
+
assert.match(source, /target: "home-settings:primary-menu"/);
|
|
75
|
+
assert.match(source, /label: "General"/);
|
|
76
|
+
assert.match(source, /nonWorkspaceSuffix: "\/settings\/general"/);
|
|
77
|
+
assert.match(source, /to: "\.\/general"/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("shell-web descriptor metadata advertises home settings outlets, default drawer links, and installs the scaffold page", () => {
|
|
41
81
|
assert.deepEqual(
|
|
42
|
-
|
|
82
|
+
readOutlets("home-settings:primary-menu"),
|
|
43
83
|
[
|
|
44
84
|
{
|
|
45
85
|
target: "home-settings:primary-menu",
|
|
@@ -50,6 +90,42 @@ test("shell-web descriptor metadata advertises home settings outlets and install
|
|
|
50
90
|
]
|
|
51
91
|
);
|
|
52
92
|
|
|
93
|
+
assert.deepEqual(
|
|
94
|
+
readContributions("shell-layout:primary-menu"),
|
|
95
|
+
[
|
|
96
|
+
{
|
|
97
|
+
id: "shell-web.home.menu.home",
|
|
98
|
+
target: "shell-layout:primary-menu",
|
|
99
|
+
surfaces: ["*"],
|
|
100
|
+
order: 50,
|
|
101
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
102
|
+
source: "templates/src/placement.js"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "shell-web.home.menu.settings",
|
|
106
|
+
target: "shell-layout:primary-menu",
|
|
107
|
+
surfaces: ["home"],
|
|
108
|
+
order: 100,
|
|
109
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
110
|
+
source: "templates/src/placement.js"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
readContributions("home-settings:primary-menu"),
|
|
117
|
+
[
|
|
118
|
+
{
|
|
119
|
+
id: "shell-web.home.settings.general",
|
|
120
|
+
target: "home-settings:primary-menu",
|
|
121
|
+
surfaces: ["home"],
|
|
122
|
+
order: 100,
|
|
123
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
124
|
+
source: "templates/src/placement.js"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
);
|
|
128
|
+
|
|
53
129
|
assert.deepEqual(findFileMutation("shell-web-page-home-settings-shell"), {
|
|
54
130
|
from: "templates/src/pages/home/settings.vue",
|
|
55
131
|
toSurface: "home",
|
|
@@ -65,14 +141,27 @@ test("shell-web descriptor metadata advertises home settings outlets and install
|
|
|
65
141
|
toSurface: "home",
|
|
66
142
|
toSurfacePath: "settings/index.vue",
|
|
67
143
|
ownership: "app",
|
|
68
|
-
reason: "Install shell-driven home settings
|
|
144
|
+
reason: "Install shell-driven home settings redirect so the starter settings shell lands on a real child page.",
|
|
69
145
|
category: "shell-web",
|
|
70
146
|
id: "shell-web-page-home-settings"
|
|
71
147
|
});
|
|
148
|
+
|
|
149
|
+
assert.deepEqual(findFileMutation("shell-web-page-home-settings-general"), {
|
|
150
|
+
from: "templates/src/pages/home/settings/general/index.vue",
|
|
151
|
+
toSurface: "home",
|
|
152
|
+
toSurfacePath: "settings/general/index.vue",
|
|
153
|
+
ownership: "app",
|
|
154
|
+
reason: "Install shell-driven general settings child page with a tiny browser-local shell preference example.",
|
|
155
|
+
category: "shell-web",
|
|
156
|
+
id: "shell-web-page-home-settings-general"
|
|
157
|
+
});
|
|
72
158
|
});
|
|
73
159
|
|
|
74
|
-
test("shell-web home starter page
|
|
160
|
+
test("shell-web home starter page relies on drawer navigation instead of dead feature buttons", async () => {
|
|
75
161
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "index.vue"), "utf8");
|
|
76
162
|
|
|
77
|
-
assert.match(source, /to
|
|
163
|
+
assert.match(source, /Use the navigation drawer to move around the shell\./);
|
|
164
|
+
assert.doesNotMatch(source, /\/home\/settings/);
|
|
165
|
+
assert.doesNotMatch(source, /\/console/);
|
|
166
|
+
assert.doesNotMatch(source, /\/auth\/signout/);
|
|
78
167
|
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createPinia } from "pinia";
|
|
4
|
+
import {
|
|
5
|
+
SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY,
|
|
6
|
+
readDrawerDefaultOpenPreference,
|
|
7
|
+
writeDrawerDefaultOpenPreference
|
|
8
|
+
} from "../src/client/composables/shellLayoutDrawerPreference.js";
|
|
9
|
+
import { useShellLayoutStore } from "../src/client/stores/useShellLayoutStore.js";
|
|
10
|
+
|
|
11
|
+
function createStorage(initial = {}) {
|
|
12
|
+
const values = new Map(Object.entries(initial));
|
|
13
|
+
return {
|
|
14
|
+
getItem(key) {
|
|
15
|
+
return values.has(key) ? values.get(key) : null;
|
|
16
|
+
},
|
|
17
|
+
setItem(key, value) {
|
|
18
|
+
values.set(key, String(value));
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("readDrawerDefaultOpenPreference defaults to true when storage is missing or empty", () => {
|
|
24
|
+
assert.equal(readDrawerDefaultOpenPreference({ storage: null }), true);
|
|
25
|
+
assert.equal(readDrawerDefaultOpenPreference({ storage: createStorage() }), true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("readDrawerDefaultOpenPreference reads explicit stored booleans", () => {
|
|
29
|
+
assert.equal(
|
|
30
|
+
readDrawerDefaultOpenPreference({
|
|
31
|
+
storage: createStorage({ [SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY]: "false" })
|
|
32
|
+
}),
|
|
33
|
+
false
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
assert.equal(
|
|
37
|
+
readDrawerDefaultOpenPreference({
|
|
38
|
+
storage: createStorage({ [SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY]: "true" })
|
|
39
|
+
}),
|
|
40
|
+
true
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("writeDrawerDefaultOpenPreference persists normalized boolean strings", () => {
|
|
45
|
+
const storage = createStorage();
|
|
46
|
+
|
|
47
|
+
writeDrawerDefaultOpenPreference(false, { storage });
|
|
48
|
+
assert.equal(storage.getItem(SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY), "false");
|
|
49
|
+
|
|
50
|
+
writeDrawerDefaultOpenPreference(true, { storage });
|
|
51
|
+
assert.equal(storage.getItem(SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY), "true");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("shell layout store keeps drawer state and default preference in sync", () => {
|
|
55
|
+
const pinia = createPinia();
|
|
56
|
+
const store = useShellLayoutStore(pinia);
|
|
57
|
+
|
|
58
|
+
store.setDrawerDefaultOpen(false);
|
|
59
|
+
assert.equal(store.drawerDefaultOpen, false);
|
|
60
|
+
assert.equal(store.drawerOpen, false);
|
|
61
|
+
|
|
62
|
+
store.toggleDrawer();
|
|
63
|
+
assert.equal(store.drawerOpen, true);
|
|
64
|
+
|
|
65
|
+
store.setDrawerOpen(false);
|
|
66
|
+
assert.equal(store.drawerOpen, false);
|
|
67
|
+
});
|