From 2e9c3e7cbb5c0fe000d0d77f83b3ccca5983424d Mon Sep 17 00:00:00 2001 From: pythoninthegrass <4097471+pythoninthegrass@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:54:21 -0500 Subject: [PATCH 1/5] fix(startup): prevent FOUC flash between window.show() and CSS load The early window.show() introduced in commit 2f35425d created a gap where the Tauri window is visible but the body is hidden via x-cloak. During this gap, the element has no explicit background color, causing a white flash when the user sees the webview/document background instead of the themed one. Fix with three changes: 1. Add critical theme background colors to inline + + diff --git a/app/frontend/main.js b/app/frontend/main.js index ba1475c..124ba4c 100644 --- a/app/frontend/main.js +++ b/app/frontend/main.js @@ -120,7 +120,23 @@ function revealApp() { * the first visible paint. */ function applyInitialTheme() { - if (!settings.initialized) return; + // Hardcoded background colors matching theme definitions. + // Applied as inline style so the background is correct + // before the Tailwind CSS bundle computes theme variables. + const themeBackgrounds = { + 'metro-teal': '#1e1e1e', + 'neon-love': '#1f1731', + 'dark': '#09090b', + 'light': '#ffffff', + }; + + if (!settings.initialized) { + // Settings unavailable (e.g. browser mode) — apply system-preferred default + const fallback = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + document.documentElement.classList.add(fallback); + document.documentElement.style.backgroundColor = themeBackgrounds[fallback]; + return; + } const themePreset = settings.get('ui:themePreset', 'light'); const theme = settings.get('ui:theme', 'system'); @@ -131,11 +147,13 @@ function applyInitialTheme() { if (themePreset === 'metro-teal' || themePreset === 'neon-love') { document.documentElement.classList.add('dark'); document.documentElement.dataset.themePreset = themePreset; + document.documentElement.style.backgroundColor = themeBackgrounds[themePreset]; } else { const contentTheme = theme === 'system' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme; document.documentElement.classList.add(contentTheme); + document.documentElement.style.backgroundColor = themeBackgrounds[contentTheme]; } } @@ -225,13 +243,15 @@ async function initApp() { console.log('[perf] total initApp (sync):', Math.round(t.alpine - t.start), 'ms'); console.log('[main] Test dialog with: testDialog()'); - // Reveal the app after Alpine has initialized the DOM - // Note: We use setTimeout instead of requestAnimationFrame because - // RAF callbacks don't fire when the window is hidden (visible: false) - setTimeout(() => { + // Reveal the app after Alpine has initialized the DOM. + // requestAnimationFrame fires after style recalculation and before paint, + // ensuring Alpine-rendered DOM is fully styled before x-cloak is removed. + // The window is already visible (shown early to avoid WebKit IPC throttling), + // so rAF callbacks fire reliably. + requestAnimationFrame(() => { console.log('[perf] revealApp at:', Math.round(performance.now() - t.start), 'ms'); revealApp(); - }, 0); + }); } // Make settings service globally available diff --git a/app/frontend/tests/startup-fouc.spec.js b/app/frontend/tests/startup-fouc.spec.js index 6f6bd76..53e6d31 100644 --- a/app/frontend/tests/startup-fouc.spec.js +++ b/app/frontend/tests/startup-fouc.spec.js @@ -97,6 +97,42 @@ test.describe('Startup FOUC Prevention (task-298)', () => { expect(hadCloakBeforeJS).toBe(true); }); + test('inline styles include critical theme background colors for html element', async ({ page }) => { + let inlineStyleContent = ''; + + await page.route('/', async (route) => { + const response = await route.fetch(); + inlineStyleContent = await response.text(); + await route.fulfill({ response }); + }); + + const libraryState = createLibraryState(); + await setupLibraryMocks(page, libraryState); + + await page.route(/\/api\/lastfm\/settings/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + enabled: false, + username: null, + authenticated: false, + configured: false, + scrobble_threshold: 50, + }), + }); + }); + + await page.goto('/'); + + // Inline