fix: consistent mobile surface dimming and overlay scrim
Use a dark scrim for MobileOverlayPanel instead of a translucent background so it dims the rest of the screen like the sessions sheet. Rework MobileSurfaceShell to mirror that structure: the scrim is the fixed parent's background with a quick fade (keeping it on a compositing layer iOS Safari clips to the viewport), and the sheet is a flex child sized to leave the top safe area dimmed — dropping the old top shadow that pooled at the corners. Mount Files/Changes/Settings only while open so each recomputes its safe-area layout fresh, fixing intermittent top-inset dimming, and drop the dim behind the header overflow menu to avoid a scrim-to-scrim flicker on select.
This commit is contained in:
@@ -99,7 +99,7 @@ const MobileOverflowMenu: React.FC<{
|
||||
<div className="fixed inset-0 z-50" role="dialog" aria-modal="true" aria-label={t('mobile.menu.titleAria')}>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 cursor-default bg-[rgb(0_0_0_/_0.25)]"
|
||||
className="absolute inset-0 cursor-default"
|
||||
aria-label={t('mobile.surface.closeAria')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
@@ -286,47 +286,57 @@ const MobileShell: React.FC = () => {
|
||||
<MobileSessionsSheet open={sessionsSheetOpen} onOpenChange={setSessionsSheetOpen} />
|
||||
) : null}
|
||||
|
||||
<MobileSurfaceShell
|
||||
open={filesOpen}
|
||||
onClose={() => setFilesOpen(false)}
|
||||
ariaLabel={t('mobile.menu.files')}
|
||||
headerless
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<MobileFilesSurface onClose={() => setFilesOpen(false)} />
|
||||
</ErrorBoundary>
|
||||
</MobileSurfaceShell>
|
||||
{/* Mounted only while open (like the sessions sheet) so each surface
|
||||
computes its safe-area / fixed-position layout fresh on open. Keeping
|
||||
them always-mounted left a stale startup layout, which made the
|
||||
top-inset dimming appear only intermittently on iOS. */}
|
||||
{filesOpen ? (
|
||||
<MobileSurfaceShell
|
||||
open
|
||||
onClose={() => setFilesOpen(false)}
|
||||
ariaLabel={t('mobile.menu.files')}
|
||||
headerless
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<MobileFilesSurface onClose={() => setFilesOpen(false)} />
|
||||
</ErrorBoundary>
|
||||
</MobileSurfaceShell>
|
||||
) : null}
|
||||
|
||||
<MobileSurfaceShell
|
||||
open={changesOpen}
|
||||
onClose={closeChanges}
|
||||
ariaLabel={t('mobile.menu.changes')}
|
||||
headerless
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<MobileChangesSurface
|
||||
onClose={closeChanges}
|
||||
initialDiffPath={pendingChangesDiff?.path ?? null}
|
||||
initialDiffStaged={pendingChangesDiff?.staged === true}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</MobileSurfaceShell>
|
||||
{changesOpen ? (
|
||||
<MobileSurfaceShell
|
||||
open
|
||||
onClose={closeChanges}
|
||||
ariaLabel={t('mobile.menu.changes')}
|
||||
headerless
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<MobileChangesSurface
|
||||
onClose={closeChanges}
|
||||
initialDiffPath={pendingChangesDiff?.path ?? null}
|
||||
initialDiffStaged={pendingChangesDiff?.staged === true}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</MobileSurfaceShell>
|
||||
) : null}
|
||||
|
||||
<MobileSurfaceShell
|
||||
open={settingsOpen}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
ariaLabel={t('mobile.menu.settings')}
|
||||
headerless
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<SettingsView
|
||||
forceMobile
|
||||
isWindowed
|
||||
visiblePageSlugs={[...MOBILE_SETTINGS_PAGES]}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</MobileSurfaceShell>
|
||||
{settingsOpen ? (
|
||||
<MobileSurfaceShell
|
||||
open
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
ariaLabel={t('mobile.menu.settings')}
|
||||
headerless
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<SettingsView
|
||||
forceMobile
|
||||
isWindowed
|
||||
visiblePageSlugs={[...MOBILE_SETTINGS_PAGES]}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</MobileSurfaceShell>
|
||||
) : null}
|
||||
</div>
|
||||
</DedicatedMobileAppProvider>
|
||||
);
|
||||
|
||||
@@ -69,7 +69,7 @@ export const MobileSurfaceShell: React.FC<MobileSurfaceShellProps> = ({
|
||||
return () => window.clearTimeout(id);
|
||||
}
|
||||
setEntered(false);
|
||||
const id = window.setTimeout(() => setMounted(false), 220);
|
||||
const id = window.setTimeout(() => setMounted(false), 300);
|
||||
return () => window.clearTimeout(id);
|
||||
}, [open]);
|
||||
|
||||
@@ -180,31 +180,32 @@ export const MobileSurfaceShell: React.FC<MobileSurfaceShellProps> = ({
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 flex items-end',
|
||||
'bg-[rgb(0_0_0_/_0.45)]',
|
||||
'fixed inset-0 z-50 flex flex-col bg-[rgb(0_0_0_/_0.45)]',
|
||||
// The opacity transition keeps the scrim on its own compositing layer,
|
||||
// which iOS Safari clips to the viewport — without it, a static scrim
|
||||
// bleeds the dim into the bottom toolbar overscroll zone. Quick fade so
|
||||
// it still feels near-instant.
|
||||
'transition-opacity duration-200 ease-out',
|
||||
entered ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={ariaLabel}
|
||||
onClick={onClose}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 cursor-default"
|
||||
aria-label={t('mobile.surface.closeAria')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
{/* Sheet is a normal flex child — mirroring MobileOverlayPanel. */}
|
||||
<section
|
||||
ref={surfaceRef}
|
||||
className="relative flex h-[100dvh] w-full flex-col overflow-hidden rounded-t-[20px] border-t border-border/40 bg-background text-foreground shadow-[0_-12px_48px_rgb(0_0_0_/_0.35)] will-change-transform"
|
||||
className="mt-auto flex min-h-0 w-full flex-col overflow-hidden rounded-t-[20px] border-t border-border/40 bg-background text-foreground"
|
||||
tabIndex={-1}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{
|
||||
// Sized to leave the top safe area uncovered so the scrim dims it.
|
||||
height: 'calc(100% - var(--oc-safe-area-top, 0px))',
|
||||
transform: visualTransform,
|
||||
transition: isDraggingRef.current
|
||||
? 'none'
|
||||
: 'transform 220ms cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
paddingTop: 'var(--oc-safe-area-top, 0px)',
|
||||
: 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -70,7 +70,7 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex flex-col bg-background/70"
|
||||
className="fixed inset-0 z-50 flex flex-col bg-[rgb(0_0_0_/_0.45)]"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
onClick={onClose}
|
||||
|
||||
Reference in New Issue
Block a user