From 647f2b2076b1f8ae134cece641fbbfe4923d6ddc Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Tue, 21 Jul 2026 23:45:05 +0200 Subject: [PATCH] feat(ui): implement global error toast and state management Introduces a persistent, styled error toast notification system for displaying critical application errors. This required refactoring the local error handling logic in App.svelte to manage complex state objects instead of simple timers, allowing for better control over visibility and interaction (e.g., pausing on hover). - Adds global CSS styling and animations for the visible error toast component - Updates error auto-hide mechanism to use structured state management - Implements interactive features like pause/resume timing on mouse events --- src/App.svelte | 86 +++++++++++++++++++++++++++------- src/app.css | 124 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 16 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index 8b49f8a..d87a24e 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -191,6 +191,14 @@ directoryName: string; } + interface ErrorAutoHideState { + timer?: ReturnType; + message: string; + remaining: number; + startedAt: number; + clearIfCurrent: (message: string) => void; + } + const OPEN_REPOS_KEY = "gitlite.openRepos.v1"; const RECENT_REPOS_KEY = "gitlite.recentRepos.v1"; const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1"; @@ -364,7 +372,7 @@ let updateCheckInFlight = false; let updateDownloadTotal = 0; let updateDownloadedBytes = 0; - let errorAutoHideTimers: Partial>> = {}; + let errorAutoHideStates: Partial> = {}; let commitPanelHeight = loadCommitPanelHeight(); let resizingCommitPanel = false; let resizeStartY = 0; @@ -467,8 +475,8 @@ if (backgroundFetchTimer) clearInterval(backgroundFetchTimer); if (commitAiPollTimer) clearInterval(commitAiPollTimer); if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer); - Object.values(errorAutoHideTimers).forEach((timer) => { - if (timer) clearTimeout(timer); + Object.values(errorAutoHideStates).forEach((state) => { + if (state?.timer) clearTimeout(state.timer); }); if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId); }); @@ -1110,18 +1118,39 @@ message: string, clearIfCurrent: (message: string) => void, ) { - const existing = errorAutoHideTimers[key]; - if (existing) { - clearTimeout(existing); - delete errorAutoHideTimers[key]; + const existing = errorAutoHideStates[key]; + if (existing?.timer) { + clearTimeout(existing.timer); } + delete errorAutoHideStates[key]; if (!message) return; - errorAutoHideTimers[key] = setTimeout(() => { - clearIfCurrent(message); - delete errorAutoHideTimers[key]; - }, ERROR_AUTO_HIDE_MS); + errorAutoHideStates[key] = { + message, + remaining: ERROR_AUTO_HIDE_MS, + startedAt: Date.now(), + clearIfCurrent, + }; + resumeAutoHideError(key); + } + + function pauseAutoHideError(key: string) { + const state = errorAutoHideStates[key]; + if (!state?.timer) return; + clearTimeout(state.timer); + state.timer = undefined; + state.remaining = Math.max(0, state.remaining - (Date.now() - state.startedAt)); + } + + function resumeAutoHideError(key: string) { + const state = errorAutoHideStates[key]; + if (!state || state.timer) return; + state.startedAt = Date.now(); + state.timer = setTimeout(() => { + state.clearIfCurrent(state.message); + delete errorAutoHideStates[key]; + }, state.remaining); } function repoKey(path: string): string { @@ -3798,14 +3827,39 @@ {/if} {/if} - + {#if errorMessage} - + {#key errorMessage} +
+ +
+ {/key} {/if} + {#if operation && operation !== "Opening repository" && !hasRepository}