@jskit-ai/shell-web 0.1.147 → 0.1.149
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 +2 -2
- package/package.json +2 -2
- package/src/client/components/ShellLayout.vue +48 -5
- package/src/client/components/ShellMenuLinkItem.vue +2 -0
- package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +2 -0
- package/src/client/support/drawerPresentation.js +58 -0
- package/src/test/adaptiveShellSmoke.js +13 -0
- package/templates/src/pages/home/settings/general/index.vue +2 -2
- package/test/drawerPresentation.test.js +68 -0
- package/test/linkItemScaffoldContract.test.js +4 -0
- package/test/settingsPlacementContract.test.js +9 -2
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.149",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Web shell layout runtime with outlet-based placement contributions.",
|
|
7
7
|
dependsOn: [],
|
|
@@ -311,7 +311,7 @@ export default Object.freeze({
|
|
|
311
311
|
dependencies: {
|
|
312
312
|
runtime: {
|
|
313
313
|
"@mdi/js": "^7.4.47",
|
|
314
|
-
"@jskit-ai/kernel": "0.1.
|
|
314
|
+
"@jskit-ai/kernel": "0.1.147"
|
|
315
315
|
},
|
|
316
316
|
dev: {
|
|
317
317
|
"@playwright/test": "1.61.1"
|
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.149",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@mdi/js": "^7.4.47",
|
|
31
|
-
"@jskit-ai/kernel": "0.1.
|
|
31
|
+
"@jskit-ai/kernel": "0.1.147"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"pinia": "^3.0.4",
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
} from "vue";
|
|
10
10
|
import { useDisplay } from "vuetify";
|
|
11
11
|
import { useShellLayoutState } from "../composables/useShellLayoutState.js";
|
|
12
|
+
import {
|
|
13
|
+
resolveShellDrawerPresentation,
|
|
14
|
+
resolveShellDrawerToggleLabel
|
|
15
|
+
} from "../support/drawerPresentation.js";
|
|
12
16
|
import ShellOutlet from "./ShellOutlet.vue";
|
|
13
17
|
import ShellRouteTransition from "./ShellRouteTransition.vue";
|
|
14
18
|
|
|
@@ -28,6 +32,10 @@ const props = defineProps({
|
|
|
28
32
|
subtitle: {
|
|
29
33
|
type: String,
|
|
30
34
|
default: ""
|
|
35
|
+
},
|
|
36
|
+
desktopDrawerClosedMode: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: "rail"
|
|
31
39
|
}
|
|
32
40
|
});
|
|
33
41
|
|
|
@@ -47,6 +55,7 @@ const display = useDisplay();
|
|
|
47
55
|
const refreshRuntime = inject("jskit.shell-web.runtime.web-refresh.client", null);
|
|
48
56
|
const pullDistance = ref(0);
|
|
49
57
|
const pullRefreshing = ref(false);
|
|
58
|
+
const wideDrawerOpen = ref(Boolean(drawerDefaultOpen.value));
|
|
50
59
|
let activePull = null;
|
|
51
60
|
|
|
52
61
|
const PULL_REFRESH_TRIGGER_DISTANCE = 72;
|
|
@@ -63,6 +72,15 @@ const layoutClass = computed(() => {
|
|
|
63
72
|
return "expanded";
|
|
64
73
|
});
|
|
65
74
|
const isCompactLayout = computed(() => layoutClass.value === "compact");
|
|
75
|
+
const drawerPresentation = computed(() => resolveShellDrawerPresentation({
|
|
76
|
+
compact: isCompactLayout.value,
|
|
77
|
+
open: drawerOpen.value,
|
|
78
|
+
desktopClosedMode: props.desktopDrawerClosedMode
|
|
79
|
+
}));
|
|
80
|
+
const drawerToggleLabel = computed(() => resolveShellDrawerToggleLabel({
|
|
81
|
+
compact: isCompactLayout.value,
|
|
82
|
+
open: drawerOpen.value
|
|
83
|
+
}));
|
|
66
84
|
const pullProgress = computed(() =>
|
|
67
85
|
Math.min(100, Math.round((pullDistance.value / PULL_REFRESH_TRIGGER_DISTANCE) * 100))
|
|
68
86
|
);
|
|
@@ -79,10 +97,19 @@ const pullRefreshStyle = computed(() => ({
|
|
|
79
97
|
"--shell-pull-refresh-distance": `${Math.round(Math.min(pullDistance.value, PULL_REFRESH_MAX_DISTANCE))}px`
|
|
80
98
|
}));
|
|
81
99
|
|
|
100
|
+
watch(
|
|
101
|
+
drawerOpen,
|
|
102
|
+
(open) => {
|
|
103
|
+
if (!isCompactLayout.value) {
|
|
104
|
+
wideDrawerOpen.value = Boolean(open);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
82
109
|
watch(
|
|
83
110
|
isCompactLayout,
|
|
84
111
|
(compact) => {
|
|
85
|
-
setDrawerOpen(compact ? false :
|
|
112
|
+
setDrawerOpen(compact ? false : wideDrawerOpen.value);
|
|
86
113
|
},
|
|
87
114
|
{ immediate: true }
|
|
88
115
|
);
|
|
@@ -132,6 +159,12 @@ function handlePullPointerDown(event) {
|
|
|
132
159
|
};
|
|
133
160
|
}
|
|
134
161
|
|
|
162
|
+
function handleDrawerVisibilityChange(open) {
|
|
163
|
+
if (isCompactLayout.value || drawerPresentation.value.closedMode === "hidden" || open === false) {
|
|
164
|
+
setDrawerOpen(open);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
135
168
|
function handlePullPointerMove(event) {
|
|
136
169
|
if (!activePull || event.pointerId !== activePull.pointerId) {
|
|
137
170
|
return;
|
|
@@ -359,7 +392,10 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
359
392
|
>
|
|
360
393
|
<v-app-bar-nav-icon
|
|
361
394
|
class="shell-layout__nav-toggle"
|
|
362
|
-
|
|
395
|
+
data-testid="jskit-shell-nav-toggle"
|
|
396
|
+
:aria-label="drawerToggleLabel"
|
|
397
|
+
:aria-expanded="drawerOpen"
|
|
398
|
+
aria-controls="jskit-shell-drawer"
|
|
363
399
|
@click="toggleDrawer"
|
|
364
400
|
/>
|
|
365
401
|
|
|
@@ -400,17 +436,24 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
400
436
|
</div>
|
|
401
437
|
|
|
402
438
|
<v-navigation-drawer
|
|
403
|
-
|
|
439
|
+
id="jskit-shell-drawer"
|
|
440
|
+
:model-value="drawerPresentation.visible"
|
|
404
441
|
border
|
|
405
442
|
class="bg-surface"
|
|
406
443
|
data-testid="jskit-shell-drawer"
|
|
444
|
+
:data-presentation="drawerPresentation.kind"
|
|
407
445
|
:temporary="isCompactLayout"
|
|
408
446
|
:permanent="!isCompactLayout"
|
|
409
447
|
:width="248"
|
|
448
|
+
:rail="drawerPresentation.rail"
|
|
449
|
+
:rail-width="80"
|
|
450
|
+
@update:model-value="handleDrawerVisibilityChange"
|
|
410
451
|
>
|
|
411
|
-
<slot name="menu" :surface="resolvedSurface">
|
|
452
|
+
<slot name="menu" :surface="resolvedSurface" :rail="drawerPresentation.rail">
|
|
412
453
|
<v-list nav density="comfortable" class="pt-2">
|
|
413
|
-
<v-list-subheader class="text-uppercase text-caption">
|
|
454
|
+
<v-list-subheader v-if="!drawerPresentation.rail" class="text-uppercase text-caption">
|
|
455
|
+
{{ resolvedSurfaceLabel }}
|
|
456
|
+
</v-list-subheader>
|
|
414
457
|
<ShellOutlet
|
|
415
458
|
target="shell-layout:primary-menu"
|
|
416
459
|
default
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const DESKTOP_DRAWER_CLOSED_MODES = Object.freeze(["rail", "hidden"]);
|
|
2
|
+
|
|
3
|
+
function normalizeDesktopDrawerClosedMode(value = "rail") {
|
|
4
|
+
const normalized = String(value || "rail").trim().toLowerCase();
|
|
5
|
+
return DESKTOP_DRAWER_CLOSED_MODES.includes(normalized) ? normalized : "rail";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function resolveShellDrawerPresentation({
|
|
9
|
+
compact = false,
|
|
10
|
+
open = false,
|
|
11
|
+
desktopClosedMode = "rail"
|
|
12
|
+
} = {}) {
|
|
13
|
+
const closedMode = normalizeDesktopDrawerClosedMode(desktopClosedMode);
|
|
14
|
+
if (compact) {
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
visible: Boolean(open),
|
|
17
|
+
rail: false,
|
|
18
|
+
kind: "modal",
|
|
19
|
+
closedMode
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (open) {
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
visible: true,
|
|
25
|
+
rail: false,
|
|
26
|
+
kind: "drawer",
|
|
27
|
+
closedMode
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (closedMode === "hidden") {
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
visible: false,
|
|
33
|
+
rail: false,
|
|
34
|
+
kind: "hidden",
|
|
35
|
+
closedMode
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
visible: true,
|
|
40
|
+
rail: true,
|
|
41
|
+
kind: "rail",
|
|
42
|
+
closedMode
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveShellDrawerToggleLabel({ compact = false, open = false } = {}) {
|
|
47
|
+
if (compact) {
|
|
48
|
+
return open ? "Close navigation menu" : "Open navigation menu";
|
|
49
|
+
}
|
|
50
|
+
return open ? "Collapse navigation drawer" : "Expand navigation drawer";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
DESKTOP_DRAWER_CLOSED_MODES,
|
|
55
|
+
normalizeDesktopDrawerClosedMode,
|
|
56
|
+
resolveShellDrawerPresentation,
|
|
57
|
+
resolveShellDrawerToggleLabel
|
|
58
|
+
};
|
|
@@ -94,6 +94,11 @@ async function runAdaptiveShellSmokeCase({
|
|
|
94
94
|
|
|
95
95
|
await expect(page.getByTestId("jskit-shell-bottom-nav")).toBeVisible();
|
|
96
96
|
expect(await isElementVisibleInViewport(page, "jskit-shell-drawer")).toBe(false);
|
|
97
|
+
await page.getByTestId("jskit-shell-nav-toggle").click();
|
|
98
|
+
await expect(page.getByTestId("jskit-shell-drawer")).toBeVisible();
|
|
99
|
+
await expect(page.getByTestId("jskit-shell-drawer")).toHaveAttribute("data-presentation", "modal");
|
|
100
|
+
await page.keyboard.press("Escape");
|
|
101
|
+
expect(await isElementVisibleInViewport(page, "jskit-shell-drawer")).toBe(false);
|
|
97
102
|
|
|
98
103
|
const navButtonHeights = await page.getByTestId("jskit-shell-bottom-nav").locator(".v-btn").evaluateAll((buttons) =>
|
|
99
104
|
buttons.map((button) => button.getBoundingClientRect().height)
|
|
@@ -107,6 +112,14 @@ async function runAdaptiveShellSmokeCase({
|
|
|
107
112
|
await expect.poll(() => bootstrapRequests).toBeGreaterThan(bootstrapRequestsBeforePull);
|
|
108
113
|
} else {
|
|
109
114
|
await expect(page.getByTestId("jskit-shell-drawer")).toBeVisible();
|
|
115
|
+
await page.getByTestId("jskit-shell-nav-toggle").click();
|
|
116
|
+
await expect(page.getByTestId("jskit-shell-drawer")).toBeVisible();
|
|
117
|
+
await expect(page.getByTestId("jskit-shell-drawer")).toHaveAttribute("data-presentation", "rail");
|
|
118
|
+
const railWidth = await page.getByTestId("jskit-shell-drawer").evaluate(
|
|
119
|
+
(element) => element.getBoundingClientRect().width
|
|
120
|
+
);
|
|
121
|
+
expect(railWidth).toBeGreaterThanOrEqual(79);
|
|
122
|
+
expect(railWidth).toBeLessThanOrEqual(81);
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
125
|
|
|
@@ -19,7 +19,7 @@ const drawerDefaultOpenModel = computed({
|
|
|
19
19
|
<div>
|
|
20
20
|
<h2 class="text-h6 mb-2">Navigation</h2>
|
|
21
21
|
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
22
|
-
|
|
22
|
+
On wider screens, collapsed navigation remains available as a rail. Phone layouts close the drawer and keep primary navigation in the bottom bar.
|
|
23
23
|
</p>
|
|
24
24
|
</div>
|
|
25
25
|
|
|
@@ -28,7 +28,7 @@ const drawerDefaultOpenModel = computed({
|
|
|
28
28
|
color="primary"
|
|
29
29
|
inset
|
|
30
30
|
hide-details="auto"
|
|
31
|
-
label="
|
|
31
|
+
label="Start with expanded navigation on wider screens"
|
|
32
32
|
/>
|
|
33
33
|
</section>
|
|
34
34
|
</template>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
normalizeDesktopDrawerClosedMode,
|
|
5
|
+
resolveShellDrawerPresentation,
|
|
6
|
+
resolveShellDrawerToggleLabel
|
|
7
|
+
} from "../src/client/support/drawerPresentation.js";
|
|
8
|
+
|
|
9
|
+
test("wide closed navigation defaults to a visible Material navigation rail", () => {
|
|
10
|
+
assert.deepEqual(
|
|
11
|
+
resolveShellDrawerPresentation({ compact: false, open: false }),
|
|
12
|
+
{
|
|
13
|
+
visible: true,
|
|
14
|
+
rail: true,
|
|
15
|
+
kind: "rail",
|
|
16
|
+
closedMode: "rail"
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("compact closed navigation dismisses instead of retaining a rail", () => {
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
resolveShellDrawerPresentation({ compact: true, open: false }),
|
|
24
|
+
{
|
|
25
|
+
visible: false,
|
|
26
|
+
rail: false,
|
|
27
|
+
kind: "modal",
|
|
28
|
+
closedMode: "rail"
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("desktopDrawerClosedMode hidden preserves an explicitly hidden wide drawer", () => {
|
|
34
|
+
assert.equal(normalizeDesktopDrawerClosedMode("hidden"), "hidden");
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
resolveShellDrawerPresentation({
|
|
37
|
+
compact: false,
|
|
38
|
+
open: false,
|
|
39
|
+
desktopClosedMode: "hidden"
|
|
40
|
+
}),
|
|
41
|
+
{
|
|
42
|
+
visible: false,
|
|
43
|
+
rail: false,
|
|
44
|
+
kind: "hidden",
|
|
45
|
+
closedMode: "hidden"
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("open navigation renders as a full drawer and labels its toggle responsively", () => {
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
resolveShellDrawerPresentation({ compact: false, open: true }),
|
|
53
|
+
{
|
|
54
|
+
visible: true,
|
|
55
|
+
rail: false,
|
|
56
|
+
kind: "drawer",
|
|
57
|
+
closedMode: "rail"
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
assert.equal(
|
|
61
|
+
resolveShellDrawerToggleLabel({ compact: false, open: true }),
|
|
62
|
+
"Collapse navigation drawer"
|
|
63
|
+
);
|
|
64
|
+
assert.equal(
|
|
65
|
+
resolveShellDrawerToggleLabel({ compact: true, open: false }),
|
|
66
|
+
"Open navigation menu"
|
|
67
|
+
);
|
|
68
|
+
});
|
|
@@ -151,10 +151,14 @@ test("shell-web generic link items support the expected shared route and icon be
|
|
|
151
151
|
|
|
152
152
|
assert.match(shellMenuSource, /exact:\s*\{/);
|
|
153
153
|
assert.match(shellMenuSource, /:exact="props\.exact"/);
|
|
154
|
+
assert.match(shellMenuSource, /:aria-label="props\.label"/);
|
|
155
|
+
assert.match(shellMenuSource, /v-tooltip="props\.label"/);
|
|
154
156
|
assert.match(shellMenuSource, /class="shell-menu-link-item"/);
|
|
155
157
|
assert.match(shellMenuSource, /min-height:\s*48px/);
|
|
156
158
|
assert.match(shellSurfaceAwareSource, /exact:\s*\{/);
|
|
157
159
|
assert.match(shellSurfaceAwareSource, /:exact="props\.exact"/);
|
|
160
|
+
assert.match(shellSurfaceAwareSource, /:aria-label="props\.label"/);
|
|
161
|
+
assert.match(shellSurfaceAwareSource, /v-tooltip="props\.label"/);
|
|
158
162
|
assert.match(shellSurfaceAwareSource, /class="shell-menu-link-item"/);
|
|
159
163
|
assert.match(shellSurfaceAwareSource, /min-height:\s*48px/);
|
|
160
164
|
assert.match(shellTabSource, /icon:\s*\{/);
|
|
@@ -95,6 +95,12 @@ test("shell-web shell layout registers navigation at the app layout level", asyn
|
|
|
95
95
|
assert.match(source, /target="shell-layout:primary-menu"[\s\S]*default/);
|
|
96
96
|
assert.doesNotMatch(source, /target="shell-layout:primary-bottom-nav"[\s\S]*default/);
|
|
97
97
|
assert.match(source, /data-testid="jskit-shell-drawer"/);
|
|
98
|
+
assert.match(source, /desktopDrawerClosedMode/);
|
|
99
|
+
assert.match(source, /default: "rail"/);
|
|
100
|
+
assert.match(source, /:rail="drawerPresentation\.rail"/);
|
|
101
|
+
assert.match(source, /:rail-width="80"/);
|
|
102
|
+
assert.match(source, /:model-value="drawerPresentation\.visible"/);
|
|
103
|
+
assert.match(source, /data-testid="jskit-shell-nav-toggle"/);
|
|
98
104
|
assert.match(source, /data-testid="jskit-shell-bottom-nav"/);
|
|
99
105
|
assert.match(source, /padding:\s*0\.75rem 1rem calc\(1rem \+ env\(safe-area-inset-bottom, 0px\)\)/);
|
|
100
106
|
|
|
@@ -288,8 +294,9 @@ test("shell-web settings general child page exposes an adaptive drawer preferenc
|
|
|
288
294
|
assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-general-screen/);
|
|
289
295
|
assert.match(source, /drawerDefaultOpen/);
|
|
290
296
|
assert.match(source, /setDrawerDefaultOpen/);
|
|
291
|
-
assert.match(source, /
|
|
292
|
-
assert.match(source, /
|
|
297
|
+
assert.match(source, /collapsed navigation remains available as a rail/);
|
|
298
|
+
assert.match(source, /Phone layouts close the drawer/);
|
|
299
|
+
assert.match(source, /Start with expanded navigation on wider screens/);
|
|
293
300
|
assert.match(source, /min-height:\s*48px/);
|
|
294
301
|
assert.doesNotMatch(source, /live in this browser only|tiny example|starter settings/);
|
|
295
302
|
});
|