|
1 | 1 | import { useState, useEffect } from 'react'; |
2 | 2 | import { useAuth } from '@/hooks/useAuth'; |
| 3 | +import { useMinDuration } from '@/hooks/useMinDuration'; |
3 | 4 | import LoginPage from '@/components/LoginPage/LoginPage'; |
4 | 5 | import SessionsHub from '@/components/SessionsHub/SessionsHub'; |
5 | 6 | import { TerminalApp } from '@/components/TerminalApp/TerminalApp'; |
6 | 7 | import CodeViewer from '@/components/CodeViewer/CodeViewer'; |
| 8 | +import { Splash } from '@/components/common/Splash'; |
| 9 | +import splashStyles from '@/components/common/Splash.module.css'; |
7 | 10 | import { useThemeStore } from '@/stores/themeStore'; |
8 | 11 | import { usePreferencesStore } from '@/stores/preferencesStore'; |
9 | 12 | import { THEMES } from '@/themes/terminalThemes'; |
@@ -51,6 +54,15 @@ export default function App() { |
51 | 54 | const { authenticated, passwordRequired, login, loading } = useAuth(); |
52 | 55 | const [path, setPath] = useState(getPath); |
53 | 56 |
|
| 57 | + /* |
| 58 | + * Hold the splash screen for at least 1500ms on cold load so the |
| 59 | + * per-letter Keynote-bloom animation (last letter starts at 0.55s, |
| 60 | + * 0.85s duration = 1.40s end) plays through with a 100ms beat to read |
| 61 | + * the settled wordmark. Without this gate, auth on localhost resolves |
| 62 | + * in ~50ms and the user never sees the animation. |
| 63 | + */ |
| 64 | + const splashElapsed = useMinDuration(1500); |
| 65 | + |
54 | 66 | // Hydrate user preferences from the server once we're authenticated. The |
55 | 67 | // store seeds itself synchronously from localStorage on import so the first |
56 | 68 | // paint already uses cached prefs; this fetch reconciles with the server. |
@@ -81,60 +93,27 @@ export default function App() { |
81 | 93 | const isTerminalScreen = path === '/terminal' && !codeSessionId; |
82 | 94 | useChromeColor(isTerminalScreen ? 'terminal' : 'main'); |
83 | 95 |
|
84 | | - // Still checking auth |
85 | | - if (authenticated === null) { |
86 | | - return ( |
87 | | - <div |
88 | | - style={{ |
89 | | - display: 'flex', |
90 | | - alignItems: 'center', |
91 | | - justifyContent: 'center', |
92 | | - height: '100vh', |
93 | | - background: 'var(--bg)', |
94 | | - color: 'var(--text)', |
95 | | - }} |
96 | | - > |
97 | | - <div className="spinner" /> |
98 | | - </div> |
99 | | - ); |
| 96 | + // Still checking auth, OR auth done but we haven't yet hit the minimum |
| 97 | + // splash duration. The status text changes once auth resolves so the |
| 98 | + // splash feels like a real loading sequence instead of a fixed timer. |
| 99 | + if (authenticated === null || (authenticated && !splashElapsed)) { |
| 100 | + const status = authenticated ? 'Connected' : 'Establishing link'; |
| 101 | + return <Splash status={status} />; |
100 | 102 | } |
101 | 103 |
|
102 | 104 | if (!authenticated) { |
103 | 105 | // No-password mode: server is unreachable — show reconnecting UI instead of login |
104 | 106 | if (!passwordRequired) { |
105 | 107 | return ( |
106 | | - <div |
107 | | - style={{ |
108 | | - display: 'flex', |
109 | | - flexDirection: 'column', |
110 | | - alignItems: 'center', |
111 | | - justifyContent: 'center', |
112 | | - height: '100vh', |
113 | | - gap: '16px', |
114 | | - background: 'var(--bg)', |
115 | | - color: 'var(--text)', |
116 | | - }} |
117 | | - > |
118 | | - <div className="spinner" /> |
119 | | - <p style={{ color: 'var(--text-secondary)', fontSize: '14px' }}> |
120 | | - Reconnecting to server… |
121 | | - </p> |
122 | | - <button |
123 | | - onClick={() => window.location.reload()} |
124 | | - style={{ |
125 | | - marginTop: '8px', |
126 | | - padding: '8px 20px', |
127 | | - background: 'var(--accent)', |
128 | | - color: '#fff', |
129 | | - border: 'none', |
130 | | - borderRadius: '6px', |
131 | | - cursor: 'pointer', |
132 | | - fontSize: '13px', |
133 | | - }} |
134 | | - > |
135 | | - Retry |
136 | | - </button> |
137 | | - </div> |
| 108 | + <Splash |
| 109 | + size="md" |
| 110 | + status="Reconnecting to server" |
| 111 | + action={ |
| 112 | + <button onClick={() => window.location.reload()} className={splashStyles.action}> |
| 113 | + Retry |
| 114 | + </button> |
| 115 | + } |
| 116 | + /> |
138 | 117 | ); |
139 | 118 | } |
140 | 119 | return <LoginPage onLogin={login} loading={loading} />; |
|
0 commit comments