experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(page): rewrite loading indicator — separate lifecycle from visual effect

+52 -56
+4 -10
app/page/index.html
··· 47 47 transition: opacity 0.15s ease, box-shadow 0.4s ease, background-color 0.3s ease; 48 48 } 49 49 50 + /* Loading indicator: blue pulsing glow while page loads */ 50 51 @keyframes loading-glow { 51 52 0%, 100% { 52 53 box-shadow: 0 0 8px 2px rgba(100, 150, 255, 0.15), ··· 58 59 } 59 60 } 60 61 61 - @keyframes bg-fade-in { 62 - from { background-color: transparent; } 63 - to { background-color: rgba(255, 255, 255, 0.95); } 64 - } 65 - 66 62 webview.loading { 67 63 opacity: 1; 68 64 background-color: rgba(255, 255, 255, 0.95); 69 - animation: bg-fade-in 0.3s ease forwards, loading-glow 1.2s ease-in-out 0.3s infinite; 65 + animation: loading-glow 1.2s ease-in-out infinite; 70 66 overflow: visible; 71 67 } 72 68 73 - webview.ready { 69 + /* Default visible state (after loading completes) */ 70 + webview.loaded { 74 71 opacity: 1; 75 - box-shadow: none; 76 - background: initial; 77 - animation: none; 78 72 } 79 73 80 74 /*
+48 -46
app/page/page.js
··· 302 302 navbar.setUrl(targetUrl); 303 303 } 304 304 305 - initWebview(); 306 - webview.classList.add('loading'); 307 - show({ source: 'loading' }); 305 + // --- Loading lifecycle & visual effect --- 306 + // Simple state machine: 'loading' or 'loaded'. 307 + // Visual effect (CSS class) subscribes to state changes. 308 + 309 + const loadingLifecycle = { 310 + _state: 'idle', 311 + _listeners: [], 312 + 313 + get state() { return this._state; }, 314 + 315 + startLoading() { 316 + if (this._state === 'loading') return; 317 + this._state = 'loading'; 318 + this._notify(); 319 + DEBUG && console.log('[page] Loading started'); 320 + }, 321 + 322 + stopLoading() { 323 + if (this._state === 'loaded') return; 324 + this._state = 'loaded'; 325 + this._notify(); 326 + DEBUG && console.log('[page] Loading finished'); 327 + }, 328 + 329 + onChange(fn) { 330 + this._listeners.push(fn); 331 + }, 332 + 333 + _notify() { 334 + for (const fn of this._listeners) fn(this._state); 335 + } 336 + }; 308 337 309 - // Safety timeout: if loading glow is still active after 10s, force it off. 310 - // Catches edge cases where neither dom-ready nor did-stop-loading fires. 311 - setTimeout(() => { 312 - if (webview.classList.contains('loading')) { 338 + // Visual effect module — applies/removes CSS classes based on lifecycle state 339 + loadingLifecycle.onChange((state) => { 340 + if (state === 'loading') { 341 + webview.classList.add('loading'); 342 + webview.classList.remove('loaded'); 343 + } else if (state === 'loaded') { 313 344 webview.classList.remove('loading'); 314 - webview.classList.add('ready'); 315 - DEBUG && console.log('[page] Loading glow safety timeout — forced off after 10s'); 345 + webview.classList.add('loaded'); 316 346 } 317 - }, 10000); 347 + }); 348 + 349 + initWebview(); 350 + loadingLifecycle.startLoading(); 318 351 319 352 // --- Custom drag --- 320 353 // Two modes: ··· 824 857 function scheduleHide() { 825 858 if (hideTimer) clearTimeout(hideTimer); 826 859 hideTimer = setTimeout(() => { 827 - if (showSource !== 'hover' && showSource !== 'loading') return; 860 + if (showSource !== 'hover') return; 828 861 // If cursor is still in the navbar+trigger area, don't hide. 829 862 // This prevents the hide→contract→re-trigger→show bounce when cursor 830 863 // crosses the boundary between navbar and webview. ··· 1008 1041 console.error('[page] Load failed:', e.errorCode, e.errorDescription); 1009 1042 }); 1010 1043 1011 - // On dom-ready: clear loading glow immediately, then detect page background color. 1044 + // On dom-ready: detect page background color. 1012 1045 // Background detection prevents the white flash when loading pages in dark mode. 1013 1046 webview.addEventListener('dom-ready', async () => { 1014 - // End loading state IMMEDIATELY — don't wait for async bg detection. 1015 - // This MUST be synchronous at the top of dom-ready so the glow always 1016 - // clears promptly, even if executeJavaScript hangs or never resolves. 1017 - webview.classList.add('ready'); 1018 - webview.classList.remove('loading'); 1019 - if (showSource === 'loading') { 1020 - scheduleHide(); 1021 - } 1022 - DEBUG && console.log('[page] dom-ready: loading glow cleared'); 1023 - 1024 - // Detect background color (cosmetic — must not block loading state) 1047 + // Detect background color (cosmetic) 1025 1048 try { 1026 1049 const bgResult = await webview.executeJavaScript(` 1027 1050 (function() { ··· 1070 1093 } 1071 1094 }); 1072 1095 1073 - // Re-add loading glow on new navigations (but keep webview visible — don't reset opacity) 1074 - webview.addEventListener('did-start-loading', () => { 1075 - // Only show loading glow if we're not already displaying content 1076 - // (avoid flash on in-page navigations) 1077 - if (!webview.classList.contains('ready')) { 1078 - webview.classList.add('loading'); 1079 - } 1080 - DEBUG && console.log('[page] did-start-loading'); 1081 - }); 1082 - 1083 - // Fallback: ensure loading glow stops even if dom-ready doesn't fire 1084 - webview.addEventListener('did-stop-loading', () => { 1085 - if (webview.classList.contains('loading')) { 1086 - webview.classList.remove('loading'); 1087 - webview.classList.add('ready'); 1088 - if (showSource === 'loading') { 1089 - scheduleHide(); 1090 - } 1091 - DEBUG && console.log('[page] did-stop-loading: removed loading glow (fallback)'); 1092 - } 1093 - }); 1094 - 1095 - // --- OpenSearch discovery & page:loaded event --- 1096 + // --- OpenSearch discovery, loading lifecycle & page:loaded event --- 1096 1097 1097 1098 webview.addEventListener('did-finish-load', async () => { 1099 + loadingLifecycle.stopLoading(); 1098 1100 const pageUrl = webview.getURL(); 1099 1101 if (!pageUrl) return; 1100 1102 if (!pageUrl.startsWith('http://') && !pageUrl.startsWith('https://')) return;