diff --git a/src/main/index.ts b/src/main/index.ts index 3edb069..c215a5b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -35,6 +35,9 @@ let updateInfo: { version: string; progress: number } | null = null let lastAutoExportDate: string | null = null let lastImportedExportDate: string | null = null +/** Ignore blur-to-hide briefly after showing (shortcut keyup race). */ +let ignoreBlurUntil = 0 + const POPUP_WIDTH = 360 const POPUP_HEIGHT = 680 const DEFAULT_SHORTCUT = 'CommandOrControl+Shift+M' @@ -98,8 +101,9 @@ function createPopupWindow(): void { } }) - // Hide when clicking outside + // Hide when clicking outside (skip briefly after show — shortcut keyup can blur) popupWindow.on('blur', () => { + if (Date.now() < ignoreBlurUntil) return popupWindow?.hide() }) @@ -144,11 +148,17 @@ function togglePopup(): void { popupWindow.hide() } else { const { x, y } = getPopupPosition() - popupWindow.setPosition(x, y, false) + // Re-assert bounds every open — size/position can drift after Space/display changes. + popupWindow.setBounds({ x, y, width: POPUP_WIDTH, height: POPUP_HEIGHT }, false) popupWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) popupWindow.setAlwaysOnTop(true, 'pop-up-menu') + // Shortcut keyup often blurs the newly focused window; ignore hide briefly. + ignoreBlurUntil = Date.now() + 400 popupWindow.show() popupWindow.focus() + // Force a repaint after hide→show (transparent/vibrancy can leave a blank band). + popupWindow.webContents.invalidate() + popupWindow.webContents.send('popup-shown') } } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 32e4382..01f6d8d 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -181,6 +181,7 @@ export interface API { // Interview Mode getInterviewMode: () => Promise setInterviewMode: (enabled: boolean) => Promise<{ success: boolean; enabled: boolean }> + onPopupShown: (callback: () => void) => () => void } declare global { diff --git a/src/preload/index.ts b/src/preload/index.ts index 5152a92..7a773ca 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -86,7 +86,15 @@ const api = { // Interview Mode getInterviewMode: () => ipcRenderer.invoke('get-interview-mode'), - setInterviewMode: (enabled: boolean) => ipcRenderer.invoke('set-interview-mode', enabled) + setInterviewMode: (enabled: boolean) => ipcRenderer.invoke('set-interview-mode', enabled), + + onPopupShown: (callback: () => void) => { + const listener = (): void => callback() + ipcRenderer.on('popup-shown', listener) + return () => { + ipcRenderer.removeListener('popup-shown', listener) + } + } } // Expose APIs diff --git a/src/renderer/src/App.svelte b/src/renderer/src/App.svelte index 10e8c37..71c58df 100644 --- a/src/renderer/src/App.svelte +++ b/src/renderer/src/App.svelte @@ -49,6 +49,23 @@ // Interview mode state let interviewModeEnabled = $state(false) + function resetPopupLayout(): void { + // Transparent/vibrancy popups can open with a blank band until layout is nudged. + window.scrollTo(0, 0) + document.documentElement.scrollTop = 0 + document.body.scrollTop = 0 + const list = document.querySelector('.flex-1.overflow-y-auto') + if (list instanceof HTMLElement) list.scrollTop = 0 + // Force a style recalc / paint on the root shell. + const root = document.getElementById('app') + if (root) { + root.style.transform = 'translateZ(0)' + requestAnimationFrame(() => { + root.style.transform = '' + }) + } + } + onMount(() => { // Initialize async operations const init = async (): Promise => { @@ -72,15 +89,22 @@ // Refresh data when window becomes visible (e.g., popup shown after new day) const handleVisibilityChange = async (): Promise => { if (document.visibilityState === 'visible') { + resetPopupLayout() const set = $currentProblemSet await Promise.all([loadTodayReviews(), loadProblems(), loadStats(set), loadActivity()]) } } document.addEventListener('visibilitychange', handleVisibilityChange) + // Main process notifies after shortcut/tray show — reset scroll/layout in case of a blank band. + const unsubscribePopupShown = window.api.onPopupShown(() => { + resetPopupLayout() + }) + return () => { if (updateCheckInterval) clearInterval(updateCheckInterval) document.removeEventListener('visibilitychange', handleVisibilityChange) + unsubscribePopupShown() } }) diff --git a/src/renderer/src/components/HomeView.svelte b/src/renderer/src/components/HomeView.svelte index 8e08d83..c1d6faf 100644 --- a/src/renderer/src/components/HomeView.svelte +++ b/src/renderer/src/components/HomeView.svelte @@ -283,7 +283,7 @@ {:else if $completedInSession >= sessionQuota && $completedInSession > 0} -
+
✨ Session completed
{#if $todayReviewsCount > 0}
@@ -301,7 +301,7 @@
{:else} -
+
✨ All caught up! ({$completedInSession} reviewed today)