mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
fix: render animated background via portal at z-index:-1 to stop implicit compositing
Previous isolation:isolate fix didn't work because: - position:fixed elements still see through isolation boundary - backdrop-filter samples all rendered content regardless of stacking context - The real issue: animated background at z-index:0 with will-change:transform forces Chrome to implicitly composite EVERY overlapping element Fix: render BackgroundRenderer via createPortal on document.body with z-index:-1, placing it below the root stacking context. This eliminates implicit compositing entirely. Also: - Remove backdrop-blur-xl from desktop header (was resampling bg 60fps) - Remove will-change:opacity from card ::after pseudo-element - Replace transition-all with transition-colors on header buttons
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Suspense, useMemo } from 'react';
|
import { Suspense, useMemo } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { brandingApi } from '@/api/branding';
|
import { brandingApi } from '@/api/branding';
|
||||||
import type { AnimationConfig, BackgroundType } from '@/components/ui/backgrounds/types';
|
import type { AnimationConfig, BackgroundType } from '@/components/ui/backgrounds/types';
|
||||||
@@ -83,20 +84,25 @@ export function BackgroundRenderer() {
|
|||||||
? reduceMobileSettings(effectiveConfig.settings)
|
? reduceMobileSettings(effectiveConfig.settings)
|
||||||
: effectiveConfig.settings;
|
: effectiveConfig.settings;
|
||||||
|
|
||||||
return (
|
// Render via portal on document.body with z-index: -1.
|
||||||
|
// This places the animated background BELOW the root stacking context,
|
||||||
|
// preventing Chrome's implicit compositing from promoting every
|
||||||
|
// overlapping element to its own GPU layer (the root cause of flickering).
|
||||||
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none fixed inset-0 z-0"
|
className="pointer-events-none fixed inset-0"
|
||||||
style={{
|
style={{
|
||||||
|
zIndex: -1,
|
||||||
opacity: effectiveConfig.opacity,
|
opacity: effectiveConfig.opacity,
|
||||||
filter: effectiveConfig.blur > 0 ? `blur(${effectiveConfig.blur}px)` : undefined,
|
filter: effectiveConfig.blur > 0 ? `blur(${effectiveConfig.blur}px)` : undefined,
|
||||||
contain: 'strict',
|
contain: 'strict',
|
||||||
willChange: 'transform',
|
backfaceVisibility: 'hidden',
|
||||||
transform: 'translateZ(0)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Component settings={settings} />
|
<Component settings={settings} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -280,162 +280,158 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
{/* Animated background — on its own GPU layer, below everything */}
|
{/* Animated background renders via portal on document.body at z-index: -1 */}
|
||||||
<BackgroundRenderer />
|
<BackgroundRenderer />
|
||||||
|
|
||||||
{/* Content layer — isolated from background to prevent backdrop-filter
|
{/* Global components */}
|
||||||
from resampling the animated background every frame (Chromium bug) */}
|
<WebSocketNotifications />
|
||||||
<div className="relative z-10" style={{ isolation: 'isolate' }}>
|
<CampaignBonusNotifier />
|
||||||
{/* Global components */}
|
<SuccessNotificationModal />
|
||||||
<WebSocketNotifications />
|
|
||||||
<CampaignBonusNotifier />
|
|
||||||
<SuccessNotificationModal />
|
|
||||||
|
|
||||||
{/* Desktop Header */}
|
{/* Desktop Header */}
|
||||||
<header className="fixed left-0 right-0 top-0 z-50 hidden border-b border-dark-800/50 bg-dark-950/80 backdrop-blur-xl lg:block">
|
<header className="fixed left-0 right-0 top-0 z-50 hidden border-b border-dark-800/50 bg-dark-950/95 lg:block">
|
||||||
<div className="mx-auto grid h-14 max-w-6xl grid-cols-[auto_1fr_auto] items-center gap-4 px-6">
|
<div className="mx-auto grid h-14 max-w-6xl grid-cols-[auto_1fr_auto] items-center gap-4 px-6">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link to="/" className="flex items-center gap-2.5" onClick={handleNavClick}>
|
<Link to="/" className="flex items-center gap-2.5" onClick={handleNavClick}>
|
||||||
<div className="relative flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg bg-dark-800">
|
<div className="relative flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg bg-dark-800">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
|
||||||
'absolute text-sm font-bold text-accent-400 transition-opacity duration-200',
|
|
||||||
hasCustomLogo && isLogoPreloaded() ? 'opacity-0' : 'opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{logoLetter}
|
|
||||||
</span>
|
|
||||||
{hasCustomLogo && logoUrl && (
|
|
||||||
<img
|
|
||||||
src={logoUrl}
|
|
||||||
alt={appName || 'Logo'}
|
|
||||||
className={cn(
|
|
||||||
'absolute h-full w-full object-contain transition-opacity duration-200',
|
|
||||||
isLogoPreloaded() ? 'opacity-100' : 'opacity-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-base font-semibold text-dark-100">{appName}</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Center Navigation */}
|
|
||||||
<nav className="flex items-center justify-center gap-1">
|
|
||||||
{desktopNavItems.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
|
||||||
onClick={handleNavClick}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
||||||
isActive(item.path)
|
|
||||||
? 'bg-dark-800 text-dark-50'
|
|
||||||
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<item.icon className="h-4 w-4" />
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
{referralEnabled && (
|
|
||||||
<Link
|
|
||||||
to="/referral"
|
|
||||||
onClick={handleNavClick}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
||||||
isActive('/referral')
|
|
||||||
? 'bg-dark-800 text-dark-50'
|
|
||||||
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<UsersIcon className="h-4 w-4" />
|
|
||||||
<span>{t('nav.referral')}</span>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{isAdmin && (
|
|
||||||
<>
|
|
||||||
{/* Separator before admin */}
|
|
||||||
<div className="mx-2 h-5 w-px bg-dark-700" />
|
|
||||||
<Link
|
|
||||||
to="/admin"
|
|
||||||
onClick={handleNavClick}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
||||||
location.pathname.startsWith('/admin')
|
|
||||||
? 'bg-warning-500/10 text-warning-400'
|
|
||||||
: 'text-warning-500/70 hover:bg-warning-500/10 hover:text-warning-400',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ShieldIcon className="h-4 w-4" />
|
|
||||||
<span>{t('admin.nav.title')}</span>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Right side actions */}
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
haptic.impact('light');
|
|
||||||
toggleTheme();
|
|
||||||
}}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-xl border border-dark-700/50 bg-dark-800/50 p-2 text-dark-400 transition-all duration-200 hover:bg-dark-700 hover:text-accent-400',
|
'absolute text-sm font-bold text-accent-400 transition-opacity duration-200',
|
||||||
!canToggleTheme && 'pointer-events-none invisible',
|
hasCustomLogo && isLogoPreloaded() ? 'opacity-0' : 'opacity-100',
|
||||||
)}
|
)}
|
||||||
title={isDark ? t('theme.light') || 'Light mode' : t('theme.dark') || 'Dark mode'}
|
|
||||||
>
|
>
|
||||||
{isDark ? <MoonIcon className="h-5 w-5" /> : <SunIcon className="h-5 w-5" />}
|
{logoLetter}
|
||||||
</button>
|
</span>
|
||||||
<TicketNotificationBell isAdmin={location.pathname.startsWith('/admin')} />
|
{hasCustomLogo && logoUrl && (
|
||||||
<LanguageSwitcher />
|
<img
|
||||||
<button
|
src={logoUrl}
|
||||||
onClick={() => {
|
alt={appName || 'Logo'}
|
||||||
haptic.impact('light');
|
className={cn(
|
||||||
logout();
|
'absolute h-full w-full object-contain transition-opacity duration-200',
|
||||||
}}
|
isLogoPreloaded() ? 'opacity-100' : 'opacity-0',
|
||||||
className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-2 text-dark-400 transition-all duration-200 hover:bg-dark-700 hover:text-accent-400"
|
)}
|
||||||
title={t('nav.logout')}
|
/>
|
||||||
>
|
)}
|
||||||
<LogoutIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-base font-semibold text-dark-100">{appName}</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Center Navigation */}
|
||||||
|
<nav className="flex items-center justify-center gap-1">
|
||||||
|
{desktopNavItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
onClick={handleNavClick}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
||||||
|
isActive(item.path)
|
||||||
|
? 'bg-dark-800 text-dark-50'
|
||||||
|
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon className="h-4 w-4" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
{referralEnabled && (
|
||||||
|
<Link
|
||||||
|
to="/referral"
|
||||||
|
onClick={handleNavClick}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
||||||
|
isActive('/referral')
|
||||||
|
? 'bg-dark-800 text-dark-50'
|
||||||
|
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<UsersIcon className="h-4 w-4" />
|
||||||
|
<span>{t('nav.referral')}</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{isAdmin && (
|
||||||
|
<>
|
||||||
|
{/* Separator before admin */}
|
||||||
|
<div className="mx-2 h-5 w-px bg-dark-700" />
|
||||||
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
onClick={handleNavClick}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
||||||
|
location.pathname.startsWith('/admin')
|
||||||
|
? 'bg-warning-500/10 text-warning-400'
|
||||||
|
: 'text-warning-500/70 hover:bg-warning-500/10 hover:text-warning-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ShieldIcon className="h-4 w-4" />
|
||||||
|
<span>{t('admin.nav.title')}</span>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Right side actions */}
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
haptic.impact('light');
|
||||||
|
toggleTheme();
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'rounded-xl border border-dark-700/50 bg-dark-800/50 p-2 text-dark-400 transition-colors duration-200 hover:bg-dark-700 hover:text-accent-400',
|
||||||
|
!canToggleTheme && 'pointer-events-none invisible',
|
||||||
|
)}
|
||||||
|
title={isDark ? t('theme.light') || 'Light mode' : t('theme.dark') || 'Dark mode'}
|
||||||
|
>
|
||||||
|
{isDark ? <MoonIcon className="h-5 w-5" /> : <SunIcon className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
<TicketNotificationBell isAdmin={location.pathname.startsWith('/admin')} />
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
haptic.impact('light');
|
||||||
|
logout();
|
||||||
|
}}
|
||||||
|
className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-2 text-dark-400 transition-colors duration-200 hover:bg-dark-700 hover:text-accent-400"
|
||||||
|
title={t('nav.logout')}
|
||||||
|
>
|
||||||
|
<LogoutIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{/* Mobile Header */}
|
{/* Mobile Header */}
|
||||||
<AppHeader
|
<AppHeader
|
||||||
mobileMenuOpen={mobileMenuOpen}
|
mobileMenuOpen={mobileMenuOpen}
|
||||||
setMobileMenuOpen={setMobileMenuOpen}
|
setMobileMenuOpen={setMobileMenuOpen}
|
||||||
onCommandPaletteOpen={() => {}}
|
onCommandPaletteOpen={() => {}}
|
||||||
headerHeight={headerHeight}
|
headerHeight={headerHeight}
|
||||||
isFullscreen={isMobileFullscreen}
|
isFullscreen={isMobileFullscreen}
|
||||||
safeAreaInset={safeAreaInset}
|
safeAreaInset={safeAreaInset}
|
||||||
contentSafeAreaInset={contentSafeAreaInset}
|
contentSafeAreaInset={contentSafeAreaInset}
|
||||||
telegramPlatform={platform}
|
telegramPlatform={platform}
|
||||||
wheelEnabled={wheelEnabled}
|
wheelEnabled={wheelEnabled}
|
||||||
referralEnabled={referralEnabled}
|
referralEnabled={referralEnabled}
|
||||||
hasContests={hasContests}
|
hasContests={hasContests}
|
||||||
hasPolls={hasPolls}
|
hasPolls={hasPolls}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Desktop spacer */}
|
{/* Desktop spacer */}
|
||||||
<div className="hidden h-14 lg:block" />
|
<div className="hidden h-14 lg:block" />
|
||||||
|
|
||||||
{/* Mobile spacer */}
|
{/* Mobile spacer */}
|
||||||
<div className="lg:hidden" style={{ height: headerHeight }} />
|
<div className="lg:hidden" style={{ height: headerHeight }} />
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<main className="mx-auto max-w-6xl px-4 py-6 pb-28 lg:px-6 lg:pb-8">{children}</main>
|
<main className="mx-auto max-w-6xl px-4 py-6 pb-28 lg:px-6 lg:pb-8">{children}</main>
|
||||||
|
|
||||||
{/* Mobile Bottom Navigation */}
|
{/* Mobile Bottom Navigation */}
|
||||||
<MobileBottomNav
|
<MobileBottomNav
|
||||||
isKeyboardOpen={isKeyboardOpen}
|
isKeyboardOpen={isKeyboardOpen}
|
||||||
referralEnabled={referralEnabled}
|
referralEnabled={referralEnabled}
|
||||||
wheelEnabled={wheelEnabled}
|
wheelEnabled={wheelEnabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,7 +400,6 @@ img.twemoji {
|
|||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
will-change: opacity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bento-card-hover:hover::after {
|
.bento-card-hover:hover::after {
|
||||||
|
|||||||
Reference in New Issue
Block a user