From a5b923fa0b514a93887e6cd4e861c17bcc7b82fc Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 3 Aug 2026 00:55:42 -0400 Subject: [PATCH] fix(ui): teleported nav must not outlive the screen that raised it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: the nav above the bottom bar — back buttons, the mesh tabs — stayed stuck across other screens. Cause is the KeepAlive work from phase 2, and specifically the half of it that is invisible from the view's own file. Main tabs are KeepAlive'd, so navigating DEACTIVATES a view instead of unmounting it. Content the view Teleports to is not in the view's DOM subtree, so deactivation does not remove it and it keeps rendering over the destination screen. Two offenders, matching the report exactly: - Mesh.vue teleports its mobile TAB BAR and its chat BACK BUTTON to , gated only on `mobileShowChat` — never on whether Mesh was the screen you were looking at. - components/BackButton.vue teleports the shared mobile back button with NO gate at all, so it leaked out of every view that uses it. Fixing the shared component fixes every caller at once: Vue propagates activated/deactivated from the KeepAlive boundary down through the subtree, so a child can guard itself. BaseModal already solved the transient-dialog half of this class in 204d4523 by closing on route change. That is the right fix for a dialog and the wrong one for chrome: a tab bar has no "closed" state to fall back to, and forcing one would lose the user's place. New useViewActive() composable instead — chrome is simply not rendered while its owner is off screen, and returns exactly as it was. THE PERFORMANCE IS NOT SACRIFICED, which was the explicit constraint. The Teleport is gated, not the view, so the instance stays cached and revisiting a tab is still instant. A test pins this: setup() must run exactly ONCE across a navigate-away-and-back round trip. If someone later "fixes" this by dropping KeepAlive, that test fails. Deliberately untouched: AppSession.vue, whose teleport is load-bearing — its own comment records that moving the iframe node reloads the app, and app-session is excluded from KeepAlive anyway so it cannot leak. Toasts, the app launcher and the connection banner are app-level rather than view-owned; gating those would be wrong. Verified: 3 new tests; full suite 105 files / 848 tests green. Co-Authored-By: Claude Opus 5 (1M context) --- neode-ui/src/components/BackButton.vue | 13 ++- .../__tests__/useViewActive.test.ts | 103 ++++++++++++++++++ neode-ui/src/composables/useViewActive.ts | 41 +++++++ neode-ui/src/views/Mesh.vue | 18 ++- 4 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 neode-ui/src/composables/__tests__/useViewActive.test.ts create mode 100644 neode-ui/src/composables/useViewActive.ts diff --git a/neode-ui/src/components/BackButton.vue b/neode-ui/src/components/BackButton.vue index eeb4d185..ed63a210 100644 --- a/neode-ui/src/components/BackButton.vue +++ b/neode-ui/src/components/BackButton.vue @@ -12,8 +12,14 @@ {{ label }} - - + +