@jskit-ai/shell-web 0.1.149 → 0.1.150
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/CHANGELOG.md +9 -0
- package/fixtures/adaptive-shell/index.html +12 -0
- package/fixtures/adaptive-shell/src/App.vue +27 -0
- package/fixtures/adaptive-shell/src/ScreenPage.vue +14 -0
- package/fixtures/adaptive-shell/src/main.js +197 -0
- package/fixtures/adaptive-shell/vite.config.mjs +17 -0
- package/package.descriptor.mjs +2 -2
- package/package.json +2 -2
- package/src/client/components/ShellLayout.vue +242 -15
- package/src/client/components/ShellMenuLinkItem.vue +18 -12
- package/src/client/components/ShellNavigationTooltip.vue +37 -0
- package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +18 -12
- package/src/client/support/drawerWidth.js +74 -0
- package/src/client/support/navigationLinkKeyboard.js +10 -0
- package/src/test/adaptiveShellSmoke.js +179 -16
- package/templates/src/components/ShellLayout.vue +1 -0
- package/test/adaptiveShell.browser.test.js +290 -0
- package/test/drawerWidth.test.js +48 -0
- package/test/linkItemScaffoldContract.test.js +18 -2
- package/test/navigationLinkKeyboard.test.js +36 -0
- package/test/playwrightContract.test.js +15 -0
- package/test/settingsPlacementContract.test.js +13 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.150 - 2026-08-13
|
|
4
|
+
|
|
5
|
+
- Give shell navigation tooltips one opaque, theme-owned Material color pair on hover and keyboard focus.
|
|
6
|
+
- Centre complete 48px navigation targets and their selected-state indicators in the collapsed desktop rail.
|
|
7
|
+
- Size the open drawer from its widest visible label with an approximately 10px end gap, while exposing `drawerWidth` and `railWidth` overrides.
|
|
8
|
+
- Dismiss compact navigation with the scrim or Escape and restore focus to the navigation toggle.
|
|
9
|
+
- Make the exported adaptive-shell Playwright helper follow rendered breakpoints and wait for stable drawer transitions and geometry.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>JSKIT shell-web browser contract</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, watchEffect } from "vue";
|
|
3
|
+
import { RouterView, useRoute } from "vue-router";
|
|
4
|
+
import { useTheme } from "vuetify";
|
|
5
|
+
import ShellLayout from "@jskit-ai/shell-web/client/components/ShellLayout";
|
|
6
|
+
|
|
7
|
+
const route = useRoute();
|
|
8
|
+
const theme = useTheme();
|
|
9
|
+
const surface = computed(() => route.path.startsWith("/w/") ? "admin" : "home");
|
|
10
|
+
const surfaceLabel = computed(() => surface.value === "admin" ? "Admin" : "Home");
|
|
11
|
+
const themeMode = computed(() => route.query.theme === "dark" ? "dark" : "light");
|
|
12
|
+
|
|
13
|
+
watchEffect(function applyFixtureTheme() {
|
|
14
|
+
const themeName = `${surface.value}-${themeMode.value}`;
|
|
15
|
+
theme.change(themeName);
|
|
16
|
+
document.body.dataset.surface = surface.value;
|
|
17
|
+
document.body.dataset.theme = themeMode.value;
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<v-app>
|
|
23
|
+
<ShellLayout :surface="surface" :surface-label="surfaceLabel">
|
|
24
|
+
<RouterView />
|
|
25
|
+
</ShellLayout>
|
|
26
|
+
</v-app>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useRoute } from "vue-router";
|
|
4
|
+
|
|
5
|
+
const route = useRoute();
|
|
6
|
+
const title = computed(() => String(route.meta.title || "Shell fixture"));
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<main class="generated-ui-screen" tabindex="-1">
|
|
11
|
+
<h1>{{ title }}</h1>
|
|
12
|
+
<p>Package-owned browser fixture for the adaptive shell contract.</p>
|
|
13
|
+
</main>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { createApp } from "vue";
|
|
2
|
+
import { createPinia } from "pinia";
|
|
3
|
+
import { createRouter, createWebHistory } from "vue-router";
|
|
4
|
+
import { createVuetify } from "vuetify";
|
|
5
|
+
import * as vuetifyComponents from "vuetify/components";
|
|
6
|
+
import * as vuetifyDirectives from "vuetify/directives";
|
|
7
|
+
import { aliases, mdi } from "vuetify/iconsets/mdi-svg";
|
|
8
|
+
import "vuetify/styles";
|
|
9
|
+
import App from "./App.vue";
|
|
10
|
+
import ScreenPage from "./ScreenPage.vue";
|
|
11
|
+
import ShellMenuLinkItem from "@jskit-ai/shell-web/client/components/ShellMenuLinkItem";
|
|
12
|
+
import ShellSurfaceAwareMenuLinkItem from "@jskit-ai/shell-web/client/components/ShellSurfaceAwareMenuLinkItem";
|
|
13
|
+
import ShellTabLinkItem from "@jskit-ai/shell-web/client/components/ShellTabLinkItem";
|
|
14
|
+
import { useShellLayoutStore } from "../../../src/client/stores/useShellLayoutStore.js";
|
|
15
|
+
import { createWebPlacementRuntime } from "../../../src/client/placement/runtime.js";
|
|
16
|
+
|
|
17
|
+
const COMPONENTS = new Map([
|
|
18
|
+
["fixture.menu-link", ShellMenuLinkItem],
|
|
19
|
+
["fixture.surface-menu-link", ShellSurfaceAwareMenuLinkItem],
|
|
20
|
+
["fixture.tab-link", ShellTabLinkItem]
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const placementContainer = Object.freeze({
|
|
24
|
+
has(token) {
|
|
25
|
+
return COMPONENTS.has(token);
|
|
26
|
+
},
|
|
27
|
+
make(token) {
|
|
28
|
+
return COMPONENTS.get(token);
|
|
29
|
+
},
|
|
30
|
+
resolveTag() {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function createSurfacePlacement(surface, order, label, suffix) {
|
|
36
|
+
return {
|
|
37
|
+
id: `fixture.${surface}.${label.toLowerCase()}`,
|
|
38
|
+
target: "shell.primary-nav",
|
|
39
|
+
kind: "link",
|
|
40
|
+
surfaces: [surface],
|
|
41
|
+
order,
|
|
42
|
+
props: {
|
|
43
|
+
label,
|
|
44
|
+
surface,
|
|
45
|
+
scopedSuffix: suffix,
|
|
46
|
+
unscopedSuffix: suffix,
|
|
47
|
+
exact: suffix === "/"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createSurfacePlacements(surface) {
|
|
53
|
+
return [
|
|
54
|
+
createSurfacePlacement(surface, 10, "Home", "/"),
|
|
55
|
+
createSurfacePlacement(surface, 20, "Assistant", "/assistant"),
|
|
56
|
+
createSurfacePlacement(surface, 30, "Contacts", "/contacts"),
|
|
57
|
+
createSurfacePlacement(surface, 40, "Bookings", "/bookings"),
|
|
58
|
+
createSurfacePlacement(surface, 50, "Rostering", "/rostering")
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const placements = [
|
|
63
|
+
...createSurfacePlacements("home"),
|
|
64
|
+
...createSurfacePlacements("admin"),
|
|
65
|
+
{
|
|
66
|
+
id: "fixture.help",
|
|
67
|
+
target: "shell.secondary-nav",
|
|
68
|
+
kind: "link",
|
|
69
|
+
componentToken: "fixture.menu-link",
|
|
70
|
+
surfaces: ["*"],
|
|
71
|
+
order: 100,
|
|
72
|
+
props: {
|
|
73
|
+
label: "Help and support",
|
|
74
|
+
to: "/help"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const menuRenderers = Object.freeze({ link: "fixture.surface-menu-link" });
|
|
80
|
+
const tabRenderers = Object.freeze({ link: "fixture.tab-link" });
|
|
81
|
+
const placementTopology = [
|
|
82
|
+
{
|
|
83
|
+
id: "shell.primary-nav",
|
|
84
|
+
surfaces: ["*"],
|
|
85
|
+
default: true,
|
|
86
|
+
variants: {
|
|
87
|
+
compact: { outlet: "shell-layout:primary-bottom-nav", renderers: tabRenderers },
|
|
88
|
+
medium: { outlet: "shell-layout:primary-menu", renderers: menuRenderers },
|
|
89
|
+
expanded: { outlet: "shell-layout:primary-menu", renderers: menuRenderers }
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "shell.secondary-nav",
|
|
94
|
+
surfaces: ["*"],
|
|
95
|
+
variants: {
|
|
96
|
+
compact: { outlet: "shell-layout:secondary-menu", renderers: menuRenderers },
|
|
97
|
+
medium: { outlet: "shell-layout:secondary-menu", renderers: menuRenderers },
|
|
98
|
+
expanded: { outlet: "shell-layout:secondary-menu", renderers: menuRenderers }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const placementRuntime = createWebPlacementRuntime({ app: placementContainer });
|
|
104
|
+
placementRuntime.replacePlacements(placements);
|
|
105
|
+
placementRuntime.replacePlacementTopology(placementTopology);
|
|
106
|
+
placementRuntime.setContext({
|
|
107
|
+
surfaceConfig: {
|
|
108
|
+
tenancyMode: "path",
|
|
109
|
+
defaultSurfaceId: "home",
|
|
110
|
+
enabledSurfaceIds: ["home", "admin"],
|
|
111
|
+
surfacesById: {
|
|
112
|
+
home: { id: "home", routeBase: "/home", enabled: true },
|
|
113
|
+
admin: { id: "admin", routeBase: "/w/:workspaceSlug/admin", enabled: true }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const routes = [
|
|
119
|
+
{ path: "/", redirect: "/home" },
|
|
120
|
+
{ path: "/home/:section?", component: ScreenPage, meta: { title: "Home surface" } },
|
|
121
|
+
{ path: "/w/:workspaceSlug/admin/:section?", component: ScreenPage, meta: { title: "Admin surface" } },
|
|
122
|
+
{ path: "/help", component: ScreenPage, meta: { title: "Help and support" } }
|
|
123
|
+
];
|
|
124
|
+
const router = createRouter({ history: createWebHistory(), routes });
|
|
125
|
+
const pinia = createPinia();
|
|
126
|
+
const vuetify = createVuetify({
|
|
127
|
+
components: vuetifyComponents,
|
|
128
|
+
directives: vuetifyDirectives,
|
|
129
|
+
icons: {
|
|
130
|
+
defaultSet: "mdi",
|
|
131
|
+
aliases,
|
|
132
|
+
sets: { mdi }
|
|
133
|
+
},
|
|
134
|
+
theme: {
|
|
135
|
+
defaultTheme: "home-light",
|
|
136
|
+
themes: {
|
|
137
|
+
"home-light": {
|
|
138
|
+
dark: false,
|
|
139
|
+
colors: {
|
|
140
|
+
background: "#F4F7F5",
|
|
141
|
+
surface: "#FFFFFF",
|
|
142
|
+
"on-surface": "#34413F",
|
|
143
|
+
"surface-variant": "#E2E8E5",
|
|
144
|
+
"on-surface-variant": "#52605F",
|
|
145
|
+
primary: "#006C51"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"admin-light": {
|
|
149
|
+
dark: false,
|
|
150
|
+
colors: {
|
|
151
|
+
background: "#F5F6F4",
|
|
152
|
+
surface: "#FFFFFF",
|
|
153
|
+
"on-surface": "#34413F",
|
|
154
|
+
"surface-variant": "#424242",
|
|
155
|
+
"on-surface-variant": "#52605F",
|
|
156
|
+
primary: "#006C51"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"home-dark": {
|
|
160
|
+
dark: true,
|
|
161
|
+
colors: {
|
|
162
|
+
background: "#101513",
|
|
163
|
+
surface: "#18201D",
|
|
164
|
+
"on-surface": "#E1E9E5",
|
|
165
|
+
"surface-variant": "#2B3632",
|
|
166
|
+
"on-surface-variant": "#BEC9C4",
|
|
167
|
+
primary: "#67DBB7"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"admin-dark": {
|
|
171
|
+
dark: true,
|
|
172
|
+
colors: {
|
|
173
|
+
background: "#111412",
|
|
174
|
+
surface: "#1A201D",
|
|
175
|
+
"on-surface": "#E2E8E4",
|
|
176
|
+
"surface-variant": "#333936",
|
|
177
|
+
"on-surface-variant": "#606864",
|
|
178
|
+
primary: "#67DBB7"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const app = createApp(App);
|
|
186
|
+
app.use(pinia);
|
|
187
|
+
app.use(router);
|
|
188
|
+
app.use(vuetify);
|
|
189
|
+
app.provide("jskit.shell-web.runtime.web-placement.client", placementRuntime);
|
|
190
|
+
app.provide("jskit.shell-web.runtime.web-refresh.client", {
|
|
191
|
+
async refresh() {
|
|
192
|
+
await fetch("/api/bootstrap?reason=pull-to-refresh");
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
useShellLayoutStore(pinia).setDrawerDefaultOpen(true);
|
|
196
|
+
await router.isReady();
|
|
197
|
+
app.mount("#app");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { fileURLToPath, URL } from "node:url";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
import vue from "@vitejs/plugin-vue";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
root: fileURLToPath(new URL(".", import.meta.url)),
|
|
7
|
+
resolve: {
|
|
8
|
+
preserveSymlinks: true,
|
|
9
|
+
alias: {
|
|
10
|
+
"@": fileURLToPath(new URL("./src", import.meta.url))
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
plugins: [vue()],
|
|
14
|
+
server: {
|
|
15
|
+
host: "127.0.0.1"
|
|
16
|
+
}
|
|
17
|
+
});
|
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.150",
|
|
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.148"
|
|
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.150",
|
|
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.148"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"pinia": "^3.0.4",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
computed,
|
|
4
4
|
inject,
|
|
5
|
+
nextTick,
|
|
5
6
|
onBeforeUnmount,
|
|
6
7
|
onMounted,
|
|
7
8
|
ref,
|
|
@@ -13,6 +14,14 @@ import {
|
|
|
13
14
|
resolveShellDrawerPresentation,
|
|
14
15
|
resolveShellDrawerToggleLabel
|
|
15
16
|
} from "../support/drawerPresentation.js";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_SHELL_DRAWER_WIDTH,
|
|
19
|
+
DEFAULT_SHELL_RAIL_WIDTH,
|
|
20
|
+
SHELL_DRAWER_LABEL_END_GAP,
|
|
21
|
+
normalizeShellDrawerWidth,
|
|
22
|
+
normalizeShellRailWidth,
|
|
23
|
+
resolveContentAwareDrawerWidth
|
|
24
|
+
} from "../support/drawerWidth.js";
|
|
16
25
|
import ShellOutlet from "./ShellOutlet.vue";
|
|
17
26
|
import ShellRouteTransition from "./ShellRouteTransition.vue";
|
|
18
27
|
|
|
@@ -36,6 +45,14 @@ const props = defineProps({
|
|
|
36
45
|
desktopDrawerClosedMode: {
|
|
37
46
|
type: String,
|
|
38
47
|
default: "rail"
|
|
48
|
+
},
|
|
49
|
+
drawerWidth: {
|
|
50
|
+
type: Number,
|
|
51
|
+
default: null
|
|
52
|
+
},
|
|
53
|
+
railWidth: {
|
|
54
|
+
type: Number,
|
|
55
|
+
default: DEFAULT_SHELL_RAIL_WIDTH
|
|
39
56
|
}
|
|
40
57
|
});
|
|
41
58
|
|
|
@@ -56,7 +73,13 @@ const refreshRuntime = inject("jskit.shell-web.runtime.web-refresh.client", null
|
|
|
56
73
|
const pullDistance = ref(0);
|
|
57
74
|
const pullRefreshing = ref(false);
|
|
58
75
|
const wideDrawerOpen = ref(Boolean(drawerDefaultOpen.value));
|
|
76
|
+
const navigationToggle = ref(null);
|
|
77
|
+
const navigationDrawer = ref(null);
|
|
78
|
+
const measuredDrawerWidth = ref(DEFAULT_SHELL_DRAWER_WIDTH);
|
|
59
79
|
let activePull = null;
|
|
80
|
+
let drawerContentObserver = null;
|
|
81
|
+
let drawerMeasurementFrame = null;
|
|
82
|
+
let shellMounted = false;
|
|
60
83
|
|
|
61
84
|
const PULL_REFRESH_TRIGGER_DISTANCE = 72;
|
|
62
85
|
const PULL_REFRESH_MAX_DISTANCE = 112;
|
|
@@ -81,6 +104,13 @@ const drawerToggleLabel = computed(() => resolveShellDrawerToggleLabel({
|
|
|
81
104
|
compact: isCompactLayout.value,
|
|
82
105
|
open: drawerOpen.value
|
|
83
106
|
}));
|
|
107
|
+
const resolvedDrawerWidth = computed(() => {
|
|
108
|
+
if (props.drawerWidth !== null && props.drawerWidth !== undefined) {
|
|
109
|
+
return normalizeShellDrawerWidth(props.drawerWidth);
|
|
110
|
+
}
|
|
111
|
+
return normalizeShellDrawerWidth(measuredDrawerWidth.value);
|
|
112
|
+
});
|
|
113
|
+
const resolvedRailWidth = computed(() => normalizeShellRailWidth(props.railWidth));
|
|
84
114
|
const pullProgress = computed(() =>
|
|
85
115
|
Math.min(100, Math.round((pullDistance.value / PULL_REFRESH_TRIGGER_DISTANCE) * 100))
|
|
86
116
|
);
|
|
@@ -99,26 +129,29 @@ const pullRefreshStyle = computed(() => ({
|
|
|
99
129
|
|
|
100
130
|
watch(
|
|
101
131
|
drawerOpen,
|
|
102
|
-
|
|
103
|
-
if (!isCompactLayout.value) {
|
|
104
|
-
wideDrawerOpen.value = Boolean(open);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
132
|
+
handleDrawerOpenChange
|
|
107
133
|
);
|
|
108
134
|
|
|
109
135
|
watch(
|
|
110
136
|
isCompactLayout,
|
|
111
|
-
|
|
112
|
-
setDrawerOpen(compact ? false : wideDrawerOpen.value);
|
|
113
|
-
},
|
|
137
|
+
handleLayoutClassChange,
|
|
114
138
|
{ immediate: true }
|
|
115
139
|
);
|
|
116
140
|
|
|
117
|
-
|
|
141
|
+
watch(drawerPresentation, scheduleDrawerWidthMeasurement, { flush: "post" });
|
|
142
|
+
watch(resolvedSurface, scheduleDrawerWidthMeasurement, { flush: "post" });
|
|
143
|
+
|
|
144
|
+
onMounted(attachShellListeners);
|
|
145
|
+
onBeforeUnmount(detachShellListeners);
|
|
146
|
+
|
|
147
|
+
function attachShellListeners() {
|
|
148
|
+
shellMounted = true;
|
|
118
149
|
if (typeof window !== "object") {
|
|
119
150
|
return;
|
|
120
151
|
}
|
|
121
152
|
|
|
153
|
+
window.addEventListener("keydown", handleShellKeydown);
|
|
154
|
+
window.addEventListener("resize", scheduleDrawerWidthMeasurement, { passive: true });
|
|
122
155
|
window.addEventListener("pointerdown", handlePullPointerDown, { capture: true, passive: true });
|
|
123
156
|
window.addEventListener("pointermove", handlePullPointerMove, { capture: true, passive: false });
|
|
124
157
|
window.addEventListener("pointerup", handlePullPointerEnd, { capture: true, passive: true });
|
|
@@ -127,13 +160,22 @@ onMounted(() => {
|
|
|
127
160
|
window.addEventListener("touchmove", handlePullTouchMove, { capture: true, passive: false });
|
|
128
161
|
window.addEventListener("touchend", handlePullTouchEnd, { capture: true, passive: true });
|
|
129
162
|
window.addEventListener("touchcancel", handlePullTouchCancel, { capture: true, passive: true });
|
|
130
|
-
});
|
|
131
163
|
|
|
132
|
-
|
|
164
|
+
if (document?.fonts) {
|
|
165
|
+
void document.fonts.ready.then(scheduleDrawerWidthMeasurement);
|
|
166
|
+
document.fonts.addEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
|
|
167
|
+
}
|
|
168
|
+
void nextTick().then(initializeDrawerMeasurement);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function detachShellListeners() {
|
|
172
|
+
shellMounted = false;
|
|
133
173
|
if (typeof window !== "object") {
|
|
134
174
|
return;
|
|
135
175
|
}
|
|
136
176
|
|
|
177
|
+
window.removeEventListener("keydown", handleShellKeydown);
|
|
178
|
+
window.removeEventListener("resize", scheduleDrawerWidthMeasurement);
|
|
137
179
|
window.removeEventListener("pointerdown", handlePullPointerDown, { capture: true });
|
|
138
180
|
window.removeEventListener("pointermove", handlePullPointerMove, { capture: true });
|
|
139
181
|
window.removeEventListener("pointerup", handlePullPointerEnd, { capture: true });
|
|
@@ -142,7 +184,135 @@ onBeforeUnmount(() => {
|
|
|
142
184
|
window.removeEventListener("touchmove", handlePullTouchMove, { capture: true });
|
|
143
185
|
window.removeEventListener("touchend", handlePullTouchEnd, { capture: true });
|
|
144
186
|
window.removeEventListener("touchcancel", handlePullTouchCancel, { capture: true });
|
|
145
|
-
|
|
187
|
+
document?.fonts?.removeEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
|
|
188
|
+
drawerContentObserver?.disconnect();
|
|
189
|
+
drawerContentObserver = null;
|
|
190
|
+
if (drawerMeasurementFrame !== null) {
|
|
191
|
+
window.cancelAnimationFrame(drawerMeasurementFrame);
|
|
192
|
+
drawerMeasurementFrame = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleDrawerOpenChange(open) {
|
|
197
|
+
if (!isCompactLayout.value) {
|
|
198
|
+
wideDrawerOpen.value = Boolean(open);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleLayoutClassChange(compact) {
|
|
203
|
+
setDrawerOpen(compact ? false : wideDrawerOpen.value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function handleShellKeydown(event) {
|
|
207
|
+
if (
|
|
208
|
+
event?.defaultPrevented ||
|
|
209
|
+
event?.key !== "Escape" ||
|
|
210
|
+
!isCompactLayout.value ||
|
|
211
|
+
!drawerOpen.value
|
|
212
|
+
) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
setDrawerOpen(false);
|
|
218
|
+
void nextTick().then(focusNavigationToggle);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function focusNavigationToggle() {
|
|
222
|
+
const toggleElement = navigationToggle.value?.$el || navigationToggle.value;
|
|
223
|
+
toggleElement?.focus?.({ preventScroll: true });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function initializeDrawerMeasurement() {
|
|
227
|
+
const drawerElement = resolveNavigationDrawerElement();
|
|
228
|
+
const drawerContent = drawerElement?.querySelector?.(".v-navigation-drawer__content") || drawerElement;
|
|
229
|
+
if (typeof MutationObserver === "function" && drawerContent) {
|
|
230
|
+
drawerContentObserver?.disconnect();
|
|
231
|
+
drawerContentObserver = new MutationObserver(scheduleDrawerWidthMeasurement);
|
|
232
|
+
drawerContentObserver.observe(drawerContent, {
|
|
233
|
+
childList: true,
|
|
234
|
+
characterData: true,
|
|
235
|
+
subtree: true
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
scheduleDrawerWidthMeasurement();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveNavigationDrawerElement() {
|
|
242
|
+
const exposedElement = navigationDrawer.value?.$el || navigationDrawer.value || null;
|
|
243
|
+
if (typeof exposedElement?.getBoundingClientRect === "function") {
|
|
244
|
+
return exposedElement;
|
|
245
|
+
}
|
|
246
|
+
if (typeof document === "object") {
|
|
247
|
+
return document.getElementById("jskit-shell-drawer");
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function scheduleDrawerWidthMeasurement() {
|
|
253
|
+
if (
|
|
254
|
+
!shellMounted ||
|
|
255
|
+
props.drawerWidth !== null && props.drawerWidth !== undefined ||
|
|
256
|
+
!drawerPresentation.value.visible ||
|
|
257
|
+
drawerPresentation.value.rail ||
|
|
258
|
+
typeof window !== "object"
|
|
259
|
+
) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (drawerMeasurementFrame !== null) {
|
|
264
|
+
window.cancelAnimationFrame(drawerMeasurementFrame);
|
|
265
|
+
}
|
|
266
|
+
drawerMeasurementFrame = window.requestAnimationFrame(measureDrawerContentWidth);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function measureDrawerContentWidth() {
|
|
270
|
+
drawerMeasurementFrame = null;
|
|
271
|
+
const drawerElement = resolveNavigationDrawerElement();
|
|
272
|
+
if (!drawerElement || typeof document !== "object") {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const drawerRect = drawerElement.getBoundingClientRect();
|
|
277
|
+
const drawerStyle = window.getComputedStyle(drawerElement);
|
|
278
|
+
const rightToLeft = drawerStyle.direction === "rtl";
|
|
279
|
+
const endBorderWidth = Number.parseFloat(
|
|
280
|
+
rightToLeft ? drawerStyle.borderLeftWidth : drawerStyle.borderRightWidth
|
|
281
|
+
) || 0;
|
|
282
|
+
const measurements = [];
|
|
283
|
+
|
|
284
|
+
for (const label of drawerElement.querySelectorAll(".v-list-item-title")) {
|
|
285
|
+
const labelStyle = window.getComputedStyle(label);
|
|
286
|
+
if (labelStyle.display === "none" || labelStyle.visibility === "hidden") {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const textRect = measureRenderedText(label);
|
|
290
|
+
if (!textRect || textRect.width <= 0) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
measurements.push({
|
|
294
|
+
logicalStart: rightToLeft
|
|
295
|
+
? drawerRect.right - textRect.right
|
|
296
|
+
: textRect.left - drawerRect.left,
|
|
297
|
+
textWidth: textRect.width
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const nextWidth = resolveContentAwareDrawerWidth(measurements, {
|
|
302
|
+
endGap: SHELL_DRAWER_LABEL_END_GAP + endBorderWidth
|
|
303
|
+
});
|
|
304
|
+
if (Math.abs(nextWidth - measuredDrawerWidth.value) >= 0.25) {
|
|
305
|
+
measuredDrawerWidth.value = nextWidth;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function measureRenderedText(element) {
|
|
310
|
+
const range = document.createRange();
|
|
311
|
+
range.selectNodeContents(element);
|
|
312
|
+
const rect = range.getBoundingClientRect();
|
|
313
|
+
range.detach?.();
|
|
314
|
+
return rect;
|
|
315
|
+
}
|
|
146
316
|
|
|
147
317
|
function handlePullPointerDown(event) {
|
|
148
318
|
if (!canStartPullRefresh(event)) {
|
|
@@ -391,6 +561,7 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
391
561
|
data-testid="jskit-shell-app-bar"
|
|
392
562
|
>
|
|
393
563
|
<v-app-bar-nav-icon
|
|
564
|
+
ref="navigationToggle"
|
|
394
565
|
class="shell-layout__nav-toggle"
|
|
395
566
|
data-testid="jskit-shell-nav-toggle"
|
|
396
567
|
:aria-label="drawerToggleLabel"
|
|
@@ -436,17 +607,23 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
436
607
|
</div>
|
|
437
608
|
|
|
438
609
|
<v-navigation-drawer
|
|
610
|
+
ref="navigationDrawer"
|
|
439
611
|
id="jskit-shell-drawer"
|
|
440
612
|
:model-value="drawerPresentation.visible"
|
|
441
613
|
border
|
|
442
|
-
class="bg-surface"
|
|
614
|
+
class="shell-layout__drawer bg-surface"
|
|
443
615
|
data-testid="jskit-shell-drawer"
|
|
616
|
+
:data-layout="layoutClass"
|
|
444
617
|
:data-presentation="drawerPresentation.kind"
|
|
618
|
+
:data-drawer-width-mode="props.drawerWidth === null || props.drawerWidth === undefined ? 'content' : 'fixed'"
|
|
619
|
+
:data-drawer-width="resolvedDrawerWidth"
|
|
620
|
+
:data-rail-width="resolvedRailWidth"
|
|
621
|
+
:style="{ '--shell-drawer-label-end-gap': `${SHELL_DRAWER_LABEL_END_GAP}px` }"
|
|
445
622
|
:temporary="isCompactLayout"
|
|
446
623
|
:permanent="!isCompactLayout"
|
|
447
|
-
:width="
|
|
624
|
+
:width="resolvedDrawerWidth"
|
|
448
625
|
:rail="drawerPresentation.rail"
|
|
449
|
-
:rail-width="
|
|
626
|
+
:rail-width="resolvedRailWidth"
|
|
450
627
|
@update:model-value="handleDrawerVisibilityChange"
|
|
451
628
|
>
|
|
452
629
|
<slot name="menu" :surface="resolvedSurface" :rail="drawerPresentation.rail">
|
|
@@ -538,6 +715,56 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
538
715
|
min-width: 48px;
|
|
539
716
|
}
|
|
540
717
|
|
|
718
|
+
.shell-layout__drawer :deep(.v-list-item-title) {
|
|
719
|
+
white-space: nowrap;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.shell-layout__drawer[data-presentation="drawer"] :deep(.v-list--nav),
|
|
723
|
+
.shell-layout__drawer[data-presentation="drawer"] :deep(.v-list-item) {
|
|
724
|
+
padding-inline-end: calc(var(--shell-drawer-label-end-gap) / 2);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item) {
|
|
728
|
+
grid-template-areas: "prepend";
|
|
729
|
+
grid-template-columns: minmax(0, 1fr);
|
|
730
|
+
inline-size: 100%;
|
|
731
|
+
min-height: 48px;
|
|
732
|
+
min-width: 48px;
|
|
733
|
+
padding-inline: 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend) {
|
|
737
|
+
inline-size: 100%;
|
|
738
|
+
justify-content: center;
|
|
739
|
+
min-width: 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend .v-list-item__spacer),
|
|
743
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__content),
|
|
744
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__append) {
|
|
745
|
+
display: none;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__overlay) {
|
|
749
|
+
border-radius: 999px;
|
|
750
|
+
inset-block: 8px;
|
|
751
|
+
inset-inline: 4px;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active > .v-list-item__overlay) {
|
|
755
|
+
background: color-mix(
|
|
756
|
+
in srgb,
|
|
757
|
+
rgb(var(--v-theme-primary)) 18%,
|
|
758
|
+
rgb(var(--v-theme-surface))
|
|
759
|
+
);
|
|
760
|
+
opacity: 1;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active .v-icon) {
|
|
764
|
+
color: rgb(var(--v-theme-primary));
|
|
765
|
+
opacity: 1;
|
|
766
|
+
}
|
|
767
|
+
|
|
541
768
|
.shell-layout__top-right {
|
|
542
769
|
max-width: min(45vw, 18rem);
|
|
543
770
|
overflow: hidden;
|