|
19 | 19 | <link rel="stylesheet" href="resources/custom.css"> |
20 | 20 | <script src="manifest/app.js"></script> |
21 | 21 | <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> |
22 | 34 | <script nonce="{{nonce}}"> |
23 | 35 | globalThis.isElectron = isElectron(); |
24 | 36 | if (isElectron) { |
|
56 | 68 | if (globalThis.isDarkMode) { |
57 | 69 | document.body.classList.add('dark-theme'); |
58 | 70 | } |
| 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)'; |
59 | 73 | </script> |
60 | 74 | <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"> |
63 | 79 | <div class="splash-header"> |
64 | | - <!-- <img src="manifest/backend.ai-text.svg" style="height:50px;padding:35px 20px;"> --> |
65 | 80 | <div class="logo"></div> |
66 | 81 | </div> |
67 | 82 | <div class="splash-information"> |
|
75 | 90 | <li class="copyright">Copyright © 2015-2026 Lablup Inc.</li> |
76 | 91 | </ul> |
77 | 92 | </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> |
83 | 101 | </div> |
84 | | - <div id="loading-message" class="loading-message">Loading components...</div> |
85 | 102 | </div> |
86 | 103 | </div> |
87 | 104 | <noscript> |
|
112 | 129 | }); |
113 | 130 | } |
114 | 131 | } |
| 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 | + })(); |
115 | 178 | </script> |
116 | 179 | </body> |
117 | 180 |
|
|
0 commit comments