@jskit-ai/shell-web 0.1.64 → 0.1.66
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 +200 -16
- package/package.json +8 -7
- package/src/client/components/ShellErrorHost.vue +88 -15
- package/src/client/components/ShellLayout.vue +551 -50
- package/src/client/components/ShellOutlet.vue +34 -4
- package/src/client/components/ShellOutletMenuWidget.vue +1 -8
- package/src/client/components/ShellRouteTransition.vue +480 -0
- package/src/client/components/ShellTabLinkItem.vue +22 -6
- package/src/client/composables/useShellLayoutState.js +12 -1
- package/src/client/error/normalize.js +17 -0
- package/src/client/error/policy.js +25 -11
- package/src/client/error/runtime.js +2 -0
- package/src/client/index.js +1 -0
- package/src/client/placement/index.js +5 -0
- package/src/client/placement/runtime.js +149 -16
- package/src/client/placement/validators.js +36 -8
- package/src/client/providers/ShellWebClientProvider.js +189 -24
- package/src/client/stores/useShellLayoutStore.js +21 -1
- package/src/test/adaptiveShellSmoke.js +121 -0
- package/templates/expected-existing/src/pages/home/index.vue +40 -10
- package/templates/src/components/ShellLayout.vue +10 -90
- package/templates/src/components/menus/TabLinkItem.vue +4 -0
- package/templates/src/error.js +7 -1
- package/templates/src/pages/home/index.vue +64 -23
- package/templates/src/pages/home/settings/general/index.vue +12 -9
- package/templates/src/pages/home/settings.vue +68 -24
- package/templates/src/placement.js +7 -6
- package/templates/src/placementTopology.js +149 -0
- package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
- package/test/errorRuntime.test.js +42 -0
- package/test/linkItemScaffoldContract.test.js +9 -2
- package/test/outletMenuWidgetContract.test.js +2 -2
- package/test/placementRegistry.test.js +3 -3
- package/test/placementRuntime.test.js +144 -14
- package/test/provider.test.js +97 -5
- package/test/settingsPlacementContract.test.js +234 -20
- package/test/useShellLayoutState.test.js +19 -0
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
export const useShellLayoutStore = defineStore("jskit.shell-web.layout", () => {
|
|
9
9
|
const drawerDefaultOpen = ref(readDrawerDefaultOpenPreference());
|
|
10
10
|
const drawerOpen = ref(drawerDefaultOpen.value);
|
|
11
|
+
const supportingContentOpen = ref(false);
|
|
12
|
+
const supportingContentTitle = ref("");
|
|
11
13
|
|
|
12
14
|
function setDrawerDefaultOpen(open) {
|
|
13
15
|
const normalized = Boolean(open);
|
|
@@ -24,11 +26,29 @@ export const useShellLayoutStore = defineStore("jskit.shell-web.layout", () => {
|
|
|
24
26
|
drawerOpen.value = !drawerOpen.value;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function openSupportingContent({ title = "" } = {}) {
|
|
30
|
+
supportingContentTitle.value = String(title || "").trim();
|
|
31
|
+
supportingContentOpen.value = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function closeSupportingContent() {
|
|
35
|
+
supportingContentOpen.value = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setSupportingContentOpen(open) {
|
|
39
|
+
supportingContentOpen.value = Boolean(open);
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
return {
|
|
28
43
|
drawerDefaultOpen,
|
|
29
44
|
drawerOpen,
|
|
45
|
+
supportingContentOpen,
|
|
46
|
+
supportingContentTitle,
|
|
30
47
|
setDrawerDefaultOpen,
|
|
31
48
|
setDrawerOpen,
|
|
32
|
-
toggleDrawer
|
|
49
|
+
toggleDrawer,
|
|
50
|
+
openSupportingContent,
|
|
51
|
+
closeSupportingContent,
|
|
52
|
+
setSupportingContentOpen
|
|
33
53
|
};
|
|
34
54
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = String(process.env.PLAYWRIGHT_BASE_URL || "http://127.0.0.1:5173").replace(/\/+$/u, "");
|
|
2
|
+
const DEFAULT_SMOKE_PATH = String(process.env.JSKIT_PLAYWRIGHT_SMOKE_PATH || "/home");
|
|
3
|
+
const DEFAULT_VIEWPORTS = Object.freeze([
|
|
4
|
+
Object.freeze({ name: "compact", width: 390, height: 844 }),
|
|
5
|
+
Object.freeze({ name: "medium", width: 768, height: 1024 }),
|
|
6
|
+
Object.freeze({ name: "expanded", width: 1280, height: 900 })
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
async function expectNoHorizontalOverflow(page, expect) {
|
|
10
|
+
const metrics = await page.evaluate(() => ({
|
|
11
|
+
clientWidth: document.documentElement.clientWidth,
|
|
12
|
+
scrollWidth: document.documentElement.scrollWidth
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth + 1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function expectGeneratedScreenContract(page, expect) {
|
|
19
|
+
const screen = page.locator(".generated-ui-screen").first();
|
|
20
|
+
|
|
21
|
+
await expect(screen).toBeVisible();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function pullToRefresh(page, expect) {
|
|
25
|
+
await page.evaluate(() => {
|
|
26
|
+
window.scrollTo(0, 0);
|
|
27
|
+
});
|
|
28
|
+
await page.dispatchEvent("body", "pointerdown", {
|
|
29
|
+
pointerId: 41,
|
|
30
|
+
pointerType: "touch",
|
|
31
|
+
isPrimary: true,
|
|
32
|
+
button: 0,
|
|
33
|
+
clientX: 180,
|
|
34
|
+
clientY: 90
|
|
35
|
+
});
|
|
36
|
+
await page.dispatchEvent("body", "pointermove", {
|
|
37
|
+
pointerId: 41,
|
|
38
|
+
pointerType: "touch",
|
|
39
|
+
isPrimary: true,
|
|
40
|
+
button: 0,
|
|
41
|
+
clientX: 184,
|
|
42
|
+
clientY: 250
|
|
43
|
+
});
|
|
44
|
+
await expect(page.getByTestId("jskit-shell-pull-refresh")).toBeVisible();
|
|
45
|
+
await page.dispatchEvent("body", "pointerup", {
|
|
46
|
+
pointerId: 41,
|
|
47
|
+
pointerType: "touch",
|
|
48
|
+
isPrimary: true,
|
|
49
|
+
button: 0,
|
|
50
|
+
clientX: 184,
|
|
51
|
+
clientY: 250
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function isElementVisibleInViewport(page, testId) {
|
|
56
|
+
return page.getByTestId(testId).evaluate((element) => {
|
|
57
|
+
const rect = element.getBoundingClientRect();
|
|
58
|
+
const style = window.getComputedStyle(element);
|
|
59
|
+
return (
|
|
60
|
+
style.display !== "none" &&
|
|
61
|
+
style.visibility !== "hidden" &&
|
|
62
|
+
rect.width > 0 &&
|
|
63
|
+
rect.height > 0 &&
|
|
64
|
+
rect.right > 0 &&
|
|
65
|
+
rect.left < window.innerWidth &&
|
|
66
|
+
rect.bottom > 0 &&
|
|
67
|
+
rect.top < window.innerHeight
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runAdaptiveShellSmoke({
|
|
73
|
+
test,
|
|
74
|
+
expect,
|
|
75
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
76
|
+
smokePath = DEFAULT_SMOKE_PATH,
|
|
77
|
+
viewports = DEFAULT_VIEWPORTS
|
|
78
|
+
} = {}) {
|
|
79
|
+
if (!test || !expect) {
|
|
80
|
+
throw new Error("runAdaptiveShellSmoke requires Playwright test and expect.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test.describe("generated adaptive shell smoke", () => {
|
|
84
|
+
for (const viewport of viewports) {
|
|
85
|
+
test(`${viewport.name} layout has reachable navigation and no horizontal overflow`, async ({ page }) => {
|
|
86
|
+
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
87
|
+
await page.goto(`${baseUrl}${smokePath}`);
|
|
88
|
+
await expect(page.locator("body")).toBeVisible();
|
|
89
|
+
await expectGeneratedScreenContract(page, expect);
|
|
90
|
+
await expectNoHorizontalOverflow(page, expect);
|
|
91
|
+
|
|
92
|
+
if (viewport.name === "compact") {
|
|
93
|
+
let bootstrapRequests = 0;
|
|
94
|
+
await page.route("**/api/bootstrap**", async (route) => {
|
|
95
|
+
bootstrapRequests += 1;
|
|
96
|
+
await route.continue();
|
|
97
|
+
});
|
|
98
|
+
const bootstrapRequestsBeforePull = bootstrapRequests;
|
|
99
|
+
|
|
100
|
+
await expect(page.getByTestId("jskit-shell-bottom-nav")).toBeVisible();
|
|
101
|
+
expect(await isElementVisibleInViewport(page, "jskit-shell-drawer")).toBe(false);
|
|
102
|
+
|
|
103
|
+
const navButtonHeights = await page.getByTestId("jskit-shell-bottom-nav").locator(".v-btn").evaluateAll((buttons) =>
|
|
104
|
+
buttons.map((button) => button.getBoundingClientRect().height)
|
|
105
|
+
);
|
|
106
|
+
expect(navButtonHeights.length).toBeGreaterThan(0);
|
|
107
|
+
for (const height of navButtonHeights) {
|
|
108
|
+
expect(height).toBeGreaterThanOrEqual(48);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await pullToRefresh(page, expect);
|
|
112
|
+
await expect.poll(() => bootstrapRequests).toBeGreaterThan(bootstrapRequestsBeforePull);
|
|
113
|
+
} else {
|
|
114
|
+
await expect(page.getByTestId("jskit-shell-drawer")).toBeVisible();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { DEFAULT_VIEWPORTS, runAdaptiveShellSmoke };
|
|
@@ -1,12 +1,42 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
<section class="generated-ui-screen generated-ui-screen--app home-start-screen d-flex flex-column ga-4">
|
|
3
|
+
<header>
|
|
4
|
+
<p class="text-overline text-medium-emphasis mb-1">Home</p>
|
|
5
|
+
<h1 class="home-start-screen__title">Home base</h1>
|
|
6
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
7
|
+
The app runtime is online and ready for the first real workflow.
|
|
8
|
+
</p>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<v-sheet rounded="lg" border class="home-start-screen__panel">
|
|
12
|
+
<h2 class="text-h6 mb-2">No activity yet</h2>
|
|
13
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
14
|
+
Recent work, saved records, and next actions will appear here once this app has data.
|
|
15
|
+
</p>
|
|
16
|
+
</v-sheet>
|
|
17
|
+
</section>
|
|
12
18
|
</template>
|
|
19
|
+
|
|
20
|
+
<style scoped>
|
|
21
|
+
.generated-ui-screen {
|
|
22
|
+
--generated-ui-screen-title-size: clamp(1.5rem, 3vw, 2.5rem);
|
|
23
|
+
--generated-ui-screen-panel-padding: 1rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.home-start-screen {
|
|
27
|
+
margin-inline: auto;
|
|
28
|
+
max-width: 48rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.home-start-screen__title {
|
|
32
|
+
font-size: var(--generated-ui-screen-title-size);
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
letter-spacing: -0.03em;
|
|
35
|
+
line-height: 1.08;
|
|
36
|
+
margin: 0 0 0.45rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.home-start-screen__panel {
|
|
40
|
+
padding: var(--generated-ui-screen-panel-padding);
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -1,92 +1,12 @@
|
|
|
1
|
-
<script
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
surfaceLabel: {
|
|
11
|
-
type: String,
|
|
12
|
-
default: ""
|
|
13
|
-
},
|
|
14
|
-
title: {
|
|
15
|
-
type: String,
|
|
16
|
-
default: ""
|
|
17
|
-
},
|
|
18
|
-
subtitle: {
|
|
19
|
-
type: String,
|
|
20
|
-
default: ""
|
|
1
|
+
<script>
|
|
2
|
+
import { h } from "vue";
|
|
3
|
+
import PackageShellLayout from "@jskit-ai/shell-web/client/components/ShellLayout";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: "ShellLayout",
|
|
7
|
+
inheritAttrs: false,
|
|
8
|
+
setup(_, { attrs, slots }) {
|
|
9
|
+
return () => h(PackageShellLayout, attrs, slots);
|
|
21
10
|
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useShellLayoutState(props);
|
|
11
|
+
};
|
|
25
12
|
</script>
|
|
26
|
-
|
|
27
|
-
<template>
|
|
28
|
-
<v-layout class="shell-layout border rounded-lg overflow-hidden">
|
|
29
|
-
<v-app-bar border density="comfortable" elevation="0" class="bg-surface">
|
|
30
|
-
<v-app-bar-nav-icon aria-label="Toggle navigation menu" @click="toggleDrawer" />
|
|
31
|
-
|
|
32
|
-
<slot name="top-left" :surface="resolvedSurface">
|
|
33
|
-
<div class="d-flex align-center ga-2">
|
|
34
|
-
<v-chip color="primary" size="small" label>{{ resolvedSurfaceLabel }}</v-chip>
|
|
35
|
-
<ShellOutlet target="shell-layout:top-left" />
|
|
36
|
-
</div>
|
|
37
|
-
</slot>
|
|
38
|
-
|
|
39
|
-
<v-spacer />
|
|
40
|
-
|
|
41
|
-
<slot name="top-right" :surface="resolvedSurface">
|
|
42
|
-
<div class="d-flex align-center ga-2">
|
|
43
|
-
<ShellOutlet target="shell-layout:top-right" />
|
|
44
|
-
</div>
|
|
45
|
-
</slot>
|
|
46
|
-
</v-app-bar>
|
|
47
|
-
|
|
48
|
-
<v-navigation-drawer v-model="drawerOpen" border class="bg-surface" :width="248">
|
|
49
|
-
<slot name="menu" :surface="resolvedSurface">
|
|
50
|
-
<v-list nav density="comfortable" class="pt-2">
|
|
51
|
-
<v-list-subheader class="text-uppercase text-caption">Navigation</v-list-subheader>
|
|
52
|
-
<ShellOutlet
|
|
53
|
-
target="shell-layout:primary-menu"
|
|
54
|
-
default
|
|
55
|
-
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
56
|
-
/>
|
|
57
|
-
<v-divider class="my-2" />
|
|
58
|
-
<ShellOutlet
|
|
59
|
-
target="shell-layout:secondary-menu"
|
|
60
|
-
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
61
|
-
/>
|
|
62
|
-
</v-list>
|
|
63
|
-
</slot>
|
|
64
|
-
</v-navigation-drawer>
|
|
65
|
-
|
|
66
|
-
<v-main class="bg-background">
|
|
67
|
-
<v-container fluid class="shell-layout__content">
|
|
68
|
-
<h1 v-if="title" class="shell-layout__title text-h5">{{ title }}</h1>
|
|
69
|
-
<p v-if="subtitle" class="shell-layout__subtitle text-body-2 text-medium-emphasis">{{ subtitle }}</p>
|
|
70
|
-
<slot />
|
|
71
|
-
</v-container>
|
|
72
|
-
</v-main>
|
|
73
|
-
</v-layout>
|
|
74
|
-
</template>
|
|
75
|
-
|
|
76
|
-
<style scoped>
|
|
77
|
-
.shell-layout {
|
|
78
|
-
min-height: 72vh;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.shell-layout__content {
|
|
82
|
-
padding: 0.75rem 1rem 1rem;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.shell-layout__title {
|
|
86
|
-
margin-bottom: 0.25rem;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.shell-layout__subtitle {
|
|
90
|
-
margin-bottom: 0.75rem;
|
|
91
|
-
}
|
|
92
|
-
</style>
|
package/templates/src/error.js
CHANGED
|
@@ -2,12 +2,18 @@ import { createDefaultErrorPolicy } from "@jskit-ai/shell-web/client/error";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* App-owned error handling contract.
|
|
5
|
+
* - intent chooses default presentation: resource-load, action-feedback, app-recoverable, blocking.
|
|
5
6
|
* - policy(event, ctx): decide channel + presenter + message.
|
|
6
7
|
* - defaultPresenterId: used when policy does not set presenterId.
|
|
7
8
|
* - presenters: optional custom presenters registered at boot.
|
|
8
9
|
*/
|
|
9
10
|
export default Object.freeze({
|
|
10
11
|
defaultPresenterId: "material.snackbar",
|
|
11
|
-
policy: createDefaultErrorPolicy(
|
|
12
|
+
policy: createDefaultErrorPolicy({
|
|
13
|
+
resourceLoadChannel: "silent",
|
|
14
|
+
actionFeedbackChannel: "snackbar",
|
|
15
|
+
appRecoverableChannel: "banner",
|
|
16
|
+
blockingChannel: "dialog"
|
|
17
|
+
}),
|
|
12
18
|
presenters: []
|
|
13
19
|
});
|
|
@@ -28,34 +28,75 @@ const health = computed(() => {
|
|
|
28
28
|
</script>
|
|
29
29
|
|
|
30
30
|
<template>
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<v-divider />
|
|
40
|
-
<v-card-text class="home-surface-card__body d-flex flex-column ga-3">
|
|
41
|
-
<div class="d-flex flex-wrap ga-3">
|
|
42
|
-
<v-chip color="secondary" variant="tonal" label>Route: /home</v-chip>
|
|
43
|
-
<v-chip color="info" variant="tonal" label>Health: {{ health }}</v-chip>
|
|
31
|
+
<section class="generated-ui-screen generated-ui-screen--app home-surface-screen d-flex flex-column ga-4">
|
|
32
|
+
<header class="home-surface-screen__header">
|
|
33
|
+
<div>
|
|
34
|
+
<p class="text-overline text-medium-emphasis mb-1">Home</p>
|
|
35
|
+
<h1 class="home-surface-screen__title">Ready</h1>
|
|
36
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
37
|
+
Core services are available.
|
|
38
|
+
</p>
|
|
44
39
|
</div>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
<v-btn color="primary" variant="flat" to="/home/settings/general">Settings</v-btn>
|
|
41
|
+
</header>
|
|
42
|
+
|
|
43
|
+
<v-sheet rounded="lg" border class="home-surface-screen__panel">
|
|
44
|
+
<div class="home-surface-screen__status">
|
|
45
|
+
<span class="text-caption text-medium-emphasis">Service health</span>
|
|
46
|
+
<strong>{{ health }}</strong>
|
|
47
|
+
</div>
|
|
48
|
+
<v-divider vertical class="d-none d-sm-block" />
|
|
49
|
+
<div class="home-surface-screen__status">
|
|
50
|
+
<span class="text-caption text-medium-emphasis">Route</span>
|
|
51
|
+
<strong>/home</strong>
|
|
52
|
+
</div>
|
|
53
|
+
</v-sheet>
|
|
54
|
+
</section>
|
|
51
55
|
</template>
|
|
52
56
|
|
|
53
57
|
<style scoped>
|
|
54
|
-
.
|
|
55
|
-
|
|
58
|
+
.generated-ui-screen {
|
|
59
|
+
--generated-ui-screen-title-size: clamp(1.5rem, 2.5vw, 2.25rem);
|
|
60
|
+
--generated-ui-screen-panel-padding: 1rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.home-surface-screen__header {
|
|
64
|
+
align-items: flex-start;
|
|
65
|
+
display: flex;
|
|
66
|
+
gap: 1rem;
|
|
67
|
+
justify-content: space-between;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.home-surface-screen__title {
|
|
71
|
+
font-size: var(--generated-ui-screen-title-size);
|
|
72
|
+
font-weight: 700;
|
|
73
|
+
letter-spacing: -0.03em;
|
|
74
|
+
line-height: 1.1;
|
|
75
|
+
margin: 0 0 0.4rem;
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
.home-surface-
|
|
59
|
-
|
|
78
|
+
.home-surface-screen__panel {
|
|
79
|
+
align-items: stretch;
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-wrap: wrap;
|
|
82
|
+
gap: 1rem;
|
|
83
|
+
padding: var(--generated-ui-screen-panel-padding);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.home-surface-screen__status {
|
|
87
|
+
display: grid;
|
|
88
|
+
gap: 0.15rem;
|
|
89
|
+
min-width: 9rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@media (max-width: 640px) {
|
|
93
|
+
.home-surface-screen__header {
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.home-surface-screen__header :deep(.v-btn) {
|
|
98
|
+
min-height: 48px;
|
|
99
|
+
width: 100%;
|
|
100
|
+
}
|
|
60
101
|
}
|
|
61
102
|
</style>
|
|
@@ -15,10 +15,12 @@ const drawerDefaultOpenModel = computed({
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
|
-
<section class="d-flex flex-column ga-4">
|
|
18
|
+
<section class="generated-ui-screen generated-ui-screen--settings settings-general-screen d-flex flex-column ga-4">
|
|
19
19
|
<div>
|
|
20
|
-
<h2 class="text-h6 mb-2">
|
|
21
|
-
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
20
|
+
<h2 class="text-h6 mb-2">Navigation</h2>
|
|
21
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
22
|
+
Choose the default behavior for wider screens. Phone layouts keep primary navigation in the bottom bar.
|
|
23
|
+
</p>
|
|
22
24
|
</div>
|
|
23
25
|
|
|
24
26
|
<v-switch
|
|
@@ -26,12 +28,13 @@ const drawerDefaultOpenModel = computed({
|
|
|
26
28
|
color="primary"
|
|
27
29
|
inset
|
|
28
30
|
hide-details="auto"
|
|
29
|
-
label="Open
|
|
31
|
+
label="Open drawer by default on wider screens"
|
|
30
32
|
/>
|
|
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
33
|
</section>
|
|
37
34
|
</template>
|
|
35
|
+
|
|
36
|
+
<style scoped>
|
|
37
|
+
.settings-general-screen :deep(.v-switch) {
|
|
38
|
+
min-height: 48px;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
@@ -4,29 +4,73 @@ import { RouterView } from "vue-router";
|
|
|
4
4
|
</script>
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<section class="settings-shell d-flex flex-column ga-4">
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
</v-col>
|
|
28
|
-
</v-row>
|
|
29
|
-
</v-card-text>
|
|
30
|
-
</v-card>
|
|
7
|
+
<section class="generated-ui-screen generated-ui-screen--settings settings-shell d-flex flex-column ga-4">
|
|
8
|
+
<header>
|
|
9
|
+
<p class="text-overline text-medium-emphasis mb-1">Settings</p>
|
|
10
|
+
<h1 class="settings-shell__title">Home settings</h1>
|
|
11
|
+
<p class="text-body-2 text-medium-emphasis mb-0">Configure shell behavior for this surface.</p>
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<v-sheet rounded="lg" border class="settings-shell__panel">
|
|
15
|
+
<div class="settings-shell__body">
|
|
16
|
+
<nav class="settings-shell__nav" aria-label="Home settings sections">
|
|
17
|
+
<v-list nav density="comfortable" rounded="lg" border>
|
|
18
|
+
<ShellOutlet target="home-settings:primary-menu" />
|
|
19
|
+
</v-list>
|
|
20
|
+
</nav>
|
|
21
|
+
|
|
22
|
+
<main class="settings-shell__content">
|
|
23
|
+
<RouterView />
|
|
24
|
+
</main>
|
|
25
|
+
</div>
|
|
26
|
+
</v-sheet>
|
|
31
27
|
</section>
|
|
32
28
|
</template>
|
|
29
|
+
|
|
30
|
+
<style scoped>
|
|
31
|
+
.generated-ui-screen {
|
|
32
|
+
--generated-ui-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
|
|
33
|
+
--generated-ui-screen-panel-padding: 1rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.settings-shell__title {
|
|
37
|
+
font-size: var(--generated-ui-screen-title-size);
|
|
38
|
+
font-weight: 650;
|
|
39
|
+
letter-spacing: -0.02em;
|
|
40
|
+
line-height: 1.15;
|
|
41
|
+
margin: 0 0 0.35rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.settings-shell__panel {
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.settings-shell__body {
|
|
49
|
+
display: grid;
|
|
50
|
+
gap: 1rem;
|
|
51
|
+
grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
|
|
52
|
+
padding: var(--generated-ui-screen-panel-padding);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.settings-shell__content {
|
|
56
|
+
min-width: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (max-width: 960px) {
|
|
60
|
+
.settings-shell__body {
|
|
61
|
+
grid-template-columns: 1fr;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.settings-shell__nav :deep(.v-list) {
|
|
65
|
+
display: flex;
|
|
66
|
+
gap: 0.25rem;
|
|
67
|
+
overflow-x: auto;
|
|
68
|
+
scrollbar-width: thin;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.settings-shell__nav :deep(.v-list-item) {
|
|
72
|
+
flex: 0 0 auto;
|
|
73
|
+
min-height: 48px;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -13,10 +13,10 @@ export default function getPlacements() {
|
|
|
13
13
|
|
|
14
14
|
addPlacement({
|
|
15
15
|
id: "shell-web.home.menu.home",
|
|
16
|
-
target: "shell
|
|
16
|
+
target: "shell.primary-nav",
|
|
17
|
+
kind: "link",
|
|
17
18
|
surfaces: ["home"],
|
|
18
19
|
order: 50,
|
|
19
|
-
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
20
20
|
props: {
|
|
21
21
|
label: "Home",
|
|
22
22
|
surface: "home",
|
|
@@ -28,10 +28,10 @@ addPlacement({
|
|
|
28
28
|
|
|
29
29
|
addPlacement({
|
|
30
30
|
id: "shell-web.home.menu.settings",
|
|
31
|
-
target: "shell
|
|
31
|
+
target: "shell.primary-nav",
|
|
32
|
+
kind: "link",
|
|
32
33
|
surfaces: ["home"],
|
|
33
34
|
order: 100,
|
|
34
|
-
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
35
35
|
props: {
|
|
36
36
|
label: "Settings",
|
|
37
37
|
surface: "home",
|
|
@@ -42,10 +42,11 @@ addPlacement({
|
|
|
42
42
|
|
|
43
43
|
addPlacement({
|
|
44
44
|
id: "shell-web.home.settings.general",
|
|
45
|
-
target: "
|
|
45
|
+
target: "page.section-nav",
|
|
46
|
+
owner: "home-settings",
|
|
47
|
+
kind: "link",
|
|
46
48
|
surfaces: ["home"],
|
|
47
49
|
order: 100,
|
|
48
|
-
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
49
50
|
props: {
|
|
50
51
|
label: "General",
|
|
51
52
|
surface: "home",
|