Enhance UI components and improve user notifications
- Updated App.vue to include a toast notification system for new messages, enhancing user engagement. - Modified SplashScreen.vue to streamline the intro text display with improved typing effects. - Added Montserrat font styles in style.css for better typography across the application. - Improved controller navigation in useControllerNav.ts to support enhanced focus management and sound feedback. - Updated routing logic in index.ts to redirect authenticated users from the login page to the home page. - Enhanced the Login.vue view with transition effects for a smoother user experience during login and setup processes.
This commit is contained in:
+597
-115
@@ -1,18 +1,21 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex relative dashboard-view">
|
||||
<!-- Background container with 3D perspective -->
|
||||
<div class="min-h-screen flex relative dashboard-view" :class="{ 'glass-throw-active': showZoomIn }">
|
||||
<!-- Background container with 3D perspective - full width to avoid letterboxing -->
|
||||
<div class="bg-perspective-container">
|
||||
<!-- Background - default -->
|
||||
<div
|
||||
<!-- Background - default (zoom animates on this layer, not container, to avoid letterboxing) -->
|
||||
<div
|
||||
ref="bgDefault"
|
||||
class="bg-layer"
|
||||
:class="{ 'bg-transitioning-out': showAltBackground || showWeb5Background || showNetworkBackground || showSettingsBackground || showMyAppsBackground || showAppStoreBackground || showCloudBackground || showHomeBackground }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${currentBackgroundImage})` }"
|
||||
/>
|
||||
class="bg-layer bg-fullwidth"
|
||||
:class="[
|
||||
{ 'bg-transitioning-out': showAltBackground || showWeb5Background || showNetworkBackground || showSettingsBackground || showMyAppsBackground || showAppStoreBackground || showCloudBackground || showHomeBackground },
|
||||
{ 'zoom-reveal-bg': showZoomIn }
|
||||
]"
|
||||
:style="{ backgroundImage: `url(/assets/img/${currentBackgroundImage})` }"
|
||||
/>
|
||||
<!-- Background - alternate for app details and Web5 -->
|
||||
<div
|
||||
ref="bgAlt"
|
||||
class="bg-layer"
|
||||
class="bg-layer bg-fullwidth"
|
||||
:class="{ 'bg-transitioning-in': showAltBackground || showWeb5Background || showNetworkBackground || showSettingsBackground || showMyAppsBackground || showAppStoreBackground || showCloudBackground || showHomeBackground }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
|
||||
/>
|
||||
@@ -31,8 +34,31 @@
|
||||
class="bg-glitch-scan"
|
||||
:class="{ 'glitch-active': isGlitching }"
|
||||
/>
|
||||
<!-- Continuous glitch/flash overlays - same as login, every 5s -->
|
||||
<div
|
||||
class="dashboard-glitch-layer dashboard-glitch-1 bg-fullwidth"
|
||||
:style="{ backgroundImage: `url(/assets/img/${currentBackgroundImage})` }"
|
||||
/>
|
||||
<div
|
||||
class="dashboard-glitch-layer dashboard-glitch-2 bg-fullwidth"
|
||||
:style="{ backgroundImage: `url(/assets/img/${currentBackgroundImage})` }"
|
||||
/>
|
||||
<div class="dashboard-glitch-scan" />
|
||||
</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"
|
||||
/>
|
||||
|
||||
<!-- 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"
|
||||
@@ -40,61 +66,70 @@
|
||||
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">
|
||||
<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>
|
||||
<!-- Sidebar - Desktop Only, animates in at end with separate parts -->
|
||||
<aside
|
||||
data-controller-zone="sidebar"
|
||||
class="hidden md:flex w-[256px] flex-shrink-0 relative flex-col"
|
||||
:class="{ 'sidebar-animate': showZoomIn }"
|
||||
>
|
||||
<div class="sidebar-shell">
|
||||
<div class="sidebar-inner">
|
||||
<div class="sidebar-logo flex items-center gap-3 mb-8 p-6 pb-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>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav space-y-2 p-6 pt-4">
|
||||
<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" 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 class="sidebar-controller px-6 pb-2">
|
||||
<ControllerIndicator />
|
||||
</div>
|
||||
|
||||
<div class="sidebar-logout p-6">
|
||||
<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" 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>
|
||||
</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>
|
||||
|
||||
<!-- Controller indicator - Desktop sidebar -->
|
||||
<div class="px-6 pb-2">
|
||||
<ControllerIndicator />
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<!-- Main Content (Xbox: Right goes here from sidebar) -->
|
||||
<main
|
||||
data-controller-zone="main"
|
||||
class="flex-1 overflow-hidden relative pb-20 md:pb-0 glass-piece"
|
||||
:class="{ 'glass-throw-main': showZoomIn }"
|
||||
>
|
||||
<!-- 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">
|
||||
@@ -117,34 +152,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Persistent Mobile Tabs for Apps/Marketplace -->
|
||||
<!-- Persistent Mobile Tabs for Apps/Marketplace - glass piece 3 -->
|
||||
<div
|
||||
v-if="showAppsTabs"
|
||||
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2"
|
||||
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 }"
|
||||
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">
|
||||
@@ -182,10 +194,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Persistent Mobile Tabs for Network/Cloud -->
|
||||
<!-- Persistent Mobile Tabs for Network/Cloud - glass piece 4 -->
|
||||
<div
|
||||
v-if="showNetworkTabs"
|
||||
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2"
|
||||
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 }"
|
||||
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' }"
|
||||
>
|
||||
@@ -224,7 +237,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="perspective-container-wrapper" :class="{ 'pt-40': showAppsTabs && showNetworkTabs, 'pt-20': showAppsTabs !== showNetworkTabs }">
|
||||
<div class="perspective-container-wrapper glass-piece" :class="{ 'pt-40': showAppsTabs && showNetworkTabs, 'pt-20': showAppsTabs !== showNetworkTabs, 'glass-throw-content': showZoomIn && !isHomeRoute }">
|
||||
<div class="perspective-container">
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition :name="getTransitionName(route)">
|
||||
@@ -246,11 +259,12 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Mobile Bottom Tab Bar -->
|
||||
<!-- Mobile Bottom Tab Bar - glass piece 5 -->
|
||||
<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"
|
||||
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);"
|
||||
>
|
||||
<div class="flex justify-around items-center px-2 py-3 relative">
|
||||
@@ -288,18 +302,21 @@
|
||||
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { RouterLink, RouterView, useRouter, useRoute } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useMessageToast } from '@/composables/useMessageToast'
|
||||
import { useLoginTransitionStore } from '../stores/loginTransition'
|
||||
import AnimatedLogo from '@/components/AnimatedLogo.vue'
|
||||
import ControllerIndicator from '@/components/ControllerIndicator.vue'
|
||||
import { playDashboardLoadOomph } from '@/composables/useLoginSounds'
|
||||
|
||||
const router = useRouter()
|
||||
const messageToast = useMessageToast()
|
||||
const toastMessage = messageToast.toastMessage
|
||||
const route = useRoute()
|
||||
const store = useAppStore()
|
||||
const loginTransition = useLoginTransitionStore()
|
||||
|
||||
const showZoomIn = ref(false)
|
||||
|
||||
// Background swap for app details, Web5, Network, Settings, My Apps, App Store, Cloud, and Home
|
||||
const showAltBackground = ref(false)
|
||||
const isHomeRoute = computed(() => route.path === '/dashboard' || route.path === '/dashboard/')
|
||||
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'))
|
||||
@@ -528,11 +545,35 @@ function updateNetworkTabIndicator() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.body.classList.add('dashboard-active')
|
||||
if (loginTransition.justLoggedIn) {
|
||||
playDashboardLoadOomph()
|
||||
showZoomIn.value = true
|
||||
loginTransition.setPendingWelcomeTyping(true)
|
||||
loginTransition.setJustLoggedIn(false)
|
||||
// Trigger glitches during background reveal (0.5s, 1.2s, 2s into 2.8s zoom)
|
||||
const triggerRevealGlitch = () => {
|
||||
isGlitching.value = true
|
||||
setTimeout(() => { isGlitching.value = false }, 380)
|
||||
}
|
||||
setTimeout(triggerRevealGlitch, 500)
|
||||
setTimeout(triggerRevealGlitch, 1200)
|
||||
setTimeout(triggerRevealGlitch, 2000)
|
||||
// Keep glass-throw active long enough for sidebar (last to animate, ~7.5s)
|
||||
setTimeout(() => {
|
||||
showZoomIn.value = false
|
||||
}, 8000)
|
||||
// Trigger welcome typing on Home after main content animation finishes
|
||||
setTimeout(() => {
|
||||
loginTransition.setStartWelcomeTyping(true)
|
||||
loginTransition.setPendingWelcomeTyping(false)
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
updateTabBarHeight()
|
||||
messageToast.startPolling()
|
||||
updateAppsTabIndicator()
|
||||
updateNetworkTabIndicator()
|
||||
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
updateTabBarHeight()
|
||||
updateAppsTabIndicator()
|
||||
@@ -541,8 +582,8 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.classList.remove('dashboard-active')
|
||||
window.removeEventListener('resize', updateTabBarHeight)
|
||||
messageToast.stopPolling()
|
||||
})
|
||||
|
||||
// Watch route changes to update indicator position
|
||||
@@ -786,6 +827,362 @@ function getTransitionName(currentRoute: any) {
|
||||
</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;
|
||||
}
|
||||
|
||||
.zoom-reveal-bg {
|
||||
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 */
|
||||
aside:not(.sidebar-animate) .sidebar-shell {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
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; }
|
||||
}
|
||||
|
||||
/* Wrapper to contain perspective without clipping */
|
||||
.perspective-container-wrapper {
|
||||
position: relative;
|
||||
@@ -817,10 +1214,10 @@ function getTransitionName(currentRoute: any) {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Forward transition: Current screen pulls forward, new screen emerges from back */
|
||||
/* Forward transition: 2advanced fluid depth */
|
||||
.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);
|
||||
transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.depth-forward-enter-from.view-wrapper {
|
||||
@@ -847,10 +1244,10 @@ function getTransitionName(currentRoute: any) {
|
||||
filter: blur(8px);
|
||||
}
|
||||
|
||||
/* Back transition: Current screen pulls back, previous screen comes forward */
|
||||
/* Back transition: 2advanced fluid depth */
|
||||
.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);
|
||||
transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.depth-back-enter-from.view-wrapper {
|
||||
@@ -877,22 +1274,22 @@ function getTransitionName(currentRoute: any) {
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Enhanced effect with rotation for more console-like feel */
|
||||
/* Subtle 3D tilt - 2advanced layered depth */
|
||||
@media (min-width: 768px) {
|
||||
.depth-forward-enter-from.view-wrapper {
|
||||
transform: translateZ(-800px) scale(0.75) rotateX(8deg);
|
||||
transform: translateZ(-800px) scale(0.75) rotateX(5deg);
|
||||
}
|
||||
|
||||
.depth-forward-leave-to.view-wrapper {
|
||||
transform: translateZ(400px) scale(1.2) rotateX(-5deg);
|
||||
transform: translateZ(400px) scale(1.2) rotateX(-4deg);
|
||||
}
|
||||
|
||||
.depth-back-enter-from.view-wrapper {
|
||||
transform: translateZ(400px) scale(1.2) rotateX(-5deg);
|
||||
transform: translateZ(400px) scale(1.2) rotateX(-4deg);
|
||||
}
|
||||
|
||||
.depth-back-leave-to.view-wrapper {
|
||||
transform: translateZ(-800px) scale(0.75) rotateX(8deg);
|
||||
transform: translateZ(-800px) scale(0.75) rotateX(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1016,46 +1413,52 @@ function getTransitionName(currentRoute: any) {
|
||||
transform: translateY(30vh);
|
||||
}
|
||||
|
||||
/* Background 3D container */
|
||||
.bg-perspective-container {
|
||||
/* Background 3D container - full width, black fill during zoom to avoid letterboxing */
|
||||
.dashboard-view .bg-perspective-container {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Background layers with 3D transitions */
|
||||
.bg-layer {
|
||||
/* Background layers with 3D transitions - full width like login */
|
||||
.dashboard-view .bg-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-size: cover !important;
|
||||
background-position: center center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
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 {
|
||||
.dashboard-view .bg-layer:first-of-type {
|
||||
opacity: 1;
|
||||
transform: translateZ(0) scale(1);
|
||||
}
|
||||
|
||||
.bg-layer:nth-child(2) {
|
||||
.dashboard-view .bg-layer:nth-of-type(2) {
|
||||
opacity: 0;
|
||||
transform: translateZ(-200px) scale(0.9) rotateY(-15deg);
|
||||
}
|
||||
|
||||
/* Transitioning out - current background moves away with zoom */
|
||||
.bg-layer.bg-transitioning-out {
|
||||
.dashboard-view .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 {
|
||||
.dashboard-view .bg-layer.bg-transitioning-in {
|
||||
opacity: 1;
|
||||
transform: translateZ(0) scale(1.05) rotateY(0deg) !important;
|
||||
}
|
||||
@@ -1166,5 +1569,84 @@ function getTransitionName(currentRoute: any) {
|
||||
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%); }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
+385
-185
@@ -2,8 +2,14 @@
|
||||
<div>
|
||||
<div class="mb-8 flex items-start justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">Welcome Noderunner</h1>
|
||||
<p class="text-white/80">Here's an overview of your sovereign life</p>
|
||||
<div class="min-h-[4.5rem]">
|
||||
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">
|
||||
{{ line1Text }}<span v-if="showCaretLine1" class="typing-caret"></span>
|
||||
</h1>
|
||||
<p class="text-white/80">
|
||||
{{ line2Text }}<span v-if="showCaretLine2" class="typing-caret"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Compact Status Indicator -->
|
||||
<div class="flex items-center gap-2 px-4 py-2 glass-card rounded-lg">
|
||||
@@ -15,212 +21,211 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Overviews -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<!-- Section Overviews - 2advanced staggered animation (hidden until typing starts, then animate with typing) -->
|
||||
<div
|
||||
class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8 transition-opacity duration-300"
|
||||
:class="{ 'opacity-0 pointer-events-none': showWelcomeBlock && !animateCards }"
|
||||
>
|
||||
<!-- My Apps Overview -->
|
||||
<div class="glass-card p-6 flex flex-col h-full min-h-0">
|
||||
<div class="flex items-start justify-between mb-4 shrink-0">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white mb-1">My Apps</h2>
|
||||
<p class="text-sm text-white/70">Manage your installed applications</p>
|
||||
<div
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="home-card controller-focusable"
|
||||
:class="{ 'home-card-animate': animateCards }"
|
||||
style="--card-stagger: 0"
|
||||
>
|
||||
<div class="home-card-shell">
|
||||
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
||||
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
||||
<div class="home-card-text">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">My Apps</h2>
|
||||
<p class="text-sm text-white/70">Manage your installed applications</p>
|
||||
</div>
|
||||
<RouterLink to="/dashboard/apps" class="text-white/60 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="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="home-card-stats grid grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Installed</p>
|
||||
<p class="text-2xl font-bold text-white">{{ appCount }}</p>
|
||||
</div>
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Running</p>
|
||||
<p class="text-2xl font-bold text-white">{{ runningCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<RouterLink to="/dashboard/marketplace" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
||||
Browse Store
|
||||
</RouterLink>
|
||||
<RouterLink to="/dashboard/apps" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
||||
Manage Apps
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<RouterLink
|
||||
to="/dashboard/apps"
|
||||
class="text-white/60 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="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Installed</p>
|
||||
<p class="text-2xl font-bold text-white">{{ appCount }}</p>
|
||||
</div>
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Running</p>
|
||||
<p class="text-2xl font-bold text-white">{{ runningCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<RouterLink
|
||||
to="/dashboard/marketplace"
|
||||
class="flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
Browse Store
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/dashboard/apps"
|
||||
class="flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
Manage Apps
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cloud Overview -->
|
||||
<div class="glass-card p-6 flex flex-col h-full min-h-0">
|
||||
<div class="flex items-start justify-between mb-4 shrink-0">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Cloud</h2>
|
||||
<p class="text-sm text-white/70">Cloud services and storage</p>
|
||||
<div
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="home-card controller-focusable"
|
||||
:class="{ 'home-card-animate': animateCards }"
|
||||
style="--card-stagger: 1"
|
||||
>
|
||||
<div class="home-card-shell">
|
||||
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
||||
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
||||
<div class="home-card-text">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Cloud</h2>
|
||||
<p class="text-sm text-white/70">Cloud services and storage</p>
|
||||
</div>
|
||||
<RouterLink to="/dashboard/cloud" class="text-white/60 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="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="home-card-stats grid grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Storage Used</p>
|
||||
<p class="text-2xl font-bold text-white">2.4 GB</p>
|
||||
</div>
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Folders</p>
|
||||
<p class="text-2xl font-bold text-white">5</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<RouterLink to="/dashboard/cloud" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
||||
View Folders
|
||||
</RouterLink>
|
||||
<button class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" @click="() => {}">
|
||||
Upload Files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<RouterLink
|
||||
to="/dashboard/cloud"
|
||||
class="text-white/60 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="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Storage Used</p>
|
||||
<p class="text-2xl font-bold text-white">2.4 GB</p>
|
||||
</div>
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Folders</p>
|
||||
<p class="text-2xl font-bold text-white">5</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<RouterLink
|
||||
to="/dashboard/cloud"
|
||||
class="flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
View Folders
|
||||
</RouterLink>
|
||||
<button
|
||||
class="flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors"
|
||||
@click="() => {}"
|
||||
>
|
||||
Upload Files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Overview -->
|
||||
<div class="glass-card p-6 flex flex-col h-full min-h-0">
|
||||
<div class="flex items-start justify-between mb-4 shrink-0">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Network</h2>
|
||||
<p class="text-sm text-white/70">Network infrastructure and Web3 services</p>
|
||||
</div>
|
||||
<RouterLink
|
||||
to="/dashboard/server"
|
||||
class="text-white/60 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="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 mb-4 flex-1 min-h-0">
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<span class="text-sm text-white/80">Services Status</span>
|
||||
<div
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="home-card controller-focusable"
|
||||
:class="{ 'home-card-animate': animateCards }"
|
||||
style="--card-stagger: 2"
|
||||
>
|
||||
<div class="home-card-shell">
|
||||
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
||||
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
||||
<div class="home-card-text">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Network</h2>
|
||||
<p class="text-sm text-white/70">Network infrastructure and Web3 services</p>
|
||||
</div>
|
||||
<RouterLink to="/dashboard/server" class="text-white/60 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="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<span class="text-sm text-green-400 font-medium">All Running</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<span class="text-sm text-white/80">Connectivity</span>
|
||||
<div class="home-card-stats space-y-3 mb-4 flex-1 min-h-0">
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<span class="text-sm text-white/80">Services Status</span>
|
||||
</div>
|
||||
<span class="text-sm text-green-400 font-medium">All Running</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<span class="text-sm text-white/80">Connectivity</span>
|
||||
</div>
|
||||
<span class="text-sm text-green-400 font-medium">Connected</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-blue-400"></div>
|
||||
<span class="text-sm text-white/80">Connected Nodes</span>
|
||||
</div>
|
||||
<span class="text-sm text-white/80 font-medium">12</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-green-400 font-medium">Connected</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-blue-400"></div>
|
||||
<span class="text-sm text-white/80">Connected Nodes</span>
|
||||
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<RouterLink to="/dashboard/server" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
||||
Manage Network
|
||||
</RouterLink>
|
||||
<button class="home-card-btn px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" @click="() => {}">
|
||||
<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="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>
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-sm text-white/80 font-medium">12</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<RouterLink
|
||||
to="/dashboard/server"
|
||||
class="flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
Manage Network
|
||||
</RouterLink>
|
||||
<button
|
||||
class="px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors"
|
||||
@click="() => {}"
|
||||
>
|
||||
<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="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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Web5 Overview -->
|
||||
<div class="glass-card p-6 flex flex-col h-full min-h-0">
|
||||
<div class="flex items-start justify-between mb-4 shrink-0">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Web5</h2>
|
||||
<p class="text-sm text-white/70">Decentralized identity and data protocols</p>
|
||||
</div>
|
||||
<RouterLink
|
||||
to="/dashboard/web5"
|
||||
class="text-white/60 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="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 mb-4 flex-1 min-h-0">
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<span class="text-sm text-white/80">DID Status</span>
|
||||
<div
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="home-card controller-focusable"
|
||||
:class="{ 'home-card-animate': animateCards }"
|
||||
style="--card-stagger: 3"
|
||||
>
|
||||
<div class="home-card-shell">
|
||||
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
||||
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
||||
<div class="home-card-text">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Web5</h2>
|
||||
<p class="text-sm text-white/70">Decentralized identity and data protocols</p>
|
||||
</div>
|
||||
<RouterLink to="/dashboard/web5" class="text-white/60 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="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<span class="text-sm text-green-400 font-medium">Active</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<span class="text-sm text-white/80">DWN Sync</span>
|
||||
<div class="home-card-stats space-y-3 mb-4 flex-1 min-h-0">
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<span class="text-sm text-white/80">DID Status</span>
|
||||
</div>
|
||||
<span class="text-sm text-green-400 font-medium">Active</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
||||
<span class="text-sm text-white/80">DWN Sync</span>
|
||||
</div>
|
||||
<span class="text-sm text-green-400 font-medium">Synced</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-lg text-orange-500 font-bold">₿</span>
|
||||
<span class="text-sm text-white/80">Networking Profits</span>
|
||||
</div>
|
||||
<span class="text-sm text-orange-500 font-medium">₿0.024</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-green-400 font-medium">Synced</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-lg text-orange-500 font-bold">₿</span>
|
||||
<span class="text-sm text-white/80">Networking Profits</span>
|
||||
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<RouterLink to="/dashboard/web5" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
||||
Manage Web5
|
||||
</RouterLink>
|
||||
<button class="home-card-btn px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" @click="() => {}">
|
||||
<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="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>
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-sm text-orange-500 font-medium">₿0.024</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<RouterLink
|
||||
to="/dashboard/web5"
|
||||
class="flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors"
|
||||
>
|
||||
Manage Web5
|
||||
</RouterLink>
|
||||
<button
|
||||
class="px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors"
|
||||
@click="() => {}"
|
||||
>
|
||||
<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="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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -265,12 +270,74 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useLoginTransitionStore } from '../stores/loginTransition'
|
||||
import { PackageState } from '../types/api'
|
||||
import { playTypingSound } from '@/composables/useLoginSounds'
|
||||
|
||||
const store = useAppStore()
|
||||
const loginTransition = useLoginTransitionStore()
|
||||
|
||||
const LINE1 = "Welcome Noderunner"
|
||||
const LINE2 = "Here's an overview of your sovereign life"
|
||||
const MS_PER_CHAR = 55
|
||||
|
||||
const displayLine1 = ref('')
|
||||
const displayLine2 = ref('')
|
||||
const showCaretLine1 = ref(false)
|
||||
const showCaretLine2 = ref(false)
|
||||
const showWelcomeBlock = ref(false)
|
||||
const hasTypedWelcome = ref(false)
|
||||
const animateCards = ref(false)
|
||||
let typingInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const line1Text = computed(() =>
|
||||
showWelcomeBlock.value ? displayLine1.value : LINE1
|
||||
)
|
||||
const line2Text = computed(() =>
|
||||
showWelcomeBlock.value ? displayLine2.value : LINE2
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (typingInterval) clearInterval(typingInterval)
|
||||
})
|
||||
|
||||
watch(() => loginTransition.pendingWelcomeTyping, (pending) => {
|
||||
if (pending) showWelcomeBlock.value = true
|
||||
})
|
||||
|
||||
watch(() => loginTransition.startWelcomeTyping, (shouldStart) => {
|
||||
if (!shouldStart || hasTypedWelcome.value) return
|
||||
hasTypedWelcome.value = true
|
||||
showWelcomeBlock.value = true
|
||||
displayLine1.value = ''
|
||||
displayLine2.value = ''
|
||||
showCaretLine1.value = true
|
||||
showCaretLine2.value = false
|
||||
|
||||
playTypingSound()
|
||||
animateCards.value = true
|
||||
|
||||
let i = 0
|
||||
typingInterval = setInterval(() => {
|
||||
if (i < LINE1.length) {
|
||||
displayLine1.value = LINE1.slice(0, i + 1)
|
||||
i++
|
||||
} else if (i < LINE1.length + LINE2.length) {
|
||||
showCaretLine1.value = false
|
||||
showCaretLine2.value = true
|
||||
displayLine2.value = LINE2.slice(0, i - LINE1.length + 1)
|
||||
i++
|
||||
} else {
|
||||
if (typingInterval) clearInterval(typingInterval)
|
||||
typingInterval = null
|
||||
showCaretLine2.value = false
|
||||
loginTransition.setStartWelcomeTyping(false)
|
||||
}
|
||||
}, MS_PER_CHAR)
|
||||
}, { immediate: true })
|
||||
|
||||
// @ts-ignore - Computed kept for future use
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -282,3 +349,136 @@ const runningCount = computed(() =>
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.typing-caret::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 1.1em;
|
||||
background: #00ffff;
|
||||
margin-left: 2px;
|
||||
vertical-align: text-bottom;
|
||||
animation: caret-blink 0.7s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes caret-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* 2advanced-style card animation sequence */
|
||||
.home-card {
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.home-card-shell {
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
border: 1px solid transparent;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.home-card-animate .home-card-shell {
|
||||
animation: card-fly-in 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
animation-delay: calc(var(--card-stagger) * 0.18s);
|
||||
opacity: 0;
|
||||
transform: translateY(50px) scale(0.92);
|
||||
}
|
||||
|
||||
@keyframes card-fly-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(50px) scale(0.92);
|
||||
border-color: transparent;
|
||||
}
|
||||
75% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
border-color: transparent;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.home-card-inner {
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.home-card-animate .home-card-inner {
|
||||
animation: inner-draw 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
animation-delay: calc(var(--card-stagger) * 0.18s + 0.9s);
|
||||
}
|
||||
|
||||
@keyframes inner-draw {
|
||||
0% {
|
||||
opacity: 0;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
}
|
||||
15% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
.home-card-text {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.home-card-stats {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.home-card-btn {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.home-card-animate .home-card-btn {
|
||||
animation: btn-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
animation-delay: calc(var(--card-stagger) * 0.18s + 1.5s);
|
||||
}
|
||||
|
||||
@keyframes btn-pop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
border-color: transparent;
|
||||
}
|
||||
85% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
border-color: transparent;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
/* When not animating, show everything */
|
||||
.home-card:not(.home-card-animate) .home-card-inner,
|
||||
.home-card:not(.home-card-animate) .home-card-btn {
|
||||
opacity: 1;
|
||||
animation: none;
|
||||
clip-path: none;
|
||||
transform: none;
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.home-card:not(.home-card-animate) .home-card-shell {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
<div class="min-h-screen flex items-center justify-center p-4 relative z-10">
|
||||
<div class="w-full max-w-md relative z-20">
|
||||
<!-- Login Card -->
|
||||
<div class="glass-card p-8 pt-20 relative login-card overflow-visible">
|
||||
<div
|
||||
class="glass-card p-8 pt-20 relative login-card overflow-visible transition-all duration-300"
|
||||
:class="{ 'whoosh-away': whooshAway }"
|
||||
>
|
||||
<!-- Logo - half in, half out of container -->
|
||||
<div class="absolute -top-10 left-1/2 -translate-x-1/2 z-10">
|
||||
<div class="logo-gradient-border">
|
||||
@@ -134,39 +137,46 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useLoginTransitionStore } from '../stores/loginTransition'
|
||||
import { rpcClient } from '../api/rpc-client'
|
||||
import { startSynthwave, stopSynthwave, playLoginSuccessWhoosh } from '@/composables/useLoginSounds'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
const loginTransition = useLoginTransitionStore()
|
||||
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isSetup = ref(false)
|
||||
const whooshAway = ref(false)
|
||||
|
||||
// Check if we're in setup mode (original StartOS node setup)
|
||||
const isSetupMode = computed(() => {
|
||||
return import.meta.env.VITE_DEV_MODE === 'setup'
|
||||
})
|
||||
|
||||
// Check if node is already set up
|
||||
onMounted(async () => {
|
||||
if (sessionStorage.getItem('archipelago_from_splash') !== '1') {
|
||||
startSynthwave()
|
||||
} else {
|
||||
sessionStorage.removeItem('archipelago_from_splash')
|
||||
}
|
||||
if (isSetupMode.value) {
|
||||
try {
|
||||
const result = await rpcClient.call<boolean>({ method: 'auth.isSetup', params: {} })
|
||||
isSetup.value = Boolean(result)
|
||||
} catch (err) {
|
||||
console.error('Failed to check setup status:', err)
|
||||
// Assume not set up if check fails
|
||||
isSetup.value = false
|
||||
}
|
||||
} else {
|
||||
// Not in setup mode, assume already set up
|
||||
isSetup.value = true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
async function handleSetup() {
|
||||
if (!password.value || password.value.length < 8) {
|
||||
error.value = 'Password must be at least 8 characters'
|
||||
@@ -187,11 +197,17 @@ async function handleSetup() {
|
||||
params: { password: password.value }
|
||||
})
|
||||
|
||||
// After setup, automatically log in
|
||||
stopSynthwave()
|
||||
whooshAway.value = true
|
||||
playLoginSuccessWhoosh()
|
||||
loginTransition.setJustLoggedIn(true)
|
||||
await store.login(password.value)
|
||||
router.push('/dashboard')
|
||||
await new Promise(r => setTimeout(r, 350))
|
||||
router.replace({ name: 'home' })
|
||||
} catch (err) {
|
||||
whooshAway.value = false
|
||||
error.value = err instanceof Error ? err.message : 'Setup failed. Please try again.'
|
||||
startSynthwave()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -205,9 +221,16 @@ async function handleLogin() {
|
||||
|
||||
try {
|
||||
await store.login(password.value)
|
||||
router.push('/dashboard')
|
||||
stopSynthwave()
|
||||
whooshAway.value = true
|
||||
playLoginSuccessWhoosh()
|
||||
loginTransition.setJustLoggedIn(true)
|
||||
await new Promise(r => setTimeout(r, 350))
|
||||
router.replace({ name: 'home' })
|
||||
} catch (err) {
|
||||
whooshAway.value = false
|
||||
error.value = err instanceof Error ? err.message : 'Login failed. Please check your password.'
|
||||
startSynthwave()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -221,3 +244,11 @@ function replayIntro() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Whoosh accent - login card blurs and shrinks as whoosh plays */
|
||||
.whoosh-away {
|
||||
transform: scale(0.92);
|
||||
filter: blur(6px);
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,19 @@
|
||||
<source src="/assets/video/video-intro.mp4?v=7" type="video/mp4">
|
||||
</video>
|
||||
|
||||
<!-- Static image background for other routes (not using video) -->
|
||||
<!-- Login: static background + archipelago-style glitch (no zoom) -->
|
||||
<template v-else-if="isLoginRoute">
|
||||
<div
|
||||
class="bg-layer bg-login-static bg-fullwidth"
|
||||
:style="{ backgroundImage: `url('/assets/img/${loginBackground}')` }"
|
||||
/>
|
||||
<!-- Archipelago-style glitch overlays - continuous every 5s -->
|
||||
<div class="login-glitch-layer login-glitch-1" :style="{ backgroundImage: `url('/assets/img/${loginBackground}')` }" />
|
||||
<div class="login-glitch-layer login-glitch-2" :style="{ backgroundImage: `url('/assets/img/${loginBackground}')` }" />
|
||||
<div class="login-glitch-scan" />
|
||||
</template>
|
||||
|
||||
<!-- Static image background for other routes (with zoom on transition) -->
|
||||
<div
|
||||
v-else
|
||||
class="bg-layer bg-zoom"
|
||||
@@ -29,9 +41,8 @@
|
||||
:key="currentBackground"
|
||||
></div>
|
||||
|
||||
|
||||
<!-- Glitch overlay layer - only for non-video backgrounds -->
|
||||
<div v-show="isGlitching && !useVideoBackground" class="bg-glitch-layer"></div>
|
||||
<!-- Glitch overlay layer - only for non-video, non-login background changes -->
|
||||
<div v-show="isGlitching && !useVideoBackground && !isLoginRoute" class="bg-glitch-layer"></div>
|
||||
</div>
|
||||
|
||||
<!-- Content with 3D transitions -->
|
||||
@@ -52,6 +63,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { resumeAudioContext, startSynthwave } from '@/composables/useLoginSounds'
|
||||
|
||||
const route = useRoute()
|
||||
const currentBackground = ref('bg-4.jpg')
|
||||
@@ -59,9 +71,12 @@ const isGlitching = ref(false)
|
||||
const isTransitioning = ref(false)
|
||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||
|
||||
// Routes that should use video background (smooth transition from splash)
|
||||
// Routes that should use video background (smooth transition from splash, loops through login)
|
||||
const videoBackgroundRoutes = ['/onboarding/intro', '/login']
|
||||
|
||||
// Login uses video when coming from splash, or static + glitch when direct
|
||||
const isLoginRoute = computed(() => route.path === '/login')
|
||||
|
||||
// Check if current route should use video background
|
||||
const useVideoBackground = computed(() => {
|
||||
return videoBackgroundRoutes.includes(route.path)
|
||||
@@ -77,9 +92,11 @@ const routeBackgrounds: Record<string, string> = {
|
||||
'/onboarding/backup': 'bg-7.jpg',
|
||||
'/onboarding/verify': 'bg-2.jpg',
|
||||
'/onboarding/done': 'bg-1.jpg',
|
||||
'/login': 'bg-1.jpg' // Video will be used instead
|
||||
'/login': 'bg-4.jpg' // Video loops from splash (same as intro)
|
||||
}
|
||||
|
||||
const loginBackground = 'bg-1.jpg'
|
||||
|
||||
// Restore video time from splash screen for seamless transition
|
||||
function restoreVideoTime() {
|
||||
if (videoElement.value && useVideoBackground.value) {
|
||||
@@ -100,14 +117,7 @@ function restoreVideoTime() {
|
||||
|
||||
// If video was playing, ensure it continues playing immediately
|
||||
if (wasPlaying) {
|
||||
// Use requestAnimationFrame for smoother transition
|
||||
requestAnimationFrame(() => {
|
||||
if (videoElement.value) {
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video play failed after time restore:', err)
|
||||
})
|
||||
}
|
||||
})
|
||||
requestAnimationFrame(() => ensureVideoPlaying())
|
||||
}
|
||||
|
||||
// Clean up session storage after successful restore
|
||||
@@ -137,6 +147,18 @@ function restoreVideoTime() {
|
||||
}
|
||||
}
|
||||
|
||||
// Video play with retry - hardened for reliability
|
||||
function ensureVideoPlaying(retries = 3): void {
|
||||
const vid = videoElement.value
|
||||
if (!vid || !useVideoBackground.value) return
|
||||
if (!vid.paused) return
|
||||
vid.play()
|
||||
.then(() => {})
|
||||
.catch(() => {
|
||||
if (retries > 0) setTimeout(() => ensureVideoPlaying(retries - 1), 300)
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure video plays when route uses video background
|
||||
watch([useVideoBackground, route], ([useVideo]) => {
|
||||
if (useVideo && videoElement.value) {
|
||||
@@ -148,9 +170,7 @@ watch([useVideoBackground, route], ([useVideo]) => {
|
||||
// Then ensure it's playing - use double RAF for smoother transition
|
||||
requestAnimationFrame(() => {
|
||||
if (videoElement.value && videoElement.value.paused) {
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video autoplay failed:', err)
|
||||
})
|
||||
ensureVideoPlaying()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -169,9 +189,7 @@ onMounted(() => {
|
||||
// Use double RAF for smoother playback start
|
||||
requestAnimationFrame(() => {
|
||||
if (videoElement.value) {
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video autoplay failed on mount:', err)
|
||||
})
|
||||
ensureVideoPlaying()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -213,9 +231,7 @@ watch(() => route.path, (newPath, oldPath) => {
|
||||
if (oldUsesVideo && newUsesVideo && videoElement.value) {
|
||||
// Video continues seamlessly, just ensure it's playing
|
||||
if (videoElement.value.paused) {
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video play failed on route change:', err)
|
||||
})
|
||||
ensureVideoPlaying()
|
||||
}
|
||||
// No glitch effect, no zoom, no transitions for video-to-video
|
||||
isGlitching.value = false
|
||||
@@ -229,6 +245,14 @@ watch(() => route.path, (newPath, oldPath) => {
|
||||
isTransitioning.value = false
|
||||
}
|
||||
|
||||
// Login route: set background immediately, no zoom, no transition (glitch is always-on)
|
||||
if (newPath === '/login') {
|
||||
currentBackground.value = 'bg-1.jpg'
|
||||
isTransitioning.value = false
|
||||
isGlitching.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Only update if we have a defined background for this route and it's different
|
||||
if (newBg && newBg !== currentBackground.value) {
|
||||
// Trigger zoom animation ONLY for non-video routes (never for video)
|
||||
@@ -260,11 +284,8 @@ watch(() => route.path, (newPath, oldPath) => {
|
||||
// Prevent video from pausing during transitions
|
||||
function handleVideoPause(event: Event) {
|
||||
if (useVideoBackground.value && videoElement.value) {
|
||||
// Prevent pause and immediately resume playback
|
||||
event.preventDefault()
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video play failed after pause prevention:', err)
|
||||
})
|
||||
ensureVideoPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,14 +293,12 @@ function handleVideoPause(event: Event) {
|
||||
function handleVideoEnded() {
|
||||
if (useVideoBackground.value && videoElement.value) {
|
||||
videoElement.value.currentTime = 0
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video play failed after loop:', err)
|
||||
})
|
||||
ensureVideoPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
// Update body class to disable global glitch effects ONLY for video backgrounds
|
||||
// This class is ONLY added on /onboarding/intro and /login routes
|
||||
// This class is ONLY added on /onboarding/intro (login uses its own glitch)
|
||||
// All other routes will have glitch effects enabled (normal behavior)
|
||||
watch(useVideoBackground, (usesVideo) => {
|
||||
if (usesVideo) {
|
||||
@@ -299,11 +318,20 @@ onMounted(() => {
|
||||
currentBackground.value = bg
|
||||
}
|
||||
|
||||
// Ensure no transitions or effects on mount for video backgrounds
|
||||
if (useVideoBackground.value) {
|
||||
isTransitioning.value = false
|
||||
isGlitching.value = false
|
||||
document.body.classList.add('video-background-active')
|
||||
if (sessionStorage.getItem('archipelago_from_splash') !== '1') {
|
||||
startSynthwave()
|
||||
}
|
||||
const unlock = () => {
|
||||
resumeAudioContext()
|
||||
document.removeEventListener('click', unlock)
|
||||
document.removeEventListener('touchstart', unlock)
|
||||
}
|
||||
document.addEventListener('click', unlock, { once: true })
|
||||
document.addEventListener('touchstart', unlock, { once: true })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -338,16 +366,16 @@ onMounted(() => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Forward transition: Current screen pulls forward, new screen emerges from back */
|
||||
/* 2advanced-style: fluid depth transitions */
|
||||
.depth-forward-enter-active.view-wrapper,
|
||||
.depth-forward-leave-active.view-wrapper {
|
||||
transition: all 0.7s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
transition: all 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.depth-forward-enter-from.view-wrapper {
|
||||
opacity: 0;
|
||||
transform: translateZ(-1500px) scale(0.5);
|
||||
filter: blur(8px);
|
||||
transform: translateZ(-1200px) scale(0.6);
|
||||
filter: blur(10px);
|
||||
}
|
||||
|
||||
.depth-forward-enter-to.view-wrapper {
|
||||
@@ -364,13 +392,13 @@ onMounted(() => {
|
||||
|
||||
.depth-forward-leave-to.view-wrapper {
|
||||
opacity: 0;
|
||||
transform: translateZ(600px) scale(1.4);
|
||||
filter: blur(12px);
|
||||
transform: translateZ(500px) scale(1.25);
|
||||
filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Background zoom effect - makes you feel like you're going deeper */
|
||||
/* Background zoom - 2advanced fluid */
|
||||
.bg-zoom {
|
||||
transition: transform 1.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: transform 1.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@@ -378,14 +406,14 @@ onMounted(() => {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
/* Enhanced effect with rotation for more console-like feel */
|
||||
/* Subtle 3D tilt - 2advanced layered depth */
|
||||
@media (min-width: 768px) {
|
||||
.depth-forward-enter-from.view-wrapper {
|
||||
transform: translateZ(-1500px) scale(0.5) rotateX(15deg);
|
||||
transform: translateZ(-1200px) scale(0.6) rotateX(6deg);
|
||||
}
|
||||
|
||||
.depth-forward-leave-to.view-wrapper {
|
||||
transform: translateZ(600px) scale(1.4) rotateX(-10deg);
|
||||
transform: translateZ(500px) scale(1.25) rotateX(-4deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +425,16 @@ onMounted(() => {
|
||||
perspective-origin: 50% 50%;
|
||||
z-index: -10;
|
||||
overflow: hidden;
|
||||
min-width: 100vw;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* Full width background on every screen */
|
||||
.bg-fullwidth {
|
||||
min-width: 100vw;
|
||||
width: 100vw;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.bg-layer {
|
||||
@@ -425,6 +463,79 @@ video.bg-layer {
|
||||
transform: translateZ(0) scale(1);
|
||||
}
|
||||
|
||||
/* Login: static background - just there, no zoom */
|
||||
.bg-login-static {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Archipelago-style glitch overlays for login - continuous every 5s */
|
||||
.login-glitch-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.login-glitch-1 {
|
||||
mix-blend-mode: screen;
|
||||
filter: hue-rotate(22deg) saturate(1.35);
|
||||
animation: login-glitch-shift 5s steps(10, end) infinite;
|
||||
}
|
||||
|
||||
.login-glitch-2 {
|
||||
mix-blend-mode: screen;
|
||||
filter: hue-rotate(-30deg) saturate(1.45);
|
||||
animation: login-glitch-shift-2 5s steps(9, end) infinite;
|
||||
}
|
||||
|
||||
.login-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: login-glitch-scan 5s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes login-glitch-shift {
|
||||
0%, 82% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
|
||||
82.1% { opacity: 0.22; }
|
||||
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 login-glitch-shift-2 {
|
||||
0%, 82% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
|
||||
82.1% { opacity: 0.18; }
|
||||
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 login-glitch-scan {
|
||||
0%, 82% { opacity: 0; transform: translateY(-20%); }
|
||||
84% { opacity: 0.4; }
|
||||
90% { opacity: 0.28; }
|
||||
100% { opacity: 0; transform: translateY(115%); }
|
||||
}
|
||||
|
||||
/* Glitch overlay layer */
|
||||
.bg-glitch-layer {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user