diff --git a/neode-ui/src/views/dashboard/DashboardRouterView.vue b/neode-ui/src/views/dashboard/DashboardRouterView.vue
index b8135c53..c0356a83 100644
--- a/neode-ui/src/views/dashboard/DashboardRouterView.vue
+++ b/neode-ui/src/views/dashboard/DashboardRouterView.vue
@@ -1,126 +1,68 @@
-
+ :mobile-tab-padding-top="mobileTabPaddingTop"
+ :needs-mobile-back-button-space="needsMobileBackButtonSpace"
+ >
+
+
+
+
diff --git a/neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts b/neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts
index e10fc9a1..35c13707 100644
--- a/neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts
+++ b/neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts
@@ -101,6 +101,56 @@ describe('DashboardRouterView keep-alive behavior', () => {
expect(shouldKeepAlive({ path: '/dashboard/marketplace' })).toBe(true)
expect(shouldKeepAlive({ path: '/dashboard/marketplace/abc' })).toBe(false)
})
+
+ // Visual contract (Task 3 checkpoint regression): the padded default-shape
+ // wrapper must render INSIDE `div.view-wrapper`, never the other way around.
+ // dashboard-styles.css scopes every transition as a compound selector
+ // (`.slide-up-enter-active.view-wrapper`, …) and `.view-wrapper` is
+ // `absolute inset-0` — if `view-wrapper` ever moves onto the view root
+ // inside the padded wrapper, page margins break and the slide/depth
+ // transitions stop firing. This test pins the structure so that regression
+ // cannot silently return.
+ it('renders the padded default wrapper inside div.view-wrapper', async () => {
+ const router = makeRouter()
+ router.push('/dashboard/other')
+ await router.isReady()
+
+ const wrapper = mount(DashboardRouterView, {
+ props: { mobileTabPaddingTop: null, needsMobileBackButtonSpace: false },
+ global: { plugins: [router] },
+ })
+ await flushPromises()
+
+ const viewWrapper = wrapper.find('.view-wrapper')
+ expect(viewWrapper.exists()).toBe(true)
+
+ // view-wrapper is the bare transition surface — no padding classes on it.
+ expect(viewWrapper.classes()).not.toContain('px-4')
+ expect(viewWrapper.classes()).not.toContain('view-container')
+
+ // Its first child is the padded scroll container carrying the page margins.
+ const padded = viewWrapper.element.firstElementChild as HTMLElement
+ expect(padded).not.toBeNull()
+ for (const cls of [
+ 'absolute', 'inset-0', 'px-4', 'pt-4', 'md:pt-8', 'md:px-8',
+ 'overflow-y-auto', 'mobile-safe-top', 'dashboard-scroll-panel', 'mobile-scroll-pad',
+ ]) {
+ expect(padded.classList.contains(cls), `padded wrapper missing ${cls}`).toBe(true)
+ }
+
+ // The routed view sits inside the padded wrapper with its per-component
+ // classes, followed by the scroll-clearance spacer.
+ const view = wrapper.find('.other-stub')
+ expect(view.exists()).toBe(true)
+ expect(view.classes()).toContain('view-container')
+ expect(view.classes()).toContain('flex-none')
+ expect(padded.contains(view.element)).toBe(true)
+ const spacer = padded.querySelector('[aria-hidden="true"]')
+ expect(spacer).not.toBeNull()
+ expect(spacer!.classList.contains('shrink-0')).toBe(true)
+
+ wrapper.unmount()
+ })
})
describe('RefreshIndicator', () => {
diff --git a/neode-ui/src/views/dashboard/dashboardViewWrappers.ts b/neode-ui/src/views/dashboard/dashboardViewWrappers.ts
new file mode 100644
index 00000000..001be55e
Binary files /dev/null and b/neode-ui/src/views/dashboard/dashboardViewWrappers.ts differ