@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.
Files changed (37) hide show
  1. package/package.descriptor.mjs +200 -16
  2. package/package.json +8 -7
  3. package/src/client/components/ShellErrorHost.vue +88 -15
  4. package/src/client/components/ShellLayout.vue +551 -50
  5. package/src/client/components/ShellOutlet.vue +34 -4
  6. package/src/client/components/ShellOutletMenuWidget.vue +1 -8
  7. package/src/client/components/ShellRouteTransition.vue +480 -0
  8. package/src/client/components/ShellTabLinkItem.vue +22 -6
  9. package/src/client/composables/useShellLayoutState.js +12 -1
  10. package/src/client/error/normalize.js +17 -0
  11. package/src/client/error/policy.js +25 -11
  12. package/src/client/error/runtime.js +2 -0
  13. package/src/client/index.js +1 -0
  14. package/src/client/placement/index.js +5 -0
  15. package/src/client/placement/runtime.js +149 -16
  16. package/src/client/placement/validators.js +36 -8
  17. package/src/client/providers/ShellWebClientProvider.js +189 -24
  18. package/src/client/stores/useShellLayoutStore.js +21 -1
  19. package/src/test/adaptiveShellSmoke.js +121 -0
  20. package/templates/expected-existing/src/pages/home/index.vue +40 -10
  21. package/templates/src/components/ShellLayout.vue +10 -90
  22. package/templates/src/components/menus/TabLinkItem.vue +4 -0
  23. package/templates/src/error.js +7 -1
  24. package/templates/src/pages/home/index.vue +64 -23
  25. package/templates/src/pages/home/settings/general/index.vue +12 -9
  26. package/templates/src/pages/home/settings.vue +68 -24
  27. package/templates/src/placement.js +7 -6
  28. package/templates/src/placementTopology.js +149 -0
  29. package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
  30. package/test/errorRuntime.test.js +42 -0
  31. package/test/linkItemScaffoldContract.test.js +9 -2
  32. package/test/outletMenuWidgetContract.test.js +2 -2
  33. package/test/placementRegistry.test.js +3 -3
  34. package/test/placementRuntime.test.js +144 -14
  35. package/test/provider.test.js +97 -5
  36. package/test/settingsPlacementContract.test.js +234 -20
  37. 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
- <v-card class="mx-auto" max-width="960" rounded="xl" border elevation="1">
3
- <v-card-item class="px-6 py-5 px-md-8 py-md-7">
4
- <v-card-title class="text-h4">welcome</v-card-title>
5
- <v-card-subtitle class="text-subtitle-1 mt-2">starter app</v-card-subtitle>
6
- </v-card-item>
7
- <v-divider />
8
- <v-card-text class="px-6 py-5 px-md-8 py-md-7 text-body-1 text-medium-emphasis">
9
- Start by adding packages and pages to this app.
10
- </v-card-text>
11
- </v-card>
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 setup>
2
- import { useShellLayoutState } from "@jskit-ai/shell-web/client/composables/useShellLayoutState";
3
- import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
4
-
5
- const props = defineProps({
6
- surface: {
7
- type: String,
8
- default: ""
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>
@@ -29,6 +29,10 @@ const props = defineProps({
29
29
  disabled: {
30
30
  type: Boolean,
31
31
  default: false
32
+ },
33
+ exact: {
34
+ type: Boolean,
35
+ default: false
32
36
  }
33
37
  });
34
38
  </script>
@@ -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
- <v-card rounded="lg" elevation="1" border>
32
- <v-card-item class="home-surface-card__header">
33
- <template #prepend>
34
- <v-chip color="primary" size="small" label>Home</v-chip>
35
- </template>
36
- <v-card-title class="text-h5">welcome</v-card-title>
37
- <v-card-subtitle>Main public surface</v-card-subtitle>
38
- </v-card-item>
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
- <p class="text-medium-emphasis mb-0">
46
- This is your primary landing page. Replace this content with your actual product home.
47
- </p>
48
- <p class="text-body-2 text-medium-emphasis mb-0">Use the navigation drawer to move around the shell.</p>
49
- </v-card-text>
50
- </v-card>
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
- .home-surface-card__header {
55
- padding: 0.875rem 1rem;
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-card__body {
59
- padding: 0.875rem 1rem 1rem;
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">General</h2>
21
- <p class="text-body-2 text-medium-emphasis mb-0">These starter settings live in this browser only.</p>
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 navigation drawer by default"
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
- <v-card rounded="lg" elevation="1" border>
9
- <v-card-item>
10
- <v-card-title>Home settings</v-card-title>
11
- <v-card-subtitle>Manage settings pages for the home surface.</v-card-subtitle>
12
- </v-card-item>
13
- <v-divider />
14
- <v-card-text class="pt-4">
15
- <v-row no-gutters>
16
- <v-col cols="12" md="3" lg="2" class="pr-md-4 mb-4 mb-md-0">
17
- <v-list nav density="comfortable" rounded="lg" border>
18
- <ShellOutlet
19
- target="home-settings:primary-menu"
20
- default-link-component-token="local.main.ui.surface-aware-menu-link-item"
21
- />
22
- </v-list>
23
- </v-col>
24
-
25
- <v-col cols="12" md="9" lg="10">
26
- <RouterView />
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-layout:primary-menu",
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-layout:primary-menu",
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: "home-settings:primary-menu",
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",