Skip to content

Commit fa8d365

Browse files
committed
fix(FR-2518): use React-driven splash dismissal to eliminate blank screen
Root cause: splash was dismissed before header/sider rendered. 1. Replace backend-ai-connected listener with globalThis.__dismissSplash() 2. MainLayout: DismissSplashOnMount inside header Suspense boundary fires only after WebUIHeader actually renders 3. LoginView: dismiss in open() callback when login form becomes visible (only fires in logged-out state) 4. MutationObserver fallback increased to 3s (was 800ms) to avoid racing with React-driven dismiss on normal routes 5. React-driven dismiss cancels MutationObserver fallback timer 6. Inline critical CSS in <head> for splash visibility before external CSS loads 7. Body background set inline to prevent white flash Verified via Playwright: splash fades only after UI is visible in both logged-in (header+sider) and logged-out (login form) flows.
1 parent b1ab3c8 commit fa8d365

7 files changed

Lines changed: 322 additions & 337 deletions

File tree

index.html

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@
1919
<link rel="stylesheet" href="resources/custom.css">
2020
<script src="manifest/app.js"></script>
2121
<title>Backend.AI</title>
22+
<!-- Critical inline CSS: ensures splash is visible before external CSS loads -->
23+
<style>
24+
body { margin: 0; background-color: rgba(247, 247, 246, 1); }
25+
body.dark-theme { background-color: #191919; }
26+
#splash {
27+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
28+
z-index: 10000; display: flex; align-items: center; justify-content: center;
29+
background-color: rgba(247, 247, 246, 1); transition: opacity 0.3s ease-out;
30+
}
31+
body.dark-theme #splash { background-color: #191919; }
32+
.splash-hidden { opacity: 0; pointer-events: none; }
33+
</style>
2234
<script nonce="{{nonce}}">
2335
globalThis.isElectron = isElectron();
2436
if (isElectron) {
@@ -56,12 +68,15 @@
5668
if (globalThis.isDarkMode) {
5769
document.body.classList.add('dark-theme');
5870
}
71+
// Set body background immediately to prevent white flash before CSS loads
72+
document.body.style.backgroundColor = globalThis.isDarkMode ? '#191919' : 'rgba(247, 247, 246, 1)';
5973
</script>
6074
<div>
61-
<div id="react-root">
62-
<div class="splash">
75+
<div id="react-root"></div>
76+
<div id="splash" class="splash">
77+
<div class="splash-drag-area"></div>
78+
<div class="splash-card">
6379
<div class="splash-header">
64-
<!-- <img src="manifest/backend.ai-text.svg" style="height:50px;padding:35px 20px;"> -->
6580
<div class="logo"></div>
6681
</div>
6782
<div class="splash-information">
@@ -75,13 +90,15 @@
7590
<li class="copyright">Copyright &copy; 2015-2026 Lablup Inc.</li>
7691
</ul>
7792
</div>
78-
<div class="sk-folding-cube">
79-
<div class="sk-cube1 sk-cube"></div>
80-
<div class="sk-cube2 sk-cube"></div>
81-
<div class="sk-cube4 sk-cube"></div>
82-
<div class="sk-cube3 sk-cube"></div>
93+
<div class="splash-loading">
94+
<div class="sk-folding-cube">
95+
<div class="sk-cube1 sk-cube"></div>
96+
<div class="sk-cube2 sk-cube"></div>
97+
<div class="sk-cube3 sk-cube"></div>
98+
<div class="sk-cube4 sk-cube"></div>
99+
</div>
100+
<div id="loading-message" class="loading-message">Loading components...</div>
83101
</div>
84-
<div id="loading-message" class="loading-message">Loading components...</div>
85102
</div>
86103
</div>
87104
<noscript>
@@ -112,6 +129,52 @@
112129
});
113130
}
114131
}
132+
133+
// Splash fade-out: React-driven dismissal.
134+
// React calls window.__dismissSplash() when meaningful UI is ready
135+
// (sider/header for logged-in, login form for logged-out).
136+
// MutationObserver serves as a fallback for independent routes
137+
// (/verify-email, /change-password, etc.) that render content quickly.
138+
(function () {
139+
var splash = document.getElementById('splash');
140+
if (!splash) return;
141+
var dismissed = false;
142+
var observer = null;
143+
var fallbackTimer = null;
144+
function dismiss() {
145+
if (dismissed) return;
146+
dismissed = true;
147+
if (observer) observer.disconnect();
148+
if (fallbackTimer) clearTimeout(fallbackTimer);
149+
splash.classList.add('splash-hidden');
150+
splash.addEventListener('transitionend', function handler(e) {
151+
if (e.propertyName === 'opacity') {
152+
splash.removeEventListener('transitionend', handler);
153+
splash.remove();
154+
}
155+
});
156+
// Safety net: remove even if transitionend never fires (e.g. prefers-reduced-motion)
157+
setTimeout(function () { if (splash.parentNode) splash.remove(); }, 1000);
158+
}
159+
// Primary trigger: called by React when the visible UI is ready.
160+
// Cancels the MutationObserver fallback timer to prevent premature dismissal.
161+
window.__dismissSplash = dismiss;
162+
// Fallback: MutationObserver for routes that don't explicitly call __dismissSplash
163+
// (e.g. /verify-email, /change-password). Uses a longer delay to avoid racing
164+
// with React-driven dismissal on normal routes.
165+
var reactRoot = document.getElementById('react-root');
166+
if (reactRoot) {
167+
observer = new MutationObserver(function () {
168+
if (reactRoot.childNodes.length > 0) {
169+
observer.disconnect();
170+
fallbackTimer = setTimeout(dismiss, 3000);
171+
}
172+
});
173+
observer.observe(reactRoot, { childList: true });
174+
}
175+
// Ultimate safety fallback
176+
setTimeout(dismiss, 10000);
177+
})();
115178
</script>
116179
</body>
117180

react/src/components/LoadingCurtain.test.tsx

Lines changed: 0 additions & 102 deletions
This file was deleted.

react/src/components/LoadingCurtain.tsx

Lines changed: 0 additions & 87 deletions
This file was deleted.

0 commit comments

Comments
 (0)