feat(app): keep all views alive after first visit, preserve scroll state

All route views now mount once and hide via CSS (.view-hidden) instead of
unmounting. Tenant pages wrapped in .page-scroll divs for scrollability.
Only LoginView stays in RouterView (no state to preserve).
TestsView updated to use active ref in provideToolbar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nico 2026-04-03 23:13:23 +02:00
parent d48612665b
commit ef5080783e
2 changed files with 25 additions and 14 deletions

View File

@ -12,11 +12,21 @@
<AppToolbar />
<div class="content-area">
<TtsPlayerBar />
<!-- Socket views: v-if=visited (lazy first mount), class-based hide (preserve scroll) -->
<!-- All views kept alive after first visit CSS hide preserves scroll/state -->
<AgentsView v-if="visited.nyx" :class="{ 'view-hidden': route.name !== 'nyx' }" />
<ViewerView v-if="ViewerView && visited.viewer" :class="{ 'view-hidden': route.name !== 'viewer' }" />
<!-- Non-socket views: scrollable -->
<div v-if="routerReady && !isSocketRoute" class="page-scroll">
<TestsView v-if="visited.tests" :class="{ 'view-hidden': route.name !== 'tests' }" />
<div v-if="visited.home" :class="['page-scroll', { 'view-hidden': route.name !== 'home' }]">
<component :is="HomePageAsync" />
</div>
<div v-if="visited.impressum" :class="['page-scroll', { 'view-hidden': route.name !== 'impressum' }]">
<component :is="ImpressumPageAsync" />
</div>
<div v-if="visited.datenschutz" :class="['page-scroll', { 'view-hidden': route.name !== 'datenschutz' }]">
<component :is="DatenschutzPageAsync" />
</div>
<!-- Login: no state to preserve -->
<div v-if="route.name === 'login'" class="page-scroll">
<RouterView />
</div>
</div>
@ -28,15 +38,13 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch } from 'vue';
import { ref, reactive, onMounted, computed, watch, defineAsyncComponent } from 'vue';
import { useRoute } from 'vue-router';
import { ws, auth, agents } from './store';
import { THEME_LOGOS, useTheme } from './composables/useTheme';
import { useChatStore } from './store/chat';
import tenant from './tenant';
import { defineAsyncComponent } from 'vue';
import GridOverlay from './components/GridOverlay.vue';
import BreakpointBadge from './components/BreakpointBadge.vue';
import AppSidebar from './components/AppSidebar.vue';
@ -44,18 +52,18 @@ import AppToolbar from './components/AppToolbar.vue';
import router from './router';
import TtsPlayerBar from './components/TtsPlayerBar.vue';
import { HomePage, ImpressumPage, DatenschutzPage } from './tenantPages';
const AgentsView = defineAsyncComponent(() => import('./views/AgentsView.vue'));
const TestsView = defineAsyncComponent(() => import('./views/TestsView.vue'));
const ViewerView = tenant.features.viewer
? defineAsyncComponent(() => import('./views/ViewerView.vue'))
: null;
const HomePageAsync = defineAsyncComponent(HomePage);
const ImpressumPageAsync = defineAsyncComponent(ImpressumPage);
const DatenschutzPageAsync = defineAsyncComponent(DatenschutzPage);
const chatStore = useChatStore();
const visited = reactive({ nyx: false, viewer: false });
const socketRoutes = ['nyx', ...(tenant.features.viewer ? ['viewer'] : [])];
const isSocketRoute = computed(() => socketRoutes.includes(route.name as string));
const routerReady = ref(false);
router.isReady().then(() => { routerReady.value = true; });
const visited = reactive({ nyx: false, viewer: false, tests: false, home: false, impressum: false, datenschutz: false });
const route = useRoute();
@ -68,7 +76,7 @@ const { theme } = useTheme();
function handleLogout() {
doLogout(disconnect);
visited.nyx = visited.viewer = false;
Object.keys(visited).forEach(k => (visited as any)[k] = false);
}
function maybeConnect() {

View File

@ -58,6 +58,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { getApiBase } from '../utils/apiBase';
import { relativeTime } from '../utils/relativeTime';
import { provideToolbar, useConnection } from '../composables/useToolbar';
@ -90,8 +91,10 @@ const updatedAt = ref('');
const expanded = ref<Set<string>>(new Set());
// SSE connection managed by useConnection, state surfaced in toolbar
const _route = useRoute();
const _viewActive = computed(() => _route.name === 'tests');
const testConn = useConnection(`${getApiBase()}/api/test-results`, handleResult);
provideToolbar({ groups: ['themes'], connection: testConn });
provideToolbar({ groups: ['themes'], connection: testConn }, _viewActive);
const displayResults = computed(() =>