From 26687055163f102f6c61674872d0b4d2ff277fcc Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 30 Jul 2026 08:40:27 -0400 Subject: [PATCH] fix(02-02): restore page margins and slide transitions broken by KeepAlive restructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Task 3 checkpoint failed on the real preview: outer page margins broke and the up/down main-tab slide animations stopped playing. Root cause: the 02-02 restructure moved view-wrapper (absolute inset-0) onto each view's root inside the padded wrapper, and split navigation across two sibling Transitions behind a stable intermediate div — but dashboard-styles.css scopes every transition as a compound selector on .view-wrapper, which must be the keyed direct child of .perspective-container. - Restore the pre-02-02 rendered DOM exactly: single Transition whose child is a keyed div.view-wrapper containing the per-route wrapper shape - Re-integrate KeepAlive via statically-named per-route wrapper components (dashboardViewWrappers.ts) so the keyed div.view-wrapper is the cached component's own root; cache membership gated by :include on wrapper names derived from KEEP_ALIVE_PATHS - Drop the scroll-retention Map: scroll containers now live inside keyed/ cached wrappers, restoring the old reset-to-top behavior for non-kept routes - Pin the visual contract structurally in keepAliveTabs.test.ts (padded wrapper classes must render INSIDE div.view-wrapper) Co-Authored-By: Claude Fable 5 --- .../views/dashboard/DashboardRouterView.vue | 160 ++++++------------ .../dashboard/__tests__/keepAliveTabs.test.ts | 50 ++++++ .../views/dashboard/dashboardViewWrappers.ts | Bin 0 -> 5332 bytes 3 files changed, 101 insertions(+), 109 deletions(-) create mode 100644 neode-ui/src/views/dashboard/dashboardViewWrappers.ts 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 @@ 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 0000000000000000000000000000000000000000..001be55eb00817fa4be42ff5ffbeb15c3deb85b1 GIT binary patch literal 5332 zcmcIo?QYx35$$h1#q_oSDMys-+#oHOIDwqVh>`flu$1HmK@hFPmB}lLOK_LA)x^Mk zh(1Ff>^@1)?D9jB<@Qf&1d&DV&d!{fIdj&70bR%zny%sv2DN#h1ytMepOZ~&g!Qo?CB)8puXE4A&`t;^AP2QZ1>GbT~h(^DjjVELJ zAA|KrqRdIeIBZ(1o>VXPyeJ;VuWuNKCg5BKw-oKa3}o@)DJ?L0&i>o{tFbui$3SZl7)+O@6Li z&>@MLRw?rLjFL=C6VhSVK|7PRy(lRF1@KQ&9J4@#s0N+m8Nv@ZcK}`%Q_{+w|MP3w zSb%+a%@xzwV|+Fu)s)~Ltl$uiBvU3%vMWTGS1P;?W?KAXNGqI9WjZLbB#nbHBG|A>^8ObR^ z&*(~kUM&Xb|J4i)6(>2()=hY9sNd-%;M~H{cXHvssyVnvF~D6Tc^i-H_lrX16Mk_= z%sS*DEU&fmb~L*9Hhg#XarEtCIC(RMtFbE6s7+$MuRmDUiM153y-sIva6rGnGeC}X z+}c54qp;Cet>oJ1hPkb=irlsUtb{@Gb)OCf9eHa5M6rU;t#h9)hZo;2E=Q+lztS;z zY~m9}P*@%Bd~t>T3Mg{yyex}e{ZG&|;k6Dx%XbY{J=+iPyPf{tP3t?hE z$A2MGU7%{aAMYw|># zTHQiZC7@}ZzyjJs#tZaRWV6_z0|(AV=D5b(6tlA+S)fsDY|Ma|c8DaEfmUTPm(gAj zn*5%*(&Wr)AK^P3zI@$`e^?Y^-RF|>@7IvUuS>@-?x*mP#im=s@II4GjW=z?j0|ea zO&~0fSgd&#@bd){6@az@1wVfV@^G;+C?K!`FIr11O;{DI31bAieKd|JQxjOK>k$gT zGEdiG*GC@KK#FOC?YWl30Mc}WIl>5GI6;&sr~(|s$|aJRc&XcuWeMt{%<2WXD8$M- zs@a}D-KZAkPigX0%;g|VBB+Hwe*agOKyN6r5_HF0RYg>}U1MbjIUAfbie=C=7xIZ%3cG7yndw<#pV$YWpij6z?-gz;ls5cjeP-sp*B^?R1*nTZyIQjz)5~`mX2D zXDVqr5XIOw4tJwFm}|l|FHYUp%H6hacZkzxBSau%>-9Eo@igNcR8$=}qJktrV?1+Z z<+O=Rc{p+HA$uo#Ga6LYrXYSDt%7FC? zrX$-&q5V#G?hHa8GScVP_KBD;PavLDHqKGvt@RVfrW-aEkZPDv&PeqE`B~<^^|}Yg zbgFRExa&3nAZ3@9@13N=1o{sm1*6nR7U2Q^5?VlP)(LI0t&|v?-Jj>dhCn4<77GM~ z9<0PJh#j_ZBP%;! z(3eM~8QS7&wXT{?zNKDQ*&K>%l1gEY`38hOvB>uKltE>SH@*b$)izrn)4zUya2q|M zqo;TI?Ue2|EQH`f*K$$w)VY&nxFx`krgxk7d~WP-d}UWWmsnVv%9G2 z{d$*eRor_mLA>ny-R&Jr#`RD5e@XOiUs8S>JR!)5C$x$}eE!{j-r{oyz9xSaNYo~- zNh#922Rqb|P1C~!$l0oMP?t;;%)pX1Y4=|3Iplwop!?nNUsA^D0^gn}u=%Ru9IJh5 zzJfZ25W!84|8fHD!L;T^VV!CIjD6R|V3n#*Jsm@0FM~shgQuL7I5>Lj^~e)NB7!&p zU1lEo!W6i*fc>{CiD(CqwzMiH+;igQ-%Vh$M_()xeXaL8_OeBN*T~d!xl+kLK;5-M zN|nOh*I{6+YG^el?w*-R%-YITC&{7B%M8~-T=T$#2hewcyR6iszrX<{{+7jZV_p_` zX)MX_73!4}r)pa;Js^&(hP5spWAbcW?tG?q=v4$-PYK9FkaD{Rmzlka3e{}4hO|c< z!deRLRD*R~|7JB-AF|5FF??pSApg(RvU;?)yLOe~h9Y#?s+Kyl7{P3|S0BbpIGI%` JRBLhZ_&4HTqtXBX literal 0 HcmV?d00001