2026-01-24 22:59:20 +00:00
|
|
|
<template>
|
|
|
|
|
<div class="min-h-screen flex relative dashboard-view">
|
|
|
|
|
<!-- Background container with 3D perspective -->
|
|
|
|
|
<div class="bg-perspective-container">
|
|
|
|
|
<!-- Background - default -->
|
|
|
|
|
<div
|
|
|
|
|
ref="bgDefault"
|
|
|
|
|
class="bg-layer"
|
|
|
|
|
:class="{ 'bg-transitioning-out': showAltBackground || showWeb5Background || showNetworkBackground || showSettingsBackground || showMyAppsBackground || showAppStoreBackground || showCloudBackground || showHomeBackground }"
|
|
|
|
|
:style="{ backgroundImage: `url(/assets/img/${currentBackgroundImage})` }"
|
|
|
|
|
/>
|
|
|
|
|
<!-- Background - alternate for app details and Web5 -->
|
|
|
|
|
<div
|
|
|
|
|
ref="bgAlt"
|
|
|
|
|
class="bg-layer"
|
|
|
|
|
:class="{ 'bg-transitioning-in': showAltBackground || showWeb5Background || showNetworkBackground || showSettingsBackground || showMyAppsBackground || showAppStoreBackground || showCloudBackground || showHomeBackground }"
|
|
|
|
|
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
|
|
|
|
|
/>
|
|
|
|
|
<!-- Glitch overlays - trigger on background change -->
|
|
|
|
|
<div
|
|
|
|
|
class="bg-glitch-layer-1"
|
|
|
|
|
:class="{ 'glitch-active': isGlitching }"
|
|
|
|
|
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
|
|
|
|
|
/>
|
|
|
|
|
<div
|
|
|
|
|
class="bg-glitch-layer-2"
|
|
|
|
|
:class="{ 'glitch-active': isGlitching }"
|
|
|
|
|
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
|
|
|
|
|
/>
|
|
|
|
|
<div
|
|
|
|
|
class="bg-glitch-scan"
|
|
|
|
|
:class="{ 'glitch-active': isGlitching }"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 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 -->
|
|
|
|
|
<aside class="hidden md:flex w-[256px] border-r border-glass-border shadow-glass-sm flex-shrink-0 relative flex-col" style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);">
|
|
|
|
|
<div class="p-6 flex-1">
|
|
|
|
|
<div class="flex items-center gap-3 mb-8">
|
2026-02-17 15:03:34 +00:00
|
|
|
<AnimatedLogo />
|
|
|
|
|
<div class="min-w-0 flex-1">
|
|
|
|
|
<h2 class="text-lg font-semibold text-white truncate">{{ serverName }}</h2>
|
2026-01-24 22:59:20 +00:00
|
|
|
<p class="text-xs text-white/60">v{{ version }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<nav class="space-y-2">
|
|
|
|
|
<RouterLink
|
|
|
|
|
v-for="item in desktopNavItems"
|
|
|
|
|
:key="item.path"
|
|
|
|
|
:to="item.path"
|
|
|
|
|
class="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"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" 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>
|
|
|
|
|
</RouterLink>
|
|
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-17 15:03:34 +00:00
|
|
|
<!-- Controller indicator - Desktop sidebar -->
|
|
|
|
|
<div class="px-6 pb-2">
|
|
|
|
|
<ControllerIndicator />
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
<!-- User Section - Desktop Only -->
|
|
|
|
|
<div class="p-6">
|
|
|
|
|
<button
|
|
|
|
|
@click="handleLogout"
|
|
|
|
|
class="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" 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>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<!-- Main Content -->
|
|
|
|
|
<main class="flex-1 overflow-hidden relative pb-20 md:pb-0">
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
2026-02-17 15:03:34 +00:00
|
|
|
<!-- New message toast (top right, glassmorphic) -->
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
<Transition name="toast">
|
|
|
|
|
<div
|
|
|
|
|
v-if="toastMessage.show"
|
|
|
|
|
@click="messageToast.dismissToastAndOpenMessages"
|
|
|
|
|
class="fixed top-20 right-4 left-4 z-[100] w-auto max-w-md cursor-pointer rounded-xl p-4 transition-all hover:border-white/30 hover:shadow-2xl md:top-6 md:right-6 md:left-auto md:max-w-md toast-glass"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-start gap-3">
|
|
|
|
|
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-orange-500/20">
|
|
|
|
|
<svg class="h-5 w-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="min-w-0 flex-1">
|
|
|
|
|
<p class="text-sm font-medium text-white">New message</p>
|
|
|
|
|
<p class="mt-0.5 text-sm text-white/70 line-clamp-2">{{ toastMessage.text }}</p>
|
|
|
|
|
<p class="mt-1 text-xs text-orange-400">Click to view</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</Teleport>
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
<!-- Persistent Mobile Tabs for Apps/Marketplace -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="showAppsTabs"
|
|
|
|
|
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2"
|
|
|
|
|
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 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="showNetworkTabs"
|
|
|
|
|
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2"
|
|
|
|
|
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" :class="{ 'pt-40': showAppsTabs && showNetworkTabs, 'pt-20': showAppsTabs !== showNetworkTabs }">
|
|
|
|
|
<div class="perspective-container">
|
|
|
|
|
<RouterView v-slot="{ Component, route }">
|
|
|
|
|
<Transition :name="getTransitionName(route)">
|
|
|
|
|
<div :key="route.path" class="view-wrapper">
|
|
|
|
|
<div
|
|
|
|
|
:class="[
|
2026-02-17 15:03:34 +00:00
|
|
|
'px-4 pt-4 pb-4 md:px-8 md:pt-8 md:pb-8 overflow-y-auto h-full',
|
2026-01-24 22:59:20 +00:00
|
|
|
needsMobileBackButtonSpace
|
2026-02-17 15:03:34 +00:00
|
|
|
? 'pb-[calc(var(--mobile-tab-bar-height,_72px)+96px)] md:pb-8'
|
2026-01-24 22:59:20 +00:00
|
|
|
: undefined
|
|
|
|
|
]"
|
|
|
|
|
>
|
|
|
|
|
<component :is="Component" class="view-container" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</RouterView>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<!-- Mobile Bottom Tab Bar -->
|
|
|
|
|
<nav
|
|
|
|
|
ref="mobileTabBar"
|
|
|
|
|
data-mobile-tab-bar
|
|
|
|
|
class="md:hidden fixed bottom-0 left-0 right-0 border-t border-glass-border shadow-glass z-50"
|
|
|
|
|
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);"
|
|
|
|
|
>
|
|
|
|
|
<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 items-center justify-center w-full py-3 rounded-lg text-white/70 transition-all duration-300 relative z-10"
|
|
|
|
|
:class="{
|
|
|
|
|
'nav-tab-active': item.isCombined
|
|
|
|
|
? (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-7 h-7 transition-all duration-300" 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>
|
|
|
|
|
</RouterLink>
|
|
|
|
|
</div>
|
|
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
|
|
|
import { RouterLink, RouterView, useRouter, useRoute } from 'vue-router'
|
|
|
|
|
import { useAppStore } from '../stores/app'
|
2026-02-17 15:03:34 +00:00
|
|
|
import { useMessageToast } from '@/composables/useMessageToast'
|
|
|
|
|
import AnimatedLogo from '@/components/AnimatedLogo.vue'
|
|
|
|
|
import ControllerIndicator from '@/components/ControllerIndicator.vue'
|
2026-01-24 22:59:20 +00:00
|
|
|
|
|
|
|
|
const router = useRouter()
|
2026-02-17 15:03:34 +00:00
|
|
|
const messageToast = useMessageToast()
|
|
|
|
|
const toastMessage = messageToast.toastMessage
|
2026-01-24 22:59:20 +00:00
|
|
|
const route = useRoute()
|
|
|
|
|
const store = useAppStore()
|
|
|
|
|
|
|
|
|
|
// Background swap for app details, Web5, Network, Settings, My Apps, App Store, Cloud, and Home
|
|
|
|
|
const showAltBackground = ref(false)
|
|
|
|
|
const showHomeBackground = ref(route.path === '/dashboard' || route.path === '/dashboard/')
|
|
|
|
|
const showWeb5Background = ref(route.path.includes('/dashboard/web5'))
|
|
|
|
|
const showNetworkBackground = ref(route.path.includes('/dashboard/server'))
|
|
|
|
|
const showSettingsBackground = ref(route.path.includes('/dashboard/settings'))
|
|
|
|
|
const showMyAppsBackground = ref(route.path.includes('/dashboard/apps') && !route.path.includes('/dashboard/apps/'))
|
|
|
|
|
const showAppStoreBackground = ref(route.path.includes('/dashboard/marketplace'))
|
|
|
|
|
const showCloudBackground = ref(route.path.includes('/dashboard/cloud'))
|
|
|
|
|
const showWeb5Overlay = ref(route.path.includes('/dashboard/web5')) // Separate ref for overlay to handle transition delay
|
|
|
|
|
const showNetworkOverlay = ref(route.path.includes('/dashboard/server'))
|
|
|
|
|
const showSettingsOverlay = ref(route.path.includes('/dashboard/settings'))
|
|
|
|
|
const showMyAppsOverlay = ref(route.path.includes('/dashboard/apps') && !route.path.includes('/dashboard/apps/'))
|
|
|
|
|
const showAppStoreOverlay = ref(route.path.includes('/dashboard/marketplace'))
|
|
|
|
|
const showCloudOverlay = ref(route.path.includes('/dashboard/cloud'))
|
|
|
|
|
const isGlitching = ref(false)
|
|
|
|
|
|
|
|
|
|
// Background images
|
|
|
|
|
const currentBackgroundImage = computed(() => {
|
|
|
|
|
if (showWeb5Background.value) return 'bg-web5.jpg'
|
|
|
|
|
if (showNetworkBackground.value) return 'bg-network.jpg'
|
|
|
|
|
if (showSettingsBackground.value) return 'bg-settings.jpg'
|
|
|
|
|
if (showMyAppsBackground.value) return 'bg-myapps.jpg'
|
|
|
|
|
if (showAppStoreBackground.value) return 'bg-appstore.jpg'
|
|
|
|
|
if (showCloudBackground.value) return 'bg-cloud.jpg'
|
|
|
|
|
if (showHomeBackground.value) return 'bg-home.jpg'
|
|
|
|
|
return 'bg-4.jpg'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const altBackgroundImage = computed(() => {
|
|
|
|
|
if (showWeb5Background.value) return 'bg-web5.jpg'
|
|
|
|
|
if (showNetworkBackground.value) return 'bg-network.jpg'
|
|
|
|
|
if (showSettingsBackground.value) return 'bg-settings.jpg'
|
|
|
|
|
if (showMyAppsBackground.value) return 'bg-myapps.jpg'
|
|
|
|
|
if (showAppStoreBackground.value) return 'bg-appstore.jpg'
|
|
|
|
|
if (showCloudBackground.value) return 'bg-cloud.jpg'
|
|
|
|
|
if (showHomeBackground.value) return 'bg-home.jpg'
|
|
|
|
|
return 'bg-3.jpg'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Check if overlay should be dark (0.8 opacity)
|
|
|
|
|
const showDarkOverlay = computed(() => {
|
|
|
|
|
return showWeb5Overlay.value || showNetworkOverlay.value || showSettingsOverlay.value || showMyAppsOverlay.value || showAppStoreOverlay.value || showCloudOverlay.value
|
|
|
|
|
})
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
function isDetailRoute(path: string) {
|
|
|
|
|
return (path.includes('/apps/') && !path.endsWith('/apps')) ||
|
|
|
|
|
(path.includes('/marketplace/') && !path.endsWith('/marketplace'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(() => route.path, (newPath) => {
|
|
|
|
|
// Check if we're on app details OR marketplace app details
|
|
|
|
|
const isAppDetails = isDetailRoute(newPath)
|
|
|
|
|
const wasAppDetails = showAltBackground.value
|
|
|
|
|
|
|
|
|
|
// Check if we're on special background routes
|
|
|
|
|
const isHome = newPath === '/dashboard' || newPath === '/dashboard/'
|
|
|
|
|
const isWeb5 = newPath.includes('/dashboard/web5')
|
|
|
|
|
const wasWeb5 = showWeb5Background.value
|
|
|
|
|
const isNetwork = newPath.includes('/dashboard/server')
|
|
|
|
|
const wasNetwork = showNetworkBackground.value
|
|
|
|
|
const isSettings = newPath.includes('/dashboard/settings')
|
|
|
|
|
const wasSettings = showSettingsBackground.value
|
|
|
|
|
const isMyApps = newPath.includes('/dashboard/apps') && !newPath.includes('/dashboard/apps/')
|
|
|
|
|
const wasMyApps = showMyAppsBackground.value
|
|
|
|
|
const isAppStore = newPath.includes('/dashboard/marketplace')
|
|
|
|
|
const wasAppStore = showAppStoreBackground.value
|
|
|
|
|
const isCloud = newPath.includes('/dashboard/cloud')
|
|
|
|
|
const wasCloud = showCloudBackground.value
|
|
|
|
|
|
|
|
|
|
// Change background immediately
|
|
|
|
|
showAltBackground.value = isAppDetails
|
|
|
|
|
showHomeBackground.value = isHome
|
|
|
|
|
showWeb5Background.value = isWeb5
|
|
|
|
|
showNetworkBackground.value = isNetwork
|
|
|
|
|
showSettingsBackground.value = isSettings
|
|
|
|
|
showMyAppsBackground.value = isMyApps
|
|
|
|
|
showAppStoreBackground.value = isAppStore
|
|
|
|
|
showCloudBackground.value = isCloud
|
|
|
|
|
|
|
|
|
|
// Handle overlay transitions with delay when leaving special backgrounds
|
|
|
|
|
// Web5 overlay
|
|
|
|
|
if (isWeb5) {
|
|
|
|
|
showWeb5Overlay.value = true
|
|
|
|
|
} else if (wasWeb5 && !isWeb5) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
showWeb5Overlay.value = false
|
|
|
|
|
}, 450)
|
|
|
|
|
} else {
|
|
|
|
|
showWeb5Overlay.value = isWeb5
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Network overlay
|
|
|
|
|
if (isNetwork) {
|
|
|
|
|
showNetworkOverlay.value = true
|
|
|
|
|
} else if (wasNetwork && !isNetwork) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
showNetworkOverlay.value = false
|
|
|
|
|
}, 450)
|
|
|
|
|
} else {
|
|
|
|
|
showNetworkOverlay.value = isNetwork
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Settings overlay
|
|
|
|
|
if (isSettings) {
|
|
|
|
|
showSettingsOverlay.value = true
|
|
|
|
|
} else if (wasSettings && !isSettings) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
showSettingsOverlay.value = false
|
|
|
|
|
}, 450)
|
|
|
|
|
} else {
|
|
|
|
|
showSettingsOverlay.value = isSettings
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// My Apps overlay
|
|
|
|
|
if (isMyApps) {
|
|
|
|
|
showMyAppsOverlay.value = true
|
|
|
|
|
} else if (wasMyApps && !isMyApps) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
showMyAppsOverlay.value = false
|
|
|
|
|
}, 450)
|
|
|
|
|
} else {
|
|
|
|
|
showMyAppsOverlay.value = isMyApps
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// App Store overlay
|
|
|
|
|
if (isAppStore) {
|
|
|
|
|
showAppStoreOverlay.value = true
|
|
|
|
|
} else if (wasAppStore && !isAppStore) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
showAppStoreOverlay.value = false
|
|
|
|
|
}, 450)
|
|
|
|
|
} else {
|
|
|
|
|
showAppStoreOverlay.value = isAppStore
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cloud overlay
|
|
|
|
|
if (isCloud) {
|
|
|
|
|
showCloudOverlay.value = true
|
|
|
|
|
} else if (wasCloud && !isCloud) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
showCloudOverlay.value = false
|
|
|
|
|
}, 450)
|
|
|
|
|
} else {
|
|
|
|
|
showCloudOverlay.value = isCloud
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trigger glitch ONLY when going forward (to app details), not back
|
|
|
|
|
if (isAppDetails && !wasAppDetails) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
isGlitching.value = true
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
isGlitching.value = false
|
|
|
|
|
}, 375) // Glitch duration - 25% faster
|
|
|
|
|
}, 500) // Wait for background 3D transition to complete
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
return route.path.includes('/server') || route.path.includes('/cloud')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
updateTabBarHeight()
|
2026-02-17 15:03:34 +00:00
|
|
|
messageToast.startPolling()
|
2026-01-24 22:59:20 +00:00
|
|
|
updateAppsTabIndicator()
|
|
|
|
|
updateNetworkTabIndicator()
|
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
updateTabBarHeight()
|
|
|
|
|
updateAppsTabIndicator()
|
|
|
|
|
updateNetworkTabIndicator()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
window.removeEventListener('resize', updateTabBarHeight)
|
2026-02-17 15:03:34 +00:00
|
|
|
messageToast.stopPolling()
|
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)
|
|
|
|
|
|
|
|
|
|
// Desktop navigation items
|
|
|
|
|
const desktopNavItems = [
|
|
|
|
|
{
|
|
|
|
|
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',
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// Mobile navigation items (Apps and App Store combined)
|
|
|
|
|
const mobileNavItems = [
|
|
|
|
|
{
|
|
|
|
|
path: '/dashboard',
|
|
|
|
|
label: 'Home',
|
|
|
|
|
icon: 'home',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/dashboard/apps',
|
|
|
|
|
label: 'Apps',
|
|
|
|
|
icon: 'apps',
|
|
|
|
|
isCombined: true, // This combines apps and marketplace
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/dashboard/cloud',
|
|
|
|
|
label: 'Network',
|
|
|
|
|
icon: 'server',
|
|
|
|
|
isCombined: true, // This combines server and cloud
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/dashboard/web5',
|
|
|
|
|
label: 'Web5',
|
|
|
|
|
icon: 'web5',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/dashboard/settings',
|
|
|
|
|
label: 'Settings',
|
|
|
|
|
icon: 'settings',
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// Use appropriate nav items based on screen size
|
|
|
|
|
// @ts-ignore - Computed kept for future use
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
|
const navItems = computed(() => {
|
|
|
|
|
if (typeof window === 'undefined') return desktopNavItems
|
|
|
|
|
return window.innerWidth >= 768 ? desktopNavItems : mobileNavItems
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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'],
|
|
|
|
|
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() {
|
|
|
|
|
await store.logout()
|
|
|
|
|
router.push('/login')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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/settings'
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// Determine transition direction based on route depth
|
|
|
|
|
function getTransitionName(currentRoute: any) {
|
|
|
|
|
const currentPath = currentRoute.path
|
|
|
|
|
|
|
|
|
|
// If no previous path, use fade transition for initial load
|
|
|
|
|
if (!previousPath) {
|
|
|
|
|
previousPath = currentPath
|
|
|
|
|
return 'fade'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
return transitionName
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
/* 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: Current screen pulls forward, new screen emerges from back */
|
|
|
|
|
.depth-forward-enter-active.view-wrapper,
|
|
|
|
|
.depth-forward-leave-active.view-wrapper {
|
|
|
|
|
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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: Current screen pulls back, previous screen comes forward */
|
|
|
|
|
.depth-back-enter-active.view-wrapper,
|
|
|
|
|
.depth-back-leave-active.view-wrapper {
|
|
|
|
|
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Enhanced effect with rotation for more console-like feel */
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
.depth-forward-enter-from.view-wrapper {
|
|
|
|
|
transform: translateZ(-800px) scale(0.75) rotateX(8deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.depth-forward-leave-to.view-wrapper {
|
|
|
|
|
transform: translateZ(400px) scale(1.2) rotateX(-5deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.depth-back-enter-from.view-wrapper {
|
|
|
|
|
transform: translateZ(400px) scale(1.2) rotateX(-5deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.depth-back-leave-to.view-wrapper {
|
|
|
|
|
transform: translateZ(-800px) scale(0.75) rotateX(8deg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-left-enter-from.view-wrapper {
|
|
|
|
|
transform: translateX(100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-left-enter-to.view-wrapper {
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-left-leave-from.view-wrapper {
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-left-leave-to.view-wrapper {
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-right-enter-from.view-wrapper {
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-right-enter-to.view-wrapper {
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-right-leave-from.view-wrapper {
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-right-leave-to.view-wrapper {
|
|
|
|
|
transform: translateX(100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 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 */
|
|
|
|
|
.bg-perspective-container {
|
|
|
|
|
position: fixed;
|
|
|
|
|
inset: 0;
|
|
|
|
|
z-index: -10;
|
|
|
|
|
perspective: 1000px;
|
|
|
|
|
perspective-origin: 50% 50%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Background layers with 3D transitions */
|
|
|
|
|
.bg-layer {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
background-size: cover;
|
|
|
|
|
background-position: center;
|
|
|
|
|
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-4 visible, bg-3 hidden back */
|
|
|
|
|
.bg-layer:first-child {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateZ(0) scale(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bg-layer:nth-child(2) {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateZ(-200px) scale(0.9) rotateY(-15deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Transitioning out - current background moves away with zoom */
|
|
|
|
|
.bg-layer.bg-transitioning-out {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateZ(200px) scale(1.15) rotateY(15deg) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Transitioning in - new background comes forward with zoom */
|
|
|
|
|
.bg-layer.bg-transitioning-in {
|
|
|
|
|
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%); }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|