@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.
@@ -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.35",
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.36",
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 index stub scaffold for app-owned landing or redirect behavior.",
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.35",
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.36",
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">{{ resolvedSurfaceLabel }}</v-list-subheader>
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
- <div class="d-flex flex-wrap ga-3">
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
- // To redirect this settings shell to a child page, uncomment and edit the example below.
3
- // definePage({
4
- // redirect: (to) => `${to.path}/your_child_segment`
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 readSettingsOutlets() {
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() === "home-settings:primary-menu")
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 home settings index template is a simple developer-owned stub", async () => {
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, /definePage/);
37
- assert.match(source, /your_child_segment/);
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
- readSettingsOutlets(),
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 index stub scaffold for app-owned landing or redirect behavior.",
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 links to the home settings scaffold", async () => {
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="\/home\/settings"/);
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
+ });