Telemetry #24
+70
-16
@@ -191,6 +191,14 @@
|
||||
directoryName: string;
|
||||
}
|
||||
|
||||
interface ErrorAutoHideState {
|
||||
timer?: ReturnType<typeof setTimeout>;
|
||||
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<Record<string, ReturnType<typeof setTimeout>>> = {};
|
||||
let errorAutoHideStates: Partial<Record<string, ErrorAutoHideState>> = {};
|
||||
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}
|
||||
|
||||
<!-- Status notices -->
|
||||
<!-- Global error toast -->
|
||||
{#if errorMessage}
|
||||
<section class="notice error" role="alert">
|
||||
<AlertCircle size={17} aria-hidden="true" />
|
||||
<span>{errorMessage}</span>
|
||||
</section>
|
||||
{#key errorMessage}
|
||||
<div class="error-toast-region" aria-live="assertive" aria-atomic="true">
|
||||
<section
|
||||
class="error-toast"
|
||||
role="alert"
|
||||
onmouseenter={() => pauseAutoHideError("errorMessage")}
|
||||
onmouseleave={() => resumeAutoHideError("errorMessage")}
|
||||
>
|
||||
<div class="error-toast-icon">
|
||||
<AlertCircle size={20} aria-hidden="true" />
|
||||
</div>
|
||||
<div class="error-toast-content">
|
||||
<strong>{appLanguage === "de" ? "Etwas ist schiefgelaufen" : "Something went wrong"}</strong>
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
<button
|
||||
class="error-toast-close"
|
||||
type="button"
|
||||
onclick={() => { errorMessage = ""; }}
|
||||
title={appLanguage === "de" ? "Schließen" : "Close"}
|
||||
aria-label={appLanguage === "de" ? "Fehlermeldung schließen" : "Dismiss error"}
|
||||
>
|
||||
<X size={15} aria-hidden="true" />
|
||||
</button>
|
||||
<div class="error-toast-timeout" aria-hidden="true"></div>
|
||||
</section>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<!-- Status notices -->
|
||||
{#if operation && operation !== "Opening repository" && !hasRepository}
|
||||
<section class="notice busy" aria-live="polite">
|
||||
<LoaderCircle class="spin" size={17} aria-hidden="true" />
|
||||
|
||||
+124
@@ -1222,6 +1222,113 @@
|
||||
color: #eadfff;
|
||||
}
|
||||
|
||||
/* --- Global error toast --- */
|
||||
|
||||
.error-toast-region {
|
||||
position: fixed;
|
||||
top: calc(var(--app-titlebar-height, 40px) + 14px);
|
||||
left: 50%;
|
||||
z-index: 90;
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.error-toast {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 11px;
|
||||
width: 100%;
|
||||
padding: 13px 13px 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(232,96,96,0.46);
|
||||
border-radius: 13px;
|
||||
color: var(--color-ink);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(232,96,96,0.18), rgba(100,108,255,0.08)),
|
||||
rgba(12,13,24,0.96);
|
||||
box-shadow: 0 18px 54px rgba(0,0,0,0.46), 0 0 0 1px rgba(255,255,255,0.035) inset;
|
||||
backdrop-filter: blur(18px);
|
||||
pointer-events: auto;
|
||||
animation: error-toast-in 180ms cubic-bezier(.2,.8,.2,1) both;
|
||||
}
|
||||
|
||||
.error-toast-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid rgba(232,96,96,0.28);
|
||||
border-radius: 10px;
|
||||
color: #ff9b9b;
|
||||
background: rgba(232,96,96,0.12);
|
||||
}
|
||||
|
||||
.error-toast-content {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.error-toast-content strong {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.error-toast-content span {
|
||||
color: var(--color-ink-muted);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.error-toast-close {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
padding: 0;
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
border-radius: 8px;
|
||||
color: var(--color-ink-dim);
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.error-toast-close:hover:not(:disabled) {
|
||||
border-color: rgba(255,255,255,0.2);
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.error-toast-timeout {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #ff7878, #ef9a9a);
|
||||
transform-origin: left;
|
||||
animation: error-toast-timeout 6s linear both;
|
||||
}
|
||||
|
||||
.error-toast:hover .error-toast-timeout {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes error-toast-in {
|
||||
from { opacity: 0; transform: translateY(-10px) scale(.985); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes error-toast-timeout {
|
||||
from { transform: scaleX(1); }
|
||||
to { transform: scaleX(0); }
|
||||
}
|
||||
|
||||
/* --- Update toast --- */
|
||||
|
||||
.update-toast {
|
||||
@@ -5306,6 +5413,23 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
|
||||
background: rgba(210, 55, 75, 0.08);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .error-toast {
|
||||
border-color: rgba(190,48,66,0.28);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(210,55,75,0.11), rgba(49,95,214,0.05)),
|
||||
rgba(255,255,255,0.96);
|
||||
box-shadow: 0 18px 48px rgba(28,44,74,0.18), 0 0 0 1px rgba(255,255,255,0.8) inset;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .error-toast-icon {
|
||||
color: #b33445;
|
||||
background: rgba(210,55,75,0.09);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .error-toast-content strong {
|
||||
color: #24314a;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .notice.busy {
|
||||
color: #315fd6;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user