fix: onboarding 401 redirect, glass card rendering bugs

- rpc-client: don't redirect to /login on 401 during onboarding flow,
  which caused session expired kicks on fresh installs
- style.css: add translateZ(0) + isolation:isolate to glass-card,
  glass-strong, path-option-card to fix Chromium compositor bug where
  backdrop-filter + animated fixed overlays cause black rectangles
- App.vue: pause background animations when tab hidden, force
  compositor layer rebuild on tab return to prevent stale renders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-03-26 20:06:09 +00:00
parent 6b857e59d0
commit e4b4519061
3 changed files with 54 additions and 2 deletions

View File

@ -168,7 +168,28 @@ const isReady = ref(false)
* - User has already seen the intro * - User has already seen the intro
* - User is on a direct route (refresh/bookmark) * - User is on a direct route (refresh/bookmark)
*/ */
// Fix Chromium backdrop-filter rendering bug: when tab loses/regains focus,
// the compositor fails to repaint backdrop-filter layers over animated
// fixed-position overlays (body::before/after with mix-blend-mode).
// On return: strip backdrop-filter via class, wait a frame, then restore.
function onVisibilityChange() {
if (document.hidden) {
document.documentElement.classList.add('tab-hidden')
} else {
// Step 1: kill all backdrop-filters (forces compositor to drop those layers)
document.documentElement.classList.add('no-backdrop')
document.documentElement.classList.remove('tab-hidden')
// Step 2: next frame, re-enable (compositor builds fresh layers)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.documentElement.classList.remove('no-backdrop')
})
})
}
}
onMounted(async () => { onMounted(async () => {
document.addEventListener('visibilitychange', onVisibilityChange)
window.addEventListener('keydown', onKeyDown, true) window.addEventListener('keydown', onKeyDown, true)
window.addEventListener('mousemove', onUserActivity) window.addEventListener('mousemove', onUserActivity)
window.addEventListener('mousedown', onUserActivity) window.addEventListener('mousedown', onUserActivity)
@ -196,6 +217,7 @@ onMounted(async () => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener('visibilitychange', onVisibilityChange)
window.removeEventListener('keydown', onKeyDown, true) window.removeEventListener('keydown', onKeyDown, true)
window.removeEventListener('mousemove', onUserActivity) window.removeEventListener('mousemove', onUserActivity)
window.removeEventListener('mousedown', onUserActivity) window.removeEventListener('mousedown', onUserActivity)

View File

@ -62,7 +62,9 @@ class RPCClient {
// Use a single shared timeout to prevent redirect storms when // Use a single shared timeout to prevent redirect storms when
// multiple parallel requests all get 401 at once // multiple parallel requests all get 401 at once
if (response.status === 401 && method !== 'auth.login') { if (response.status === 401 && method !== 'auth.login') {
if (!RPCClient._sessionExpiredRedirecting) { // Don't redirect during onboarding — those endpoints are unauthenticated
const isOnboarding = window.location.pathname.startsWith('/onboarding')
if (!isOnboarding && !RPCClient._sessionExpiredRedirecting) {
RPCClient._sessionExpiredRedirecting = true RPCClient._sessionExpiredRedirecting = true
setTimeout(() => { setTimeout(() => {
window.location.href = '/login' window.location.href = '/login'

View File

@ -115,6 +115,8 @@ input[type="radio"]:active + * {
-webkit-backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
border: 1px solid rgba(255, 255, 255, 0.18); border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
transform: translateZ(0);
isolation: isolate;
} }
.glass-strong { .glass-strong {
@ -123,6 +125,8 @@ input[type="radio"]:active + * {
-webkit-backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.18); border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
transform: translateZ(0);
isolation: isolate;
} }
.glass-card { .glass-card {
@ -134,6 +138,11 @@ input[type="radio"]:active + * {
border-radius: 1rem; border-radius: 1rem;
overflow-x: hidden; overflow-x: hidden;
overflow-y: visible; overflow-y: visible;
/* Fix Chromium compositor bug: backdrop-filter + fixed animated overlays
causes cards to render as black rectangles on scroll/tab-switch.
Own layer + isolation prevents stacking context confusion. */
transform: translateZ(0);
isolation: isolate;
} }
/* Mode switcher - sidebar toggle */ /* Mode switcher - sidebar toggle */
@ -767,6 +776,8 @@ input[type="radio"]:active + * {
cursor: pointer; cursor: pointer;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.2s ease, box-shadow 0.3s ease; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.2s ease, box-shadow 0.3s ease;
border: none; border: none;
transform: translateZ(0);
isolation: isolate;
} }
.path-option-card:active { .path-option-card:active {
@ -1175,6 +1186,23 @@ body::after {
animation-fill-mode: backwards; animation-fill-mode: backwards;
} }
/* Pause background animations when tab is hidden to prevent
Chromium compositor from corrupting backdrop-filter layers on tab return */
html.tab-hidden body::before,
html.tab-hidden body::after,
html.tab-hidden::before {
animation-play-state: paused !important;
will-change: auto !important;
}
/* Strip all backdrop-filters to force compositor layer rebuild on tab return */
html.no-backdrop *,
html.no-backdrop *::before,
html.no-backdrop *::after {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
/* Dashboard: full viewport width, no letterboxing, no body scroll */ /* Dashboard: full viewport width, no letterboxing, no body scroll */
body.dashboard-active { body.dashboard-active {
overflow: hidden; overflow: hidden;