diff --git a/electron/main.js b/electron/main.js index 7e87b888..554996ac 100644 --- a/electron/main.js +++ b/electron/main.js @@ -3125,6 +3125,13 @@ ipcMain.handle('set-window-buttons-visible', (_e, visible) => { if (process.platform !== 'darwin' || !mainWindow || mainWindow.isDestroyed()) return; try { mainWindow.setWindowButtonVisibility(!!visible); } catch (err) { console.warn('[main] setWindowButtonVisibility failed:', err.message); } }); +// The creation-time backgroundColor is boot-dark; the renderer re-points it at the live theme's page +// color so a live resize paints theme-matched filler, not a dark band behind a light UI. +ipcMain.handle('set-window-background', (_e, color) => { + if (!mainWindow || mainWindow.isDestroyed()) return; + if (typeof color !== 'string' || !/^#[0-9a-fA-F]{6}$/.test(color)) return; + try { mainWindow.setBackgroundColor(color); } catch (err) { console.warn('[main] setBackgroundColor failed:', err.message); } +}); // Phase 2 provenance: the renderer's About panel shows the commit this build // was cut from, so a screenshot is enough to identify the exact code shipped. ipcMain.handle('get-build-info', () => getBuildInfo()); diff --git a/electron/preload.js b/electron/preload.js index 65459774..3492415d 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -41,6 +41,8 @@ contextBridge.exposeInMainWorld('openswarm', { getAppVersion: () => ipcRenderer.invoke('get-app-version'), // Arc-style chrome: the mac traffic lights hide at rest; the dashboard's top-edge hover reveals them. setWindowButtonsVisible: (visible) => ipcRenderer.invoke('set-window-buttons-visible', visible), + // Native window bg tracks the theme so a live resize never paints the boot-dark color behind a light UI. + setWindowBackground: (color) => ipcRenderer.invoke('set-window-background', color), // Phase 2 provenance: { sha, shortSha, builtAt, channel } for the About panel. getBuildInfo: () => ipcRenderer.invoke('get-build-info'), diff --git a/frontend/src/shared/styles/ThemeContext.tsx b/frontend/src/shared/styles/ThemeContext.tsx index e6ecb367..6080b695 100644 --- a/frontend/src/shared/styles/ThemeContext.tsx +++ b/frontend/src/shared/styles/ThemeContext.tsx @@ -117,6 +117,9 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre const tokens = useMemo(() => withAccent(mode === 'dark' ? darkTokens : lightTokens, accent, mode), [mode, accent]); + // The native window was created boot-dark; keep it on the theme's page color so live-resize filler never flashes dark under a light UI. + useEffect(() => { void window.openswarm?.setWindowBackground?.(tokens.bg.page); }, [tokens.bg.page]); + // Stable identities: SettingsLoader's "apply settings.theme" effect lists setMode in its deps, so a setter that changed every render made that effect re-fire on each toggle and re-assert the OLD persisted theme until the debounced save caught up: live theme snapped back for ~900ms = the switch flicker. const toggleMode = useCallback(() => setModeState((m) => (m === 'light' ? 'dark' : 'light')), []); const setMode = useCallback((m: ThemeMode) => setModeState(m), []); diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index 9268e15b..290b3419 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -62,6 +62,7 @@ declare global { getWebviewPreloadPath: () => string; getAppVersion: () => Promise; setWindowButtonsVisible?: (visible: boolean) => Promise; + setWindowBackground?: (color: string) => Promise; getBuildInfo: () => Promise<{ sha: string; shortSha: string; builtAt: string | null; channel: string }>; getUpdateStatus: () => Promise<{ status: string; info: any; error: string | null }>; getCrashRecoveryInfo?: () => Promise<{ ts: number; parent_pid: number; uptime_ms: number } | null>;