fix(neode-ui): float connection banners as overlay

The offline/reconnecting banners were in-flow (mx-6 mt-6) and pushed the whole
dashboard down when shown. Teleport them to <body> as a fixed, top-centered
overlay with a fade/slide transition and safe-area inset, so they no longer
shift layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-06-19 14:40:50 +01:00
parent 0f43870e6c
commit f636c5d505

View File

@ -1,25 +1,37 @@
<template> <template>
<!-- Offline Banner --> <Teleport to="body">
<div v-if="isOffline && !store.isReconnecting && store.isAuthenticated" class="path-option-card mx-6 mt-6 px-6 py-3 border-l-4 border-yellow-500"> <!-- Offline Banner -->
<div class="flex items-center gap-2 text-yellow-200"> <Transition name="conn-banner">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> v-if="isOffline && !store.isReconnecting && store.isAuthenticated"
</svg> class="conn-banner-overlay"
<span class="font-medium"> >
{{ isRestarting ? 'Server is restarting...' : isShuttingDown ? 'Server is shutting down...' : 'Connection lost' }} <div class="path-option-card px-6 py-3 border-l-4 border-yellow-500 inline-flex items-center gap-2 text-yellow-200 shadow-2xl">
</span> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</div> </svg>
<span class="font-medium">
{{ isRestarting ? 'Server is restarting...' : isShuttingDown ? 'Server is shutting down...' : 'Connection lost' }}
</span>
</div>
</div>
</Transition>
<!-- Reconnecting Banner --> <!-- Reconnecting Banner -->
<div v-if="store.isReconnecting && store.isAuthenticated" class="path-option-card mx-6 mt-6 px-6 py-3 border-l-4 border-blue-500"> <Transition name="conn-banner">
<div class="flex items-center gap-2 text-blue-200"> <div
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"> v-if="store.isReconnecting && store.isAuthenticated"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> class="conn-banner-overlay"
</svg> >
<span class="font-medium">Reconnecting...</span> <div class="path-option-card px-6 py-3 border-l-4 border-blue-500 inline-flex items-center gap-2 text-blue-200 shadow-2xl">
</div> <svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span class="font-medium">Reconnecting...</span>
</div>
</div>
</Transition>
</Teleport>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -32,3 +44,34 @@ const isOffline = computed(() => store.isOffline)
const isRestarting = computed(() => store.isRestarting) const isRestarting = computed(() => store.isRestarting)
const isShuttingDown = computed(() => store.isShuttingDown) const isShuttingDown = computed(() => store.isShuttingDown)
</script> </script>
<style scoped>
/* Float the connection banners over the UI instead of occupying layout space
* (which previously pushed the whole dashboard down when reconnecting).
* Pinned top-center, clear of the status bar via the safe-area inset that the
* Android companion app injects (--safe-area-top), falling back to env(). */
.conn-banner-overlay {
position: fixed;
top: calc(1rem + var(--safe-area-top, env(safe-area-inset-top, 0px)));
left: 50%;
z-index: 60;
transform: translateX(-50%);
max-width: calc(100% - 2rem);
pointer-events: none; /* purely informational — never intercept taps */
}
.conn-banner-enter-active,
.conn-banner-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease;
}
.conn-banner-enter-from,
.conn-banner-leave-to {
opacity: 0;
transform: translateX(-50%) translateY(-8px);
}
.conn-banner-enter-to,
.conn-banner-leave-from {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>