fix: always-mounted modal — GPU keeps backdrop-filter composited, no blur delay

This commit is contained in:
Usman Baig
2026-03-25 23:48:57 +01:00
parent b18199aa48
commit 67334f1fd6

View File

@@ -373,108 +373,104 @@ export default function UnifiedSettingsModal() {
}, [guardedAction, closeSettings])
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop — solid scrim, no blur (modal handles its own glass) */}
<motion.div
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="fixed inset-0 z-[60] bg-black/50"
onClick={handleBackdropClick}
/>
<>
{/* Backdrop — always mounted, visibility toggled via CSS */}
<div
className={`fixed inset-0 z-[60] bg-black/50 transition-opacity duration-150 ${
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
onClick={handleBackdropClick}
/>
{/* Modal */}
<motion.div
initial={{ scale: 0.97, y: 8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.97, y: 8 }}
transition={{ type: 'spring', bounce: 0.15, duration: 0.35 }}
className="fixed inset-0 z-[61] flex items-center justify-center p-4 pointer-events-none"
>
<div
className="pointer-events-auto w-full max-w-3xl h-[85vh] bg-neutral-900/65 backdrop-blur-3xl backdrop-saturate-150 supports-[backdrop-filter]:bg-neutral-900/60 border border-white/[0.08] rounded-2xl shadow-xl shadow-black/20 flex flex-col overflow-hidden"
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="shrink-0 px-6 pt-5 pb-4 border-b border-white/[0.06]">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white">Settings</h2>
<button
onClick={handleClose}
className="p-1.5 rounded-lg text-neutral-500 hover:text-white hover:bg-neutral-800 transition-colors"
>
<X weight="bold" className="w-4 h-4" />
</button>
</div>
{/* Context Switcher */}
<ContextSwitcher
active={context}
onChange={handleContextChange}
activeSiteDomain={sites.find(s => s.id === activeSiteId)?.domain ?? null}
/>
{/* Tabs */}
<div className="mt-4">
<TabBar tabs={tabs} activeTab={activeTab} onChange={handleTabChange} />
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto overflow-x-hidden">
<AnimatePresence mode="wait">
<motion.div
key={`${context}-${activeTab}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.12 }}
className="p-6"
>
<TabContent context={context} activeTab={activeTab} siteId={activeSiteId} onDirtyChange={handleDirtyChange} onRegisterSave={handleRegisterSave} />
</motion.div>
</AnimatePresence>
</div>
{/* Save bar — fixed at modal bottom, outside scroll */}
<AnimatePresence>
{isDirtyVisible && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.15 }}
className="shrink-0 overflow-hidden"
>
<div className={`px-6 py-3 border-t flex items-center justify-between ${
hasPendingAction
? 'bg-red-900/10 border-red-900/30'
: 'bg-neutral-950/80 border-white/[0.06]'
}`}>
<span className="text-sm font-medium text-neutral-400">
{hasPendingAction ? 'Save or discard to continue' : 'Unsaved changes'}
</span>
<div className="flex items-center gap-2">
{hasPendingAction && (
<Button onClick={handleDiscard} variant="secondary" className="text-sm">
Discard
</Button>
)}
<Button onClick={handleSaveFromBar} variant="primary" disabled={saving} className="text-sm">
{saving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Modal — always mounted, blur always composited by GPU */}
<div
className={`fixed inset-0 z-[61] flex items-center justify-center p-4 transition-all duration-200 ease-out ${
isOpen
? 'opacity-100 scale-100 translate-y-0 pointer-events-auto'
: 'opacity-0 scale-[0.97] translate-y-2 pointer-events-none'
}`}
>
<div
className="w-full max-w-3xl h-[85vh] bg-neutral-900/65 backdrop-blur-3xl backdrop-saturate-150 supports-[backdrop-filter]:bg-neutral-900/60 border border-white/[0.08] rounded-2xl shadow-xl shadow-black/20 flex flex-col overflow-hidden"
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="shrink-0 px-6 pt-5 pb-4 border-b border-white/[0.06]">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white">Settings</h2>
<button
onClick={handleClose}
className="p-1.5 rounded-lg text-neutral-500 hover:text-white hover:bg-neutral-800 transition-colors"
>
<X weight="bold" className="w-4 h-4" />
</button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
{/* Context Switcher */}
<ContextSwitcher
active={context}
onChange={handleContextChange}
activeSiteDomain={sites.find(s => s.id === activeSiteId)?.domain ?? null}
/>
{/* Tabs */}
<div className="mt-4">
<TabBar tabs={tabs} activeTab={activeTab} onChange={handleTabChange} />
</div>
</div>
{/* Content — only render tab content when open to avoid unnecessary SWR calls */}
<div className="flex-1 overflow-y-auto overflow-x-hidden">
{isOpen && (
<AnimatePresence mode="wait">
<motion.div
key={`${context}-${activeTab}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.12 }}
className="p-6"
>
<TabContent context={context} activeTab={activeTab} siteId={activeSiteId} onDirtyChange={handleDirtyChange} onRegisterSave={handleRegisterSave} />
</motion.div>
</AnimatePresence>
)}
</div>
{/* Save bar */}
<AnimatePresence>
{isDirtyVisible && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.15 }}
className="shrink-0 overflow-hidden"
>
<div className={`px-6 py-3 border-t flex items-center justify-between ${
hasPendingAction
? 'bg-red-900/10 border-red-900/30'
: 'bg-neutral-950/80 border-white/[0.06]'
}`}>
<span className="text-sm font-medium text-neutral-400">
{hasPendingAction ? 'Save or discard to continue' : 'Unsaved changes'}
</span>
<div className="flex items-center gap-2">
{hasPendingAction && (
<Button onClick={handleDiscard} variant="secondary" className="text-sm">
Discard
</Button>
)}
<Button onClick={handleSaveFromBar} variant="primary" disabled={saving} className="text-sm">
{saving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</>
)
}