diff --git a/web/src/client/app.ts b/web/src/client/app.ts index fd90da75..ba2ed157 100644 --- a/web/src/client/app.ts +++ b/web/src/client/app.ts @@ -288,25 +288,77 @@ export class VibeTunnelApp extends LitElement { if (!this.initialLoadComplete) { this.loading = true; } - try { - const headers = authClient.getAuthHeader(); - const response = await fetch('/api/sessions', { headers }); - if (response.ok) { - this.sessions = (await response.json()) as Session[]; - this.clearError(); - } else if (response.status === 401) { - // Authentication failed, redirect to login - this.handleLogout(); - return; - } else { + + const performLoad = async () => { + try { + const headers = authClient.getAuthHeader(); + const response = await fetch('/api/sessions', { headers }); + if (response.ok) { + this.sessions = (await response.json()) as Session[]; + this.clearError(); + } else if (response.status === 401) { + // Authentication failed, redirect to login + this.handleLogout(); + return; + } else { + this.showError('Failed to load sessions'); + } + } catch (error) { + logger.error('error loading sessions:', error); this.showError('Failed to load sessions'); + } finally { + this.loading = false; + this.initialLoadComplete = true; + } + }; + + // Use view transition for initial load with fade effect + if ( + !this.initialLoadComplete && + 'startViewTransition' in document && + typeof document.startViewTransition === 'function' + ) { + logger.log('🎨 Using View Transition API for initial session load'); + // Add initial-load class for specific CSS handling + document.body.classList.add('initial-session-load'); + + const transition = document.startViewTransition(async () => { + await performLoad(); + await this.updateComplete; + }); + + // Log when transition is ready + transition.ready + .then(() => { + logger.log('✨ Initial load view transition ready'); + }) + .catch((err) => { + logger.error('❌ Initial load view transition failed:', err); + }); + + // Clean up the class after transition completes + transition.finished + .finally(() => { + logger.log('✅ Initial load view transition finished'); + document.body.classList.remove('initial-session-load'); + }) + .catch(() => { + // Ignore errors, just make sure we clean up + document.body.classList.remove('initial-session-load'); + }); + } else { + // Regular load without transition + if (!this.initialLoadComplete) { + logger.log('🎨 Using CSS animation fallback for initial load'); + document.body.classList.add('initial-session-load'); + await performLoad(); + // Remove class after animation completes + setTimeout(() => { + document.body.classList.remove('initial-session-load'); + }, 600); + } else { + await performLoad(); } - } catch (error) { - logger.error('error loading sessions:', error); - this.showError('Failed to load sessions'); - } finally { - this.loading = false; - this.initialLoadComplete = true; } } @@ -875,6 +927,7 @@ export class VibeTunnelApp extends LitElement { }; private handleOpenSettings = () => { + console.log('🎯 handleOpenSettings called in app.ts'); this.showSettings = true; }; diff --git a/web/src/client/components/full-header.ts b/web/src/client/components/full-header.ts index 965dc621..2cdc71b7 100644 --- a/web/src/client/components/full-header.ts +++ b/web/src/client/components/full-header.ts @@ -6,7 +6,6 @@ import { html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { HeaderBase } from './header-base.js'; -import type { Session } from './session-list.js'; import './terminal-icon.js'; import './notification-status.js'; diff --git a/web/src/client/styles.css b/web/src/client/styles.css index 2c8684ae..d7e32b16 100644 --- a/web/src/client/styles.css +++ b/web/src/client/styles.css @@ -161,7 +161,20 @@ /* Authentication styles */ .auth-container { - @apply min-h-screen bg-dark-bg flex items-center justify-center p-4; + @apply bg-dark-bg flex items-center justify-center p-4; + min-height: 100vh; + min-height: 100dvh; /* Dynamic viewport height for mobile */ + } + + @media (max-width: 768px) { + .auth-container { + @apply py-8; /* Add vertical padding on mobile */ + min-height: 100vh; + min-height: 100dvh; + /* Allow natural scrolling if content exceeds viewport */ + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } } .auth-header { @@ -365,6 +378,55 @@ body.sessions-showing .space-y-2 > div:nth-child(n+9) { animation-delay: 0.4s; } } } +/* Initial session load animation */ +@keyframes initialLoad { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Apply fade animation to session cards during initial load */ +body.initial-session-load .session-flex-responsive > session-card { + animation: initialLoad 0.4s ease-out backwards; +} + +/* Stagger animation for initial load */ +body.initial-session-load .session-flex-responsive > session-card:nth-child(1) { animation-delay: 0s; } +body.initial-session-load .session-flex-responsive > session-card:nth-child(2) { animation-delay: 0.05s; } +body.initial-session-load .session-flex-responsive > session-card:nth-child(3) { animation-delay: 0.1s; } +body.initial-session-load .session-flex-responsive > session-card:nth-child(4) { animation-delay: 0.15s; } +body.initial-session-load .session-flex-responsive > session-card:nth-child(5) { animation-delay: 0.2s; } +body.initial-session-load .session-flex-responsive > session-card:nth-child(6) { animation-delay: 0.25s; } +body.initial-session-load .session-flex-responsive > session-card:nth-child(7) { animation-delay: 0.3s; } +body.initial-session-load .session-flex-responsive > session-card:nth-child(8) { animation-delay: 0.35s; } +body.initial-session-load .session-flex-responsive > session-card:nth-child(n+9) { animation-delay: 0.4s; } + +/* View transition specific styles for initial load */ +::view-transition-old(root) { + animation: 90ms ease-out fade-out; +} + +::view-transition-new(root) { + animation: 210ms ease-in fade-in; +} + +body.initial-session-load ::view-transition-new(root) { + animation-duration: 400ms; +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fade-out { + from { opacity: 1; } + to { opacity: 0; } +} + /* View transition names for smoother morphing */ .session-card { view-transition-name: session-card;