@jskit-ai/shell-web 0.1.35 → 0.1.36
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 +21 -4
- package/package.json +2 -2
- package/src/client/composables/shellLayoutDrawerPreference.js +43 -0
- package/src/client/composables/useShellLayoutState.js +16 -1
- package/templates/src/components/ShellLayout.vue +1 -1
- package/templates/src/pages/home/index.vue +1 -5
- package/templates/src/pages/home/settings/index.vue +34 -5
- package/templates/src/placement.js +29 -0
- package/test/settingsPlacementContract.test.js +59 -10
- package/test/useShellLayoutState.test.js +50 -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.36",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Web shell layout runtime with outlet-based placement contributions.",
|
|
7
7
|
dependsOn: [],
|
|
@@ -83,7 +83,24 @@ 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
|
+
]
|
|
87
104
|
}
|
|
88
105
|
}
|
|
89
106
|
},
|
|
@@ -92,7 +109,7 @@ export default Object.freeze({
|
|
|
92
109
|
runtime: {
|
|
93
110
|
"@mdi/js": "^7.4.47",
|
|
94
111
|
"@tanstack/vue-query": "^5.90.5",
|
|
95
|
-
"@jskit-ai/kernel": "0.1.
|
|
112
|
+
"@jskit-ai/kernel": "0.1.37",
|
|
96
113
|
"vuetify": "^4.0.0"
|
|
97
114
|
},
|
|
98
115
|
dev: {}
|
|
@@ -258,7 +275,7 @@ export default Object.freeze({
|
|
|
258
275
|
toSurface: "home",
|
|
259
276
|
toSurfacePath: "settings/index.vue",
|
|
260
277
|
ownership: "app",
|
|
261
|
-
reason: "Install shell-driven home settings
|
|
278
|
+
reason: "Install shell-driven home settings landing page with a tiny browser-local shell preference example.",
|
|
262
279
|
category: "shell-web",
|
|
263
280
|
id: "shell-web-page-home-settings"
|
|
264
281
|
}
|
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.36",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -24,7 +24,7 @@
|
|
|
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.37",
|
|
28
28
|
"vuetify": "^4.0.0"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -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
|
+
};
|
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
resolveSurfaceDefinitionFromPlacementContext,
|
|
8
8
|
resolveSurfaceIdFromPlacementPathname
|
|
9
9
|
} from "../placement/surfaceContext.js";
|
|
10
|
+
import {
|
|
11
|
+
readDrawerDefaultOpenPreference,
|
|
12
|
+
writeDrawerDefaultOpenPreference
|
|
13
|
+
} from "./shellLayoutDrawerPreference.js";
|
|
10
14
|
|
|
11
15
|
function toSurfaceLabel(surfaceId = "") {
|
|
12
16
|
const normalizedSurfaceId = String(surfaceId || "").trim().toLowerCase();
|
|
@@ -21,6 +25,16 @@ function toSurfaceLabel(surfaceId = "") {
|
|
|
21
25
|
.join(" ");
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
const drawerDefaultOpen = ref(readDrawerDefaultOpenPreference());
|
|
29
|
+
const drawerOpen = ref(drawerDefaultOpen.value);
|
|
30
|
+
|
|
31
|
+
function setDrawerDefaultOpen(open) {
|
|
32
|
+
const normalized = Boolean(open);
|
|
33
|
+
drawerDefaultOpen.value = normalized;
|
|
34
|
+
drawerOpen.value = normalized;
|
|
35
|
+
writeDrawerDefaultOpenPreference(normalized);
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
function useShellLayoutState(props = {}) {
|
|
25
39
|
let route = null;
|
|
26
40
|
try {
|
|
@@ -30,7 +44,6 @@ function useShellLayoutState(props = {}) {
|
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
const { context: placementContext } = useWebPlacementContext();
|
|
33
|
-
const drawerOpen = ref(true);
|
|
34
47
|
|
|
35
48
|
function toggleDrawer() {
|
|
36
49
|
drawerOpen.value = !drawerOpen.value;
|
|
@@ -78,7 +91,9 @@ function useShellLayoutState(props = {}) {
|
|
|
78
91
|
});
|
|
79
92
|
|
|
80
93
|
return Object.freeze({
|
|
94
|
+
drawerDefaultOpen,
|
|
81
95
|
drawerOpen,
|
|
96
|
+
setDrawerDefaultOpen,
|
|
82
97
|
toggleDrawer,
|
|
83
98
|
resolvedSurface,
|
|
84
99
|
resolvedSurfaceLabel
|
|
@@ -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>
|
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
});
|
|
6
15
|
</script>
|
|
7
16
|
|
|
8
|
-
<template
|
|
17
|
+
<template>
|
|
18
|
+
<section class="d-flex flex-column ga-4">
|
|
19
|
+
<div>
|
|
20
|
+
<h2 class="text-h6 mb-2">Shell settings</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>
|
|
@@ -10,3 +10,32 @@ 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
|
+
});
|
|
@@ -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,31 @@ 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 exposes a tiny browser-local drawer preference", async () => {
|
|
34
43
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "index.vue"), "utf8");
|
|
35
44
|
|
|
36
|
-
assert.match(source, /
|
|
37
|
-
assert.match(source, /
|
|
45
|
+
assert.match(source, /useShellLayoutState/);
|
|
46
|
+
assert.match(source, /drawerDefaultOpen/);
|
|
47
|
+
assert.match(source, /setDrawerDefaultOpen/);
|
|
48
|
+
assert.match(source, /Open navigation drawer by default/);
|
|
49
|
+
assert.match(source, /live in this browser only/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("shell-web placement template seeds default Home and Settings drawer navigation", async () => {
|
|
53
|
+
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "placement.js"), "utf8");
|
|
54
|
+
|
|
55
|
+
assert.match(source, /id: "shell-web\.home\.menu\.home"/);
|
|
56
|
+
assert.match(source, /target: "shell-layout:primary-menu"/);
|
|
57
|
+
assert.match(source, /label: "Home"/);
|
|
58
|
+
assert.match(source, /nonWorkspaceSuffix: "\/"/);
|
|
59
|
+
assert.match(source, /id: "shell-web\.home\.menu\.settings"/);
|
|
60
|
+
assert.match(source, /label: "Settings"/);
|
|
61
|
+
assert.match(source, /nonWorkspaceSuffix: "\/settings"/);
|
|
38
62
|
});
|
|
39
63
|
|
|
40
|
-
test("shell-web descriptor metadata advertises home settings outlets and installs the scaffold page", () => {
|
|
64
|
+
test("shell-web descriptor metadata advertises home settings outlets, default drawer links, and installs the scaffold page", () => {
|
|
41
65
|
assert.deepEqual(
|
|
42
|
-
|
|
66
|
+
readOutlets("home-settings:primary-menu"),
|
|
43
67
|
[
|
|
44
68
|
{
|
|
45
69
|
target: "home-settings:primary-menu",
|
|
@@ -50,6 +74,28 @@ test("shell-web descriptor metadata advertises home settings outlets and install
|
|
|
50
74
|
]
|
|
51
75
|
);
|
|
52
76
|
|
|
77
|
+
assert.deepEqual(
|
|
78
|
+
readContributions("shell-layout:primary-menu"),
|
|
79
|
+
[
|
|
80
|
+
{
|
|
81
|
+
id: "shell-web.home.menu.home",
|
|
82
|
+
target: "shell-layout:primary-menu",
|
|
83
|
+
surfaces: ["*"],
|
|
84
|
+
order: 50,
|
|
85
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
86
|
+
source: "templates/src/placement.js"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "shell-web.home.menu.settings",
|
|
90
|
+
target: "shell-layout:primary-menu",
|
|
91
|
+
surfaces: ["home"],
|
|
92
|
+
order: 100,
|
|
93
|
+
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
94
|
+
source: "templates/src/placement.js"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
);
|
|
98
|
+
|
|
53
99
|
assert.deepEqual(findFileMutation("shell-web-page-home-settings-shell"), {
|
|
54
100
|
from: "templates/src/pages/home/settings.vue",
|
|
55
101
|
toSurface: "home",
|
|
@@ -65,14 +111,17 @@ test("shell-web descriptor metadata advertises home settings outlets and install
|
|
|
65
111
|
toSurface: "home",
|
|
66
112
|
toSurfacePath: "settings/index.vue",
|
|
67
113
|
ownership: "app",
|
|
68
|
-
reason: "Install shell-driven home settings
|
|
114
|
+
reason: "Install shell-driven home settings landing page with a tiny browser-local shell preference example.",
|
|
69
115
|
category: "shell-web",
|
|
70
116
|
id: "shell-web-page-home-settings"
|
|
71
117
|
});
|
|
72
118
|
});
|
|
73
119
|
|
|
74
|
-
test("shell-web home starter page
|
|
120
|
+
test("shell-web home starter page relies on drawer navigation instead of dead feature buttons", async () => {
|
|
75
121
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "index.vue"), "utf8");
|
|
76
122
|
|
|
77
|
-
assert.match(source, /to
|
|
123
|
+
assert.match(source, /Use the navigation drawer to move around the shell\./);
|
|
124
|
+
assert.doesNotMatch(source, /\/home\/settings/);
|
|
125
|
+
assert.doesNotMatch(source, /\/console/);
|
|
126
|
+
assert.doesNotMatch(source, /\/auth\/signout/);
|
|
78
127
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY,
|
|
5
|
+
readDrawerDefaultOpenPreference,
|
|
6
|
+
writeDrawerDefaultOpenPreference
|
|
7
|
+
} from "../src/client/composables/shellLayoutDrawerPreference.js";
|
|
8
|
+
|
|
9
|
+
function createStorage(initial = {}) {
|
|
10
|
+
const values = new Map(Object.entries(initial));
|
|
11
|
+
return {
|
|
12
|
+
getItem(key) {
|
|
13
|
+
return values.has(key) ? values.get(key) : null;
|
|
14
|
+
},
|
|
15
|
+
setItem(key, value) {
|
|
16
|
+
values.set(key, String(value));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("readDrawerDefaultOpenPreference defaults to true when storage is missing or empty", () => {
|
|
22
|
+
assert.equal(readDrawerDefaultOpenPreference({ storage: null }), true);
|
|
23
|
+
assert.equal(readDrawerDefaultOpenPreference({ storage: createStorage() }), true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("readDrawerDefaultOpenPreference reads explicit stored booleans", () => {
|
|
27
|
+
assert.equal(
|
|
28
|
+
readDrawerDefaultOpenPreference({
|
|
29
|
+
storage: createStorage({ [SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY]: "false" })
|
|
30
|
+
}),
|
|
31
|
+
false
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
assert.equal(
|
|
35
|
+
readDrawerDefaultOpenPreference({
|
|
36
|
+
storage: createStorage({ [SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY]: "true" })
|
|
37
|
+
}),
|
|
38
|
+
true
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("writeDrawerDefaultOpenPreference persists normalized boolean strings", () => {
|
|
43
|
+
const storage = createStorage();
|
|
44
|
+
|
|
45
|
+
writeDrawerDefaultOpenPreference(false, { storage });
|
|
46
|
+
assert.equal(storage.getItem(SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY), "false");
|
|
47
|
+
|
|
48
|
+
writeDrawerDefaultOpenPreference(true, { storage });
|
|
49
|
+
assert.equal(storage.getItem(SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY), "true");
|
|
50
|
+
});
|