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:
Dorian
2026-02-17 19:19:54 +00:00
parent 1073d9fd2c
commit 1b05b5b8f1
24 changed files with 2038 additions and 470 deletions
+597 -115
View File
@@ -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
View File
@@ -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>
+38 -7
View File
@@ -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>
+154 -43
View File
@@ -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;