archy/neode-ui/src/views/Dashboard.vue

1765 lines
57 KiB
Vue
Raw Normal View History

2026-01-24 22:59:20 +00:00
<template>
<div class="min-h-screen flex relative dashboard-view" :class="{ 'glass-throw-active': showZoomIn }">
<!-- Skip to main content link for keyboard users -->
<a href="#main-content" class="skip-to-content">Skip to main content</a>
<!-- Background container with 3D perspective - full width to avoid letterboxing -->
2026-01-24 22:59:20 +00:00
<div class="bg-perspective-container">
<!-- Background - primary layer (visible for all routes, transitions out only for detail pages) -->
<div
2026-01-24 22:59:20 +00:00
ref="bgDefault"
class="bg-layer bg-fullwidth"
:class="[
{ 'bg-transitioning-out': showAltBackground },
{ 'zoom-reveal-bg': showZoomIn }
]"
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
/>
<!-- Background - detail layer (only visible during app/marketplace detail 3D transition) -->
2026-01-24 22:59:20 +00:00
<div
ref="bgAlt"
class="bg-layer bg-fullwidth"
:class="{ 'bg-transitioning-in': showAltBackground }"
style="background-image: url(/assets/img/bg-intro-3.jpg)"
2026-01-24 22:59:20 +00:00
/>
<!-- Glitch overlays - trigger on background change -->
<div
class="bg-glitch-layer-1"
:class="{ 'glitch-active': isGlitching }"
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
2026-01-24 22:59:20 +00:00
/>
<div
class="bg-glitch-layer-2"
:class="{ 'glitch-active': isGlitching }"
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
2026-01-24 22:59:20 +00:00
/>
<div
class="bg-glitch-scan"
:class="{ 'glitch-active': isGlitching }"
/>
<!-- Glitch overlays removed only intro glitch plays (via isGlitching) -->
2026-01-24 22:59:20 +00:00
</div>
<!-- Oomph accent - brief impact flash when dashboard loads -->
<div
v-if="showZoomIn"
class="fixed inset-0 pointer-events-none z-[100] oomph-flash"
aria-hidden="true"
/>
<!-- Reveal flashes and glitch overlay - enthralling entrance -->
<div
v-if="showZoomIn"
class="fixed inset-0 pointer-events-none z-[99] reveal-flash-glitch"
aria-hidden="true"
/>
2026-01-24 22:59:20 +00:00
<!-- Background overlay - 0.3 opacity default, 0.8 opacity for Web5, Network, and Settings -->
<div
class="fixed inset-0 transition-opacity duration-500 pointer-events-none"
:class="showDarkOverlay ? 'bg-black/80' : 'bg-black/30'"
style="z-index: -5;"
/>
<!-- Sidebar - Desktop Only, animates in at end with separate parts -->
<aside
v-show="!chatFullscreen"
data-controller-zone="sidebar"
class="hidden md:flex w-[256px] flex-shrink-0 relative flex-col z-10"
:class="{ 'sidebar-animate': showZoomIn }"
>
<div class="sidebar-shell">
<div class="sidebar-inner flex flex-col min-h-full">
<div class="sidebar-logo flex items-center gap-3 mb-8 p-6 pb-0 shrink-0">
<AnimatedLogo />
<div class="min-w-0 flex-1">
<h2 class="text-lg font-semibold text-white truncate">{{ serverName }}</h2>
<p class="text-xs text-white/60">v{{ version }}</p>
</div>
2026-01-24 22:59:20 +00:00
</div>
<nav class="sidebar-nav flex-1 min-h-0 space-y-2 p-6 pt-4" aria-label="Main navigation">
<RouterLink
v-for="(item, idx) in desktopNavItems"
:key="item.path"
:to="item.path"
class="sidebar-nav-item flex items-center gap-3 px-4 py-3 rounded-lg text-white/80 hover:bg-white/10 hover:text-white transition-colors"
exact-active-class="nav-tab-active"
:style="{ '--nav-stagger': idx }"
>
<svg class="w-5 h-5" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-for="(path, index) in getIconPath(item.icon)"
:key="index"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="path"
/>
</svg>
<span>{{ item.label }}</span>
<span
v-if="item.path === '/dashboard/web5' && web5Badge.pendingRequestCount > 0"
class="ml-auto w-5 h-5 flex items-center justify-center rounded-full bg-orange-500 text-white text-[10px] font-bold"
>{{ web5Badge.pendingRequestCount }}</span>
</RouterLink>
<!-- Chat launcher button -->
<button
@click="router.push('/dashboard/chat')"
class="chat-launcher-btn w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-300"
>
<svg class="w-5 h-5" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-for="(path, index) in getIconPath('chat')" :key="index" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="path" />
</svg>
<span>Chat</span>
</button>
<!-- Logout - styled as nav item, below Settings -->
<button
@click="handleLogout"
class="sidebar-logout-btn w-full flex items-center gap-3 px-4 py-3 rounded-lg text-white/80 hover:bg-white/10 hover:text-white transition-colors"
>
<svg class="w-5 h-5" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
<span>Logout</span>
</button>
</nav>
<div class="sidebar-controller px-6 pb-2 shrink-0">
<ControllerIndicator />
</div>
<!-- Online status -->
<div class="px-6 pb-2 shrink-0">
<div class="rounded-lg bg-white/5 border border-white/10 px-4 py-2.5">
<OnlineStatusPill />
</div>
</div>
<!-- Mode switcher -->
<div class="px-6 pb-6 shrink-0">
<ModeSwitcher />
</div>
</div>
2026-01-24 22:59:20 +00:00
</div>
</aside>
<!-- Main Content (Xbox: Right goes here from sidebar) -->
<main
id="main-content"
data-controller-zone="main"
class="flex-1 overflow-hidden relative pb-20 md:pb-0 glass-piece z-10"
:class="{ 'glass-throw-main': showZoomIn }"
>
<div data-controller-main-entry class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
<!-- Controller zone entry point - no switcher -->
</div>
2026-01-24 22:59:20 +00:00
<!-- Connection Status Banner -->
<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">
<div class="flex items-center gap-2 text-yellow-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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" />
</svg>
<span class="font-medium">
{{ isRestarting ? 'Server is restarting...' : isShuttingDown ? 'Server is shutting down...' : 'Connection lost' }}
</span>
</div>
</div>
<!-- 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">
<div class="flex items-center gap-2 text-blue-200">
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
<!-- Persistent Mobile Tabs for Apps/Marketplace - glass piece 3 -->
2026-01-24 22:59:20 +00:00
<div
v-if="showAppsTabs"
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2 glass-piece"
:class="{ 'glass-throw-mobile-tabs': showZoomIn }"
2026-01-24 22:59:20 +00:00
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
>
<div class="glass-card p-2 rounded-lg flex gap-2 relative">
<!-- Animated Active Indicator -->
<div
class="absolute top-2 bottom-2 rounded-lg bg-white/20 transition-all duration-300 ease-out"
:style="{
left: `${appsTabIndicatorLeft}px`,
width: `${appsTabIndicatorWidth}px`,
}"
></div>
<RouterLink
ref="appsTabRef"
to="/dashboard/apps"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/'))
}"
>
My Apps
</RouterLink>
<RouterLink
ref="marketplaceTabRef"
to="/dashboard/marketplace"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/'))
}"
>
App Store
</RouterLink>
</div>
</div>
<!-- Persistent Mobile Tabs for Network/Cloud - glass piece 4 -->
2026-01-24 22:59:20 +00:00
<div
v-if="showNetworkTabs"
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2 glass-piece"
:class="{ 'glass-throw-mobile-tabs-2': showZoomIn }"
2026-01-24 22:59:20 +00:00
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
:style="{ top: showAppsTabs ? '80px' : '0' }"
>
<div class="glass-card p-2 rounded-lg flex gap-2 relative">
<!-- Animated Active Indicator -->
<div
class="absolute top-2 bottom-2 rounded-lg bg-white/20 transition-all duration-300 ease-out"
:style="{
left: `${networkTabIndicatorLeft}px`,
width: `${networkTabIndicatorWidth}px`,
}"
></div>
<RouterLink
ref="cloudTabRef"
to="/dashboard/cloud"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/cloud' || route.path.startsWith('/dashboard/cloud/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/cloud' || route.path.startsWith('/dashboard/cloud/'))
}"
>
Cloud
</RouterLink>
<RouterLink
ref="serverTabRef"
to="/dashboard/server"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/server' || route.path.startsWith('/dashboard/server/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/server' || route.path.startsWith('/dashboard/server/'))
}"
>
Network
</RouterLink>
</div>
</div>
<div class="perspective-container-wrapper glass-piece" :class="{ 'glass-throw-content': showZoomIn && !isHomeRoute }">
2026-01-24 22:59:20 +00:00
<div class="perspective-container">
<RouterView v-slot="{ Component, route }">
<Transition :name="getTransitionName(route)">
<div :key="route.path" class="view-wrapper">
<div
v-if="route.path === '/dashboard/chat'"
class="h-full"
>
<component :is="Component" />
</div>
<div
v-else
2026-01-24 22:59:20 +00:00
:class="[
'px-4 pt-4 md:pt-8 md:px-8 overflow-y-auto h-full',
needsMobileBackButtonSpace
? 'pb-[calc(var(--mobile-tab-bar-height,_72px)+96px)] md:pb-8'
: 'pb-4 md:pb-8'
2026-01-24 22:59:20 +00:00
]"
:style="mobileTabPaddingTop ? { paddingTop: (mobileTabPaddingTop + 16) + 'px' } : undefined"
2026-01-24 22:59:20 +00:00
>
<component :is="Component" class="view-container" />
</div>
</div>
</Transition>
</RouterView>
</div>
</div>
</main>
<!-- Mobile Bottom Tab Bar - glass piece 5 -->
2026-01-24 22:59:20 +00:00
<nav
ref="mobileTabBar"
data-mobile-tab-bar
aria-label="Mobile navigation"
class="md:hidden fixed bottom-0 left-0 right-0 border-t border-glass-border shadow-glass z-50 glass-piece"
:class="{ 'glass-throw-tabbar': showZoomIn }"
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); padding-bottom: env(safe-area-inset-bottom, 0px);"
2026-01-24 22:59:20 +00:00
>
<div class="flex justify-around items-center px-2 py-3 relative">
<RouterLink
v-for="item in mobileNavItems"
:key="item.path"
:to="item.path"
class="flex flex-col items-center justify-center w-full py-1.5 rounded-lg text-white/70 transition-all duration-300 relative z-10 gap-0.5"
2026-01-24 22:59:20 +00:00
:class="{
'nav-tab-active': item.isCombined
2026-01-24 22:59:20 +00:00
? (item.path === '/dashboard/apps'
? (route.path.includes('/apps') || route.path.includes('/marketplace'))
: (route.path.includes('/cloud') || route.path.includes('/server')))
: undefined
}"
:exact-active-class="item.isCombined ? undefined : 'nav-tab-active'"
>
<svg class="w-6 h-6 transition-all duration-300" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-for="(path, index) in getIconPath(item.icon)"
2026-01-24 22:59:20 +00:00
:key="index"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="path"
2026-01-24 22:59:20 +00:00
/>
</svg>
<span class="text-[10px] leading-tight">{{ item.label }}</span>
2026-01-24 22:59:20 +00:00
</RouterLink>
<!-- Chat launcher -->
<button
@click="router.push('/dashboard/chat')"
class="chat-launcher-btn-mobile flex flex-col items-center justify-center w-full py-1.5 rounded-lg transition-all duration-300 relative z-10 gap-0.5"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-for="(path, index) in getIconPath('chat')" :key="index" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="path" />
</svg>
<span class="text-[10px] leading-tight">Chat</span>
</button>
2026-01-24 22:59:20 +00:00
</div>
</nav>
<!-- Health Notifications Toast -->
<div
v-if="healthNotifications.length > 0"
class="fixed top-4 right-4 z-[200] flex flex-col gap-2 max-w-sm"
>
<div
v-for="notif in healthNotifications"
:key="notif.id"
class="p-3 rounded-xl border backdrop-blur-lg shadow-lg"
:class="notif.level === 'error'
? 'bg-red-500/15 border-red-500/30'
: notif.level === 'warning'
? 'bg-yellow-500/15 border-yellow-500/30'
: 'bg-blue-500/15 border-blue-500/30'"
>
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mt-0.5 shrink-0" :class="notif.level === 'error' ? 'text-red-400' : notif.level === 'warning' ? 'text-yellow-400' : 'text-blue-400'" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white">{{ notif.title }}</p>
<p class="text-xs text-white/60 mt-0.5">{{ notif.message }}</p>
</div>
<button
class="text-white/40 hover:text-white/80 transition-colors shrink-0"
@click="dismissNotification(notif.id)"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
2026-01-24 22:59:20 +00:00
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { RouterLink, RouterView, useRouter, useRoute, type RouteLocationNormalizedLoaded } from 'vue-router'
2026-01-24 22:59:20 +00:00
import { useAppStore } from '../stores/app'
import { useLoginTransitionStore } from '../stores/loginTransition'
import AnimatedLogo from '@/components/AnimatedLogo.vue'
import OnlineStatusPill from '@/components/OnlineStatusPill.vue'
import ControllerIndicator from '@/components/ControllerIndicator.vue'
import ModeSwitcher from '@/components/ModeSwitcher.vue'
import { useUIModeStore } from '@/stores/uiMode'
import { playDashboardLoadOomph } from '@/composables/useLoginSounds'
import { useWeb5BadgeStore } from '@/stores/web5Badge'
2026-01-24 22:59:20 +00:00
const uiMode = useUIModeStore()
// Chat fullscreen: hide sidebar + mobile nav when on /dashboard/chat (any mode)
const chatFullscreen = computed(() => route.path === '/dashboard/chat')
2026-01-24 22:59:20 +00:00
const router = useRouter()
const route = useRoute()
const store = useAppStore()
const loginTransition = useLoginTransitionStore()
const web5Badge = useWeb5BadgeStore()
const showZoomIn = ref(false)
const pendingTimers: ReturnType<typeof setTimeout>[] = []
function scheduledTimeout(fn: () => void, ms: number) {
const id = setTimeout(fn, ms)
pendingTimers.push(id)
return id
}
function isDetailRoute(path: string) {
return (path.includes('/apps/') && !path.endsWith('/apps')) ||
(path.includes('/marketplace/') && !path.endsWith('/marketplace'))
}
2026-01-24 22:59:20 +00:00
const showAltBackground = ref(false)
const isHomeRoute = computed(() => route.path === '/dashboard' || route.path === '/dashboard/')
2026-01-24 22:59:20 +00:00
const isGlitching = ref(false)
const ROUTE_BACKGROUNDS: Record<string, string> = {
'/dashboard': 'bg-home.jpg',
'/dashboard/': 'bg-home.jpg',
'/dashboard/apps': 'bg-myapps.jpg',
'/dashboard/marketplace': 'bg-appstore.jpg',
'/dashboard/cloud': 'bg-cloud.jpg',
'/dashboard/server': 'bg-network.jpg',
'/dashboard/web5': 'bg-web5.jpg',
'/dashboard/settings': 'bg-settings.jpg',
'/dashboard/chat': 'bg-home.jpg',
}
const backgroundImage = computed(() => {
if (isDetailRoute(route.path)) return 'bg-intro.jpg'
return ROUTE_BACKGROUNDS[route.path] || 'bg-home.jpg'
2026-01-24 22:59:20 +00:00
})
const isDarkRoute = computed(() => {
const p = route.path
return p.includes('/dashboard/web5') ||
p.includes('/dashboard/server') ||
p.includes('/dashboard/settings') ||
(p.includes('/dashboard/apps') && !isDetailRoute(p)) ||
p.includes('/dashboard/marketplace') ||
p.includes('/dashboard/cloud')
2026-01-24 22:59:20 +00:00
})
const showDarkOverlay = ref(isDarkRoute.value)
let overlayTimer: ReturnType<typeof setTimeout> | null = null
watch(isDarkRoute, (dark) => {
if (overlayTimer) { clearTimeout(overlayTimer); overlayTimer = null }
if (dark) {
showDarkOverlay.value = true
} else {
overlayTimer = scheduledTimeout(() => { showDarkOverlay.value = false }, 450)
}
2026-01-24 22:59:20 +00:00
})
2026-01-24 22:59:20 +00:00
const mobileTabBar = ref<HTMLElement | null>(null)
const appsTabRef = ref<HTMLElement | null>(null)
const marketplaceTabRef = ref<HTMLElement | null>(null)
const appsTabIndicatorLeft = ref(0)
const appsTabIndicatorWidth = ref(0)
const serverTabRef = ref<HTMLElement | null>(null)
const cloudTabRef = ref<HTMLElement | null>(null)
const networkTabIndicatorLeft = ref(0)
const networkTabIndicatorWidth = ref(0)
watch(() => route.path, (newPath) => {
const isAppDetails = isDetailRoute(newPath)
const wasAppDetails = showAltBackground.value
2026-01-24 22:59:20 +00:00
showAltBackground.value = isAppDetails
2026-01-24 22:59:20 +00:00
if (isAppDetails && !wasAppDetails) {
scheduledTimeout(() => {
2026-01-24 22:59:20 +00:00
isGlitching.value = true
scheduledTimeout(() => { isGlitching.value = false }, 375)
}, 500)
2026-01-24 22:59:20 +00:00
}
})
const needsMobileBackButtonSpace = computed(() => isDetailRoute(route.path))
// Show persistent tabs for Apps/Marketplace on mobile
const showAppsTabs = computed(() => {
if (typeof window === 'undefined') return false
if (window.innerWidth >= 768) return false
return route.path.includes('/apps') || route.path.includes('/marketplace')
})
// Show persistent tabs for Network/Cloud on mobile
const showNetworkTabs = computed(() => {
if (typeof window === 'undefined') return false
if (window.innerWidth >= 768) return false
if (route.name === 'cloud-folder') return false
2026-01-24 22:59:20 +00:00
return route.path.includes('/server') || route.path.includes('/cloud')
})
// Top padding for content div to clear fixed mobile tab overlays
const mobileTabPaddingTop = computed(() => {
if (typeof window === 'undefined' || window.innerWidth >= 768) return 0
if (showAppsTabs.value && showNetworkTabs.value) return 160
if (showAppsTabs.value || showNetworkTabs.value) return 80
return 0
})
2026-01-24 22:59:20 +00:00
function updateTabBarHeight() {
if (typeof window === 'undefined') return
if (mobileTabBar.value) {
const height = mobileTabBar.value.offsetHeight
document.documentElement.style.setProperty('--mobile-tab-bar-height', `${height}px`)
}
}
function updateAppsTabIndicator() {
if (typeof window === 'undefined' || window.innerWidth >= 768) return
const isAppsActive = route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/')
const activeTab = isAppsActive ? appsTabRef.value : marketplaceTabRef.value
if (!activeTab) return
const container = activeTab.parentElement
if (!container) return
const containerRect = container.getBoundingClientRect()
const tabRect = activeTab.getBoundingClientRect()
appsTabIndicatorLeft.value = tabRect.left - containerRect.left
appsTabIndicatorWidth.value = tabRect.width
}
function updateNetworkTabIndicator() {
if (typeof window === 'undefined' || window.innerWidth >= 768) return
const isCloudActive = route.path === '/dashboard/cloud' || route.path.startsWith('/dashboard/cloud/')
const activeTab = isCloudActive ? cloudTabRef.value : serverTabRef.value
if (!activeTab) return
const container = activeTab.parentElement
if (!container) return
const containerRect = container.getBoundingClientRect()
const tabRect = activeTab.getBoundingClientRect()
networkTabIndicatorLeft.value = tabRect.left - containerRect.left
networkTabIndicatorWidth.value = tabRect.width
}
function onResize() {
updateTabBarHeight()
updateAppsTabIndicator()
updateNetworkTabIndicator()
}
2026-01-24 22:59:20 +00:00
onMounted(() => {
document.body.classList.add('dashboard-active')
if (loginTransition.justLoggedIn) {
playDashboardLoadOomph()
showZoomIn.value = true
loginTransition.setPendingWelcomeTyping(true)
loginTransition.setJustLoggedIn(false)
const triggerRevealGlitch = () => {
isGlitching.value = true
scheduledTimeout(() => { isGlitching.value = false }, 380)
}
scheduledTimeout(triggerRevealGlitch, 500)
scheduledTimeout(triggerRevealGlitch, 1200)
scheduledTimeout(triggerRevealGlitch, 2000)
scheduledTimeout(() => { showZoomIn.value = false }, 8000)
scheduledTimeout(() => {
loginTransition.setStartWelcomeTyping(true)
loginTransition.setPendingWelcomeTyping(false)
}, 4000)
}
onResize()
window.addEventListener('resize', onResize)
window.addEventListener('keydown', handleKioskShortcuts)
web5Badge.refresh()
2026-01-24 22:59:20 +00:00
})
onBeforeUnmount(() => {
document.body.classList.remove('dashboard-active')
window.removeEventListener('resize', onResize)
window.removeEventListener('keydown', handleKioskShortcuts)
for (const id of pendingTimers) clearTimeout(id)
pendingTimers.length = 0
if (overlayTimer) { clearTimeout(overlayTimer); overlayTimer = null }
2026-01-24 22:59:20 +00:00
})
// Kiosk keyboard shortcuts (active when kiosk=true in localStorage or query param)
function isKioskMode(): boolean {
try {
return localStorage.getItem('kiosk') === 'true' || new URLSearchParams(window.location.search).has('kiosk')
} catch { return false }
}
function handleKioskShortcuts(e: KeyboardEvent) {
if (!isKioskMode()) return
if (e.ctrlKey && e.shiftKey) {
if (e.key === 'R' || e.key === 'r') {
e.preventDefault()
router.push('/recovery')
} else if (e.key === 'H' || e.key === 'h') {
e.preventDefault()
router.push('/dashboard')
} else if (e.key === 'Q' || e.key === 'q') {
e.preventDefault()
if (confirm('Reboot the server?')) {
fetch('/rpc/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'system.reboot' }) }).catch(() => {})
}
}
}
}
2026-01-24 22:59:20 +00:00
// Watch route changes to update indicator position
watch(() => route.path, () => {
nextTick(() => {
updateAppsTabIndicator()
updateNetworkTabIndicator()
})
})
const serverName = computed(() => store.serverName)
const version = computed(() => store.serverInfo?.version || '0.0.0')
const isOffline = computed(() => store.isOffline)
const isRestarting = computed(() => store.isRestarting)
const isShuttingDown = computed(() => store.isShuttingDown)
// Navigation items — reactive based on UI mode
interface NavItem {
path: string
label: string
icon: string
isCombined?: boolean
}
const gamerDesktopNav: NavItem[] = [
{ path: '/dashboard', label: 'Home', icon: 'home' },
{ path: '/dashboard/apps', label: 'My Apps', icon: 'apps' },
{ path: '/dashboard/marketplace', label: 'App Store', icon: 'marketplace' },
{ path: '/dashboard/cloud', label: 'Cloud', icon: 'cloud' },
{ path: '/dashboard/server', label: 'Network', icon: 'server' },
{ path: '/dashboard/web5', label: 'Web5', icon: 'web5' },
{ path: '/dashboard/settings', label: 'Settings', icon: 'settings' },
2026-01-24 22:59:20 +00:00
]
const easyDesktopNav: NavItem[] = [
{ path: '/dashboard', label: 'Home', icon: 'home' },
{ path: '/dashboard/apps', label: 'My Apps', icon: 'apps' },
{ path: '/dashboard/cloud', label: 'Cloud', icon: 'cloud' },
{ path: '/dashboard/settings', label: 'Settings', icon: 'settings' },
2026-01-24 22:59:20 +00:00
]
const chatDesktopNav: NavItem[] = [
{ path: '/dashboard', label: 'Home', icon: 'home' },
{ path: '/dashboard/apps', label: 'My Apps', icon: 'apps' },
{ path: '/dashboard/settings', label: 'Settings', icon: 'settings' },
]
const desktopNavItems = computed(() => {
if (uiMode.isEasy) return easyDesktopNav
if (uiMode.isChat) return chatDesktopNav
return gamerDesktopNav
})
const gamerMobileNav: NavItem[] = [
{ path: '/dashboard', label: 'Home', icon: 'home' },
{ path: '/dashboard/apps', label: 'Apps', icon: 'apps', isCombined: true },
{ path: '/dashboard/cloud', label: 'Network', icon: 'server', isCombined: true },
{ path: '/dashboard/settings', label: 'Settings', icon: 'settings' },
]
const easyMobileNav: NavItem[] = [
{ path: '/dashboard', label: 'Home', icon: 'home' },
{ path: '/dashboard/apps', label: 'Apps', icon: 'apps' },
{ path: '/dashboard/cloud', label: 'Cloud', icon: 'cloud' },
{ path: '/dashboard/settings', label: 'Settings', icon: 'settings' },
]
const chatMobileNav: NavItem[] = [
{ path: '/dashboard', label: 'Home', icon: 'home' },
{ path: '/dashboard/apps', label: 'Apps', icon: 'apps' },
{ path: '/dashboard/settings', label: 'Settings', icon: 'settings' },
]
const mobileNavItems = computed(() => {
if (uiMode.isEasy) return easyMobileNav
if (uiMode.isChat) return chatMobileNav
return gamerMobileNav
})
2026-01-24 22:59:20 +00:00
function getIconPath(iconName: string): string[] {
const icons: Record<string, string[]> = {
home: ['M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'],
apps: ['M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z'],
marketplace: ['M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z'],
cloud: ['M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z'],
server: ['M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01'],
web5: ['M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9'],
chat: ['M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z'],
2026-01-24 22:59:20 +00:00
settings: [
'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
'M15 12a3 3 0 11-6 0 3 3 0 016 0z',
],
}
return icons[iconName] || []
}
async function handleLogout() {
try {
await store.logout()
} catch {
/* proceed to login regardless */
}
router.push('/login').catch(() => {
window.location.href = '/login'
})
2026-01-24 22:59:20 +00:00
}
// Track previous route for transition logic
let previousPath = ''
// Tab order for vertical transitions
const tabOrder = [
'/dashboard',
'/dashboard/apps',
'/dashboard/marketplace',
'/dashboard/cloud',
'/dashboard/server',
'/dashboard/web5',
'/dashboard/chat',
2026-01-24 22:59:20 +00:00
'/dashboard/settings'
]
// Determine transition direction based on route depth
function getTransitionName(currentRoute: RouteLocationNormalizedLoaded) {
2026-01-24 22:59:20 +00:00
const currentPath = currentRoute.path
// If no previous path, use fade transition for initial load
if (!previousPath) {
previousPath = currentPath
return 'fade'
}
// Chat transitions: directional slide (chat from/to left, dashboard from/to right)
const isChat = currentPath === '/dashboard/chat'
const wasChat = previousPath === '/dashboard/chat'
if (isChat) {
previousPath = currentPath
return 'chat-open'
}
if (wasChat) {
previousPath = currentPath
return 'chat-close'
}
2026-01-24 22:59:20 +00:00
const isAppDetails = currentPath.includes('/apps/') && !currentPath.endsWith('/apps')
const isAppsList = currentPath === '/dashboard/apps'
const wasAppDetails = previousPath.includes('/apps/') && !previousPath.endsWith('/apps')
const wasAppsList = previousPath === '/dashboard/apps'
// Marketplace detail transitions
const isMarketplaceDetails = currentPath.includes('/marketplace/') && !currentPath.endsWith('/marketplace')
const isMarketplaceList = currentPath === '/dashboard/marketplace'
const wasMarketplaceDetails = previousPath.includes('/marketplace/') && !previousPath.endsWith('/marketplace')
const wasMarketplaceList = previousPath === '/dashboard/marketplace'
// Cloud folder transitions
const isCloudFolder = currentPath.includes('/cloud/') && !currentPath.endsWith('/cloud')
const isCloudList = currentPath === '/dashboard/cloud'
const wasCloudFolder = previousPath.includes('/cloud/') && !previousPath.endsWith('/cloud')
const wasCloudList = previousPath === '/dashboard/cloud'
let transitionName = 'fade'
// Mobile: Horizontal slide transitions between Apps and Marketplace (check first on mobile)
if (typeof window !== 'undefined' && window.innerWidth < 768) {
// From Marketplace to Apps: slide right
if (wasMarketplaceList && isAppsList) {
transitionName = 'slide-right'
}
// From Apps to Marketplace: slide left
else if (wasAppsList && isMarketplaceList) {
transitionName = 'slide-left'
}
// From Network to Cloud: slide right
else if (previousPath === '/dashboard/server' && isCloudList) {
transitionName = 'slide-right'
}
// From Cloud to Network: slide left
else if (wasCloudList && currentPath === '/dashboard/server') {
transitionName = 'slide-left'
}
// Vertical transition: between main tabs (mobile fallback)
else {
const currentIndex = tabOrder.indexOf(currentPath)
const previousIndex = tabOrder.indexOf(previousPath)
if (currentIndex !== -1 && previousIndex !== -1 && currentIndex !== previousIndex) {
// Moving down the menu (visual down)
if (currentIndex > previousIndex) {
transitionName = 'slide-down'
}
// Moving up the menu (visual up)
else {
transitionName = 'slide-up'
}
}
}
}
// Horizontal depth transition: apps list <-> app details
else if (wasAppsList && isAppDetails) {
transitionName = 'depth-forward'
} else if (wasAppDetails && isAppsList) {
transitionName = 'depth-back'
}
// Horizontal depth transition: marketplace list <-> marketplace details
else if (wasMarketplaceList && isMarketplaceDetails) {
transitionName = 'depth-forward'
} else if (wasMarketplaceDetails && isMarketplaceList) {
transitionName = 'depth-back'
}
// Horizontal depth transition: cloud list <-> cloud folder
else if (wasCloudList && isCloudFolder) {
transitionName = 'depth-forward'
} else if (wasCloudFolder && isCloudList) {
transitionName = 'depth-back'
}
// Horizontal depth transition: marketplace list <-> installed app details
// (when clicking installed app from marketplace)
else if (wasMarketplaceList && isAppDetails) {
transitionName = 'depth-forward'
} else if (wasAppDetails && isMarketplaceList) {
transitionName = 'depth-back'
}
// Vertical transition: between main tabs (desktop)
else {
const currentIndex = tabOrder.indexOf(currentPath)
const previousIndex = tabOrder.indexOf(previousPath)
if (currentIndex !== -1 && previousIndex !== -1 && currentIndex !== previousIndex) {
// Moving down the menu (visual down)
if (currentIndex > previousIndex) {
transitionName = 'slide-down'
}
// Moving up the menu (visual up)
else {
transitionName = 'slide-up'
}
}
}
// Update previous path for next transition
previousPath = currentPath
2026-01-24 22:59:20 +00:00
return transitionName
}
// Health notifications from WebSocket data
const dismissedNotifications = ref<Set<string>>(new Set())
const healthNotifications = computed(() => {
const notifs = store.data?.notifications ?? []
return notifs.filter(n => !dismissedNotifications.value.has(n.id)).slice(-5)
})
function dismissNotification(id: string) {
dismissedNotifications.value.add(id)
}
2026-01-24 22:59:20 +00:00
</script>
<style>
/* 2advanced-style: fluid, cinematic, layered motion */
/* Strong easeInOut - smooth acceleration/deceleration, no bounce */
/* Background - zoom in from depth with motion blur */
.zoom-reveal-bg {
animation: zoom-reveal 2.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
transform-origin: center center;
opacity: 0;
transform: scale(0.15);
filter: blur(24px);
}
@keyframes zoom-reveal {
0% {
opacity: 0;
transform: scale(0.15);
filter: blur(24px);
}
35% {
opacity: 0.5;
transform: scale(0.5);
filter: blur(20px);
}
65% {
opacity: 0.85;
transform: scale(0.88);
filter: blur(6px);
}
100% {
opacity: 1;
transform: scale(1);
filter: blur(0);
}
}
/* 2advanced-style glass assembly - fluid, layered, deliberate timing */
.glass-throw-active {
perspective: 1400px;
}
.glass-piece {
will-change: transform, opacity;
}
/* Sidebar - animates in at end with separate parts (like cards) */
.sidebar-shell {
width: 100%;
height: 100%;
min-height: 100vh;
background: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
border-right: 1px solid transparent;
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.sidebar-animate .sidebar-shell {
animation: sidebar-shell-fly 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: 5.2s;
opacity: 0;
transform: translateX(-100%);
}
@keyframes sidebar-shell-fly {
0% {
opacity: 0;
transform: translateX(-100%);
border-color: transparent;
}
70% {
opacity: 1;
transform: translateX(0);
border-color: transparent;
}
100% {
opacity: 1;
transform: translateX(0);
border-color: rgba(255, 255, 255, 0.18);
}
}
.sidebar-inner {
overflow: hidden;
opacity: 0;
}
.sidebar-animate .sidebar-inner {
animation: sidebar-inner-draw 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: 6.1s;
}
@keyframes sidebar-inner-draw {
0% {
opacity: 0;
clip-path: inset(0 100% 0 0);
}
20% { opacity: 1; }
100% {
opacity: 1;
clip-path: inset(0 0 0 0);
}
}
.sidebar-nav-item {
opacity: 0;
transform: translateX(-12px);
}
.sidebar-animate .sidebar-nav-item {
animation: sidebar-nav-item-in 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: calc(6.3s + var(--nav-stagger, 0) * 0.06s);
}
@keyframes sidebar-nav-item-in {
0% {
opacity: 0;
transform: translateX(-12px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.sidebar-controller {
opacity: 0;
}
.sidebar-animate .sidebar-controller {
animation: sidebar-fade-in 0.4s ease-out forwards;
animation-delay: 6.9s;
}
.sidebar-logout-btn {
opacity: 0;
transform: scale(0.95);
}
.sidebar-animate .sidebar-logout-btn {
animation: sidebar-logout-pop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
animation-delay: 7.1s;
}
@keyframes sidebar-fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes sidebar-logout-pop {
0% {
opacity: 0;
transform: scale(0.95);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.sidebar-logo {
opacity: 0;
transform: translateY(-8px);
}
.sidebar-animate .sidebar-logo {
animation: sidebar-logo-in 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: 6.15s;
}
@keyframes sidebar-logo-in {
0% {
opacity: 0;
transform: translateY(-8px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* When not animating, show everything (direct load / hard refresh) */
aside:not(.sidebar-animate) .sidebar-shell {
border-color: rgba(255, 255, 255, 0.18);
opacity: 1;
transform: none;
}
aside:not(.sidebar-animate) .sidebar-inner,
aside:not(.sidebar-animate) .sidebar-logo,
aside:not(.sidebar-animate) .sidebar-nav-item,
aside:not(.sidebar-animate) .sidebar-controller,
aside:not(.sidebar-animate) .sidebar-logout-btn {
opacity: 1;
transform: none;
animation: none;
clip-path: none;
}
/* Smooth easeInOut - no overshoot, refined deceleration */
.glass-throw-main {
animation: glass-throw-main 1.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.15s forwards;
opacity: 0;
transform: translateX(20%) scale(0.2);
filter: blur(14px);
}
.glass-throw-content {
animation: glass-throw-content 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.22s forwards;
opacity: 0;
transform: translateY(12%) scale(0.25);
filter: blur(10px);
}
.glass-throw-mobile-tabs {
animation: glass-throw-top 1.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.08s forwards;
opacity: 0;
transform: translateY(-90%) scale(0.28);
filter: blur(10px);
}
.glass-throw-mobile-tabs-2 {
animation: glass-throw-top 1.35s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.18s forwards;
opacity: 0;
transform: translateY(-90%) scale(0.28);
filter: blur(10px);
}
.glass-throw-tabbar {
animation: glass-throw-bottom 1.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.2s forwards;
opacity: 0;
transform: translateY(85%) scale(0.25);
filter: blur(10px);
}
/* Motion blur peaks mid-transition, smooth settle */
@keyframes glass-throw-sidebar {
0% {
opacity: 0;
transform: translateX(-100%) scale(0.25);
filter: blur(12px);
}
45% {
opacity: 0.9;
transform: translateX(-15%) scale(0.85);
filter: blur(8px);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
filter: blur(0);
}
}
@keyframes glass-throw-main {
0% {
opacity: 0;
transform: translateX(20%) scale(0.2);
filter: blur(14px);
}
50% {
opacity: 0.85;
transform: translateX(0) scale(0.9);
filter: blur(6px);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
filter: blur(0);
}
}
@keyframes glass-throw-content {
0% {
opacity: 0;
transform: translateY(12%) scale(0.25);
filter: blur(10px);
}
50% {
opacity: 0.9;
transform: translateY(0) scale(0.9);
filter: blur(4px);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0);
}
}
@keyframes glass-throw-top {
0% {
opacity: 0;
transform: translateY(-90%) scale(0.28);
filter: blur(10px);
}
50% {
opacity: 0.9;
transform: translateY(0) scale(0.95);
filter: blur(4px);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0);
}
}
@keyframes glass-throw-bottom {
0% {
opacity: 0;
transform: translateY(85%) scale(0.25);
filter: blur(10px);
}
50% {
opacity: 0.9;
transform: translateY(0) scale(0.95);
filter: blur(4px);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0);
}
}
/* Oomph accent - subtle 2advanced-style flash synced with boot thud */
.oomph-flash {
background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.08) 0%, transparent 65%);
animation: oomph-flash 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
}
@keyframes oomph-flash {
0% { opacity: 0; }
25% { opacity: 0.9; }
100% { opacity: 0; }
}
/* Reveal flashes - enthralling entrance during zoom */
.reveal-flash-glitch {
background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.12) 0%, transparent 70%);
animation: reveal-flash-sequence 2.8s ease-out forwards;
}
@keyframes reveal-flash-sequence {
0% { opacity: 0; }
12% { opacity: 0.6; }
18% { opacity: 0; }
42% { opacity: 0.4; }
48% { opacity: 0; }
70% { opacity: 0.35; }
78% { opacity: 0; }
100% { opacity: 0; }
}
2026-01-24 22:59:20 +00:00
/* Wrapper to contain perspective without clipping */
.perspective-container-wrapper {
position: relative;
overflow: hidden;
height: 100%;
}
/* Perspective container for 3D depth effect */
.perspective-container {
perspective: 2000px;
perspective-origin: 50% 50%;
position: relative;
height: 100%;
overflow: hidden;
}
/* View wrapper - allows smooth transitions with absolute positioning */
.view-wrapper {
position: absolute;
inset: 0;
transform-style: preserve-3d;
backface-visibility: hidden;
will-change: transform, opacity;
opacity: 1;
}
.view-container {
height: 100%;
min-height: 100%;
}
/* Forward transition: 2advanced fluid depth */
2026-01-24 22:59:20 +00:00
.depth-forward-enter-active.view-wrapper,
.depth-forward-leave-active.view-wrapper {
transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
2026-01-24 22:59:20 +00:00
}
.depth-forward-enter-from.view-wrapper {
opacity: 0;
transform: translateZ(-800px) scale(0.75);
filter: blur(4px);
}
.depth-forward-enter-to.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-forward-leave-from.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-forward-leave-to.view-wrapper {
opacity: 0;
transform: translateZ(400px) scale(1.2);
filter: blur(8px);
}
/* Back transition: 2advanced fluid depth */
2026-01-24 22:59:20 +00:00
.depth-back-enter-active.view-wrapper,
.depth-back-leave-active.view-wrapper {
transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
2026-01-24 22:59:20 +00:00
}
.depth-back-enter-from.view-wrapper {
opacity: 0;
transform: translateZ(400px) scale(1.2);
filter: blur(8px);
}
.depth-back-enter-to.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-back-leave-from.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-back-leave-to.view-wrapper {
opacity: 0;
transform: translateZ(-800px) scale(0.75);
filter: blur(4px);
}
/* Subtle 3D tilt - 2advanced layered depth */
2026-01-24 22:59:20 +00:00
@media (min-width: 768px) {
.depth-forward-enter-from.view-wrapper {
transform: translateZ(-800px) scale(0.75) rotateX(5deg);
2026-01-24 22:59:20 +00:00
}
.depth-forward-leave-to.view-wrapper {
transform: translateZ(400px) scale(1.2) rotateX(-4deg);
2026-01-24 22:59:20 +00:00
}
.depth-back-enter-from.view-wrapper {
transform: translateZ(400px) scale(1.2) rotateX(-4deg);
2026-01-24 22:59:20 +00:00
}
.depth-back-leave-to.view-wrapper {
transform: translateZ(-800px) scale(0.75) rotateX(5deg);
2026-01-24 22:59:20 +00:00
}
}
/* Chat open transition — chat slides in from left, dashboard slides out to right */
.chat-open-enter-active.view-wrapper,
.chat-open-leave-active.view-wrapper {
transition: opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1), transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.chat-open-enter-from.view-wrapper {
opacity: 0;
transform: translateX(-60px) scale(0.96);
}
.chat-open-enter-to.view-wrapper {
opacity: 1;
transform: translateX(0) scale(1);
}
.chat-open-leave-from.view-wrapper {
opacity: 1;
transform: translateX(0) scale(1);
}
.chat-open-leave-to.view-wrapper {
opacity: 0;
transform: translateX(60px) scale(0.96);
}
/* Chat close transition — chat slides out to left, dashboard slides in from right */
.chat-close-enter-active.view-wrapper,
.chat-close-leave-active.view-wrapper {
transition: opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1), transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.chat-close-enter-from.view-wrapper {
opacity: 0;
transform: translateX(60px) scale(0.96);
}
.chat-close-enter-to.view-wrapper {
opacity: 1;
transform: translateX(0) scale(1);
}
.chat-close-leave-from.view-wrapper {
opacity: 1;
transform: translateX(0) scale(1);
}
.chat-close-leave-to.view-wrapper {
opacity: 0;
transform: translateX(-60px) scale(0.96);
}
2026-01-24 22:59:20 +00:00
/* Fade transition for initial loads and default cases */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
/* Mobile: Slide left transition (Apps -> Marketplace) */
.slide-left-enter-active.view-wrapper,
.slide-left-leave-active.view-wrapper {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
2026-01-24 22:59:20 +00:00
}
.slide-left-enter-from.view-wrapper {
transform: translateX(100%);
opacity: 0;
2026-01-24 22:59:20 +00:00
}
.slide-left-enter-to.view-wrapper {
transform: translateX(0);
opacity: 1;
2026-01-24 22:59:20 +00:00
}
.slide-left-leave-from.view-wrapper {
transform: translateX(0);
opacity: 1;
2026-01-24 22:59:20 +00:00
}
.slide-left-leave-to.view-wrapper {
transform: translateX(-100%);
opacity: 0;
2026-01-24 22:59:20 +00:00
}
/* Mobile: Slide right transition (Marketplace -> Apps) */
.slide-right-enter-active.view-wrapper,
.slide-right-leave-active.view-wrapper {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
2026-01-24 22:59:20 +00:00
}
.slide-right-enter-from.view-wrapper {
transform: translateX(-100%);
opacity: 0;
2026-01-24 22:59:20 +00:00
}
.slide-right-enter-to.view-wrapper {
transform: translateX(0);
opacity: 1;
2026-01-24 22:59:20 +00:00
}
.slide-right-leave-from.view-wrapper {
transform: translateX(0);
opacity: 1;
2026-01-24 22:59:20 +00:00
}
.slide-right-leave-to.view-wrapper {
transform: translateX(100%);
opacity: 0;
2026-01-24 22:59:20 +00:00
}
/* Slide down: Moving down the menu (content slides up like a scroll) */
.slide-down-enter-active.view-wrapper {
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.slide-down-leave-active.view-wrapper {
transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-down-enter-from.view-wrapper {
opacity: 0;
transform: translateY(40vh);
}
.slide-down-enter-to.view-wrapper {
opacity: 1;
transform: translateY(0);
}
.slide-down-leave-from.view-wrapper {
opacity: 1;
transform: translateY(0);
}
.slide-down-leave-to.view-wrapper {
opacity: 0;
transform: translateY(-30vh);
}
/* Slide up: Moving up the menu (content slides down like a scroll) */
.slide-up-enter-active.view-wrapper {
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.slide-up-leave-active.view-wrapper {
transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-up-enter-from.view-wrapper {
opacity: 0;
transform: translateY(-40vh);
}
.slide-up-enter-to.view-wrapper {
opacity: 1;
transform: translateY(0);
}
.slide-up-leave-from.view-wrapper {
opacity: 1;
transform: translateY(0);
}
.slide-up-leave-to.view-wrapper {
opacity: 0;
transform: translateY(30vh);
}
/* Background 3D container - full width, black fill during zoom to avoid letterboxing */
.dashboard-view .bg-perspective-container {
2026-01-24 22:59:20 +00:00
position: fixed;
inset: 0;
z-index: -10;
perspective: 1000px;
perspective-origin: 50% 50%;
overflow: hidden;
left: 0 !important;
right: 0 !important;
width: 100% !important;
min-width: 100% !important;
background: #000;
2026-01-24 22:59:20 +00:00
}
/* Background layers with 3D transitions - full width like login */
.dashboard-view .bg-layer {
2026-01-24 22:59:20 +00:00
position: absolute;
inset: 0;
background-size: cover !important;
background-position: center center !important;
background-repeat: no-repeat !important;
2026-01-24 22:59:20 +00:00
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform-style: preserve-3d;
will-change: transform, opacity;
}
/* Default state - bg-intro visible, bg-intro-3 hidden back */
.dashboard-view .bg-layer:first-of-type {
2026-01-24 22:59:20 +00:00
opacity: 1;
transform: translateZ(0) scale(1);
}
.dashboard-view .bg-layer:nth-of-type(2) {
2026-01-24 22:59:20 +00:00
opacity: 0;
transform: translateZ(-200px) scale(0.9) rotateY(-15deg);
}
/* Transitioning out - current background moves away with zoom */
.dashboard-view .bg-layer.bg-transitioning-out {
2026-01-24 22:59:20 +00:00
opacity: 0;
transform: translateZ(200px) scale(1.15) rotateY(15deg) !important;
}
/* Transitioning in - new background comes forward with zoom */
.dashboard-view .bg-layer.bg-transitioning-in {
2026-01-24 22:59:20 +00:00
opacity: 1;
transform: translateZ(0) scale(1.05) rotateY(0deg) !important;
}
/* Background glitch effect layers - World Fair style */
.bg-glitch-layer-1,
.bg-glitch-layer-2,
.bg-glitch-scan {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 10;
opacity: 0;
}
.bg-glitch-layer-1 {
background-size: cover;
background-position: center;
mix-blend-mode: lighten;
filter: brightness(1.8) contrast(2) saturate(1.5) hue-rotate(180deg);
will-change: transform, clip-path, opacity;
}
.bg-glitch-layer-2 {
background-size: cover;
background-position: center;
mix-blend-mode: color-dodge;
filter: brightness(2) contrast(2) saturate(2) hue-rotate(90deg);
will-change: transform, clip-path, opacity;
}
.bg-glitch-scan {
background:
linear-gradient(90deg,
rgba(255,0,255,0.2) 0%,
rgba(0,255,255,0.2) 25%,
rgba(255,255,0,0.2) 50%,
rgba(0,255,255,0.2) 75%,
rgba(255,0,255,0.2) 100%
),
repeating-linear-gradient(0deg,
rgba(255,255,255,0.05) 0px,
rgba(255,255,255,0.05) 2px,
transparent 2px,
transparent 4px
);
will-change: transform, opacity;
}
/* Trigger glitch animation when active */
.bg-glitch-layer-1.glitch-active {
animation: bg-glitch-shift 0.375s steps(15, end) forwards;
}
.bg-glitch-layer-2.glitch-active {
animation: bg-glitch-shift-2 0.375s steps(12, end) forwards;
}
.bg-glitch-scan.glitch-active {
animation: bg-glitch-scan 0.375s linear forwards;
}
/* World Fair style - visible but tasteful glitch */
@keyframes bg-glitch-shift {
0% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
5% { opacity: 0.5; }
12% { transform: translate(15px,-8px); clip-path: inset(12% 0 70% 0); }
20% { transform: translate(-20px,10px); clip-path: inset(45% 0 35% 0); }
28% { transform: translate(18px,-5px); clip-path: inset(68% 0 15% 0); }
36% { transform: translate(-15px,12px); clip-path: inset(20% 0 60% 0); }
44% { transform: translate(22px,-10px); clip-path: inset(52% 0 28% 0); }
52% { transform: translate(-18px,8px); clip-path: inset(10% 0 75% 0); }
60% { transform: translate(12px,-6px); clip-path: inset(58% 0 22% 0); }
68% { transform: translate(-10px,15px); clip-path: inset(32% 0 48% 0); }
76% { transform: translate(16px,-4px); clip-path: inset(72% 0 12% 0); }
84% { transform: translate(-12px,7px); clip-path: inset(18% 0 65% 0); }
92% { transform: translate(8px,-3px); clip-path: inset(42% 0 40% 0); }
96% { opacity: 0.4; }
100% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
}
@keyframes bg-glitch-shift-2 {
0% { transform: translate(0,0) skewX(0deg); clip-path: inset(0% 0 0 0); opacity: 0; }
8% { opacity: 0.5; }
15% { transform: translate(-18px,10px) skewX(4deg); clip-path: inset(25% 0 55% 0); }
23% { transform: translate(22px,-12px) skewX(-5deg); clip-path: inset(50% 0 30% 0); }
31% { transform: translate(-16px,8px) skewX(3deg); clip-path: inset(72% 0 12% 0); }
39% { transform: translate(20px,-15px) skewX(-4deg); clip-path: inset(18% 0 65% 0); }
47% { transform: translate(-22px,12px) skewX(5deg); clip-path: inset(42% 0 38% 0); }
55% { transform: translate(18px,-8px) skewX(-3deg); clip-path: inset(62% 0 20% 0); }
63% { transform: translate(-14px,14px) skewX(4deg); clip-path: inset(30% 0 52% 0); }
71% { transform: translate(16px,-6px) skewX(-2deg); clip-path: inset(8% 0 78% 0); }
79% { transform: translate(-12px,10px) skewX(3deg); clip-path: inset(55% 0 28% 0); }
87% { transform: translate(10px,-4px) skewX(-2deg); clip-path: inset(35% 0 45% 0); }
95% { opacity: 0.4; }
100% { transform: translate(0,0) skewX(0deg); clip-path: inset(0% 0 0 0); opacity: 0; }
}
@keyframes bg-glitch-scan {
0% { opacity: 0; transform: translateX(-120%); }
5% { opacity: 0.5; }
15% { opacity: 0.55; transform: translateX(-80%); }
30% { opacity: 0.6; transform: translateX(-40%); }
50% { opacity: 0.6; transform: translateX(0%); }
70% { opacity: 0.55; transform: translateX(40%); }
85% { opacity: 0.5; transform: translateX(80%); }
95% { opacity: 0.45; }
100% { opacity: 0; transform: translateX(120%); }
}
/* Full width background - same as login */
.dashboard-view .bg-fullwidth {
min-width: 100%;
width: 100%;
background-size: cover !important;
background-position: center center !important;
}
/* Continuous glitch overlays - same as login, every 5s */
.dashboard-glitch-layer {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 5;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0;
}
.dashboard-glitch-1 {
mix-blend-mode: screen;
filter: hue-rotate(22deg) saturate(1.35);
animation: dashboard-glitch-shift 5s steps(10, end) infinite;
background-size: cover !important;
background-position: center center !important;
}
.dashboard-glitch-2 {
mix-blend-mode: screen;
filter: hue-rotate(-30deg) saturate(1.45);
animation: dashboard-glitch-shift-2 5s steps(9, end) infinite;
background-size: cover !important;
background-position: center center !important;
}
.dashboard-glitch-scan {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 6;
background:
linear-gradient(180deg, rgba(255,255,255,0.16), rgba(0,0,0,0) 60%),
repeating-linear-gradient(180deg, rgba(255,255,255,0.05) 0 2px, rgba(0,0,0,0) 2px 4px),
radial-gradient(ellipse at center, rgba(0,0,0,0) 40%, rgba(0,0,0,0.35) 100%);
opacity: 0;
animation: dashboard-glitch-scan 5s ease-out infinite;
}
@keyframes dashboard-glitch-shift {
0%, 82% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
82.1% { opacity: 0.28; }
84% { transform: translate(6px,-2px); clip-path: inset(8% 0 70% 0); }
86% { transform: translate(-5px,2px); clip-path: inset(42% 0 40% 0); }
88% { transform: translate(3px,0); clip-path: inset(68% 0 10% 0); }
91% { transform: translate(-4px,3px); clip-path: inset(18% 0 60% 0); }
93% { transform: translate(5px,-3px); clip-path: inset(55% 0 20% 0); }
95% { transform: translate(-3px,1px); clip-path: inset(10% 0 80% 0); }
100% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
}
@keyframes dashboard-glitch-shift-2 {
0%, 82% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
82.1% { opacity: 0.24; }
84% { transform: translate(-6px,2px); clip-path: inset(12% 0 65% 0); }
86% { transform: translate(5px,-1px) skewX(0.6deg); clip-path: inset(36% 0 42% 0); }
89% { transform: translate(-3px,2px); clip-path: inset(72% 0 8% 0); }
92% { transform: translate(4px,-3px); clip-path: inset(22% 0 58% 0); }
95% { transform: translate(-4px,1px); clip-path: inset(50% 0 26% 0); }
100% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
}
@keyframes dashboard-glitch-scan {
0%, 82% { opacity: 0; transform: translateY(-20%); }
84% { opacity: 0.5; }
90% { opacity: 0.35; }
100% { opacity: 0; transform: translateY(115%); }
}
2026-01-24 22:59:20 +00:00
</style>