fix(ios): respect safe-area insets so chrome clears the notch/home bar

Content was drawing under the iOS status bar and home indicator because
the app uses viewport-fit=cover + black-translucent but the fixed/sticky
chrome didn't reserve the safe areas.

- bottom nav (MyLayout): move the 50px row into an inner box and let the
  bar extend by env(safe-area-inset-bottom) so icons sit above the home
  indicator (previously the fixed height clipped the inset padding).
- top headers gain env(safe-area-inset-top): HomePageTopBar, ExploreTopBar,
  SearchTopBar, CartHeader, ChatHeader, and wrapper-padded HeaderWithSupport
  / ProfilePagesHeader (so their absolutely-positioned buttons shift too).
- chat input pads bottom by the inset; chat scroll area height accounts for
  the taller header/input.
- _index.tsx no longer overrides the viewport meta (it was dropping
  viewport-fit=cover on the home page); the global one in root.tsx wins.

env(safe-area-inset-*) is 0 on non-notch devices, so desktop/Android render
identically — only iOS gains the padding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Arda Samadi 2026-06-18 13:42:51 +03:30
parent 66b14cfa74
commit b4d5f8d8db
11 changed files with 72 additions and 64 deletions

View File

@ -39,23 +39,25 @@ const HeaderWithSupport = memo(function HeaderWithSupport({
return (
<>
<div className="flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
<ArrowRight
onClick={onBack}
className="absolute top-5 right-5 cursor-pointer"
/>
<p className="text-B16 font-bold ">{title}</p>
{!hideSupportButton && (
<div
className="absolute top-4 h-8 w-8 left-5 rounded-full bg-WHITE shadow-xl flex items-center justify-center cursor-pointer"
onClick={handleSupportClick}
role="button"
tabIndex={0}
onKeyDown={handleKeyDown}
>
<Headset size={20} />
</div>
)}
<div className="sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
<div className="relative flex flex-row h-[65px] bg-WHITE w-full items-center justify-center border-b border-inner-border">
<ArrowRight
onClick={onBack}
className="absolute top-5 right-5 cursor-pointer"
/>
<p className="text-B16 font-bold ">{title}</p>
{!hideSupportButton && (
<div
className="absolute top-4 h-8 w-8 left-5 rounded-full bg-WHITE shadow-xl flex items-center justify-center cursor-pointer"
onClick={handleSupportClick}
role="button"
tabIndex={0}
onKeyDown={handleKeyDown}
>
<Headset size={20} />
</div>
)}
</div>
</div>
<CallDialog

View File

@ -65,7 +65,7 @@ const MyLayout = React.memo(({ children }: { children: React.ReactNode }) => {
}`}
>
<div
className={`pb-[calc(50px+env(safe-area-inset-bottom))] ${
className={`pb-[calc(50px_+_env(safe-area-inset-bottom))] ${
expandOnDesktop ? "lg:pb-0" : ""
}`}
>
@ -364,34 +364,36 @@ const MobileBottomNavigation = ({
return (
<>
<div
className={`h-[50px] z-20 bg-WHITE flex gap-[48px] px-[20px] fixed bottom-0 border-t w-full max-w-md mx-auto items-center justify-center pb-[env(safe-area-inset-bottom)] lg:left-1/2 lg:transform lg:-translate-x-1/2 ${
className={`z-20 bg-WHITE fixed bottom-0 border-t w-full max-w-md mx-auto pb-[env(safe-area-inset-bottom)] lg:left-1/2 lg:transform lg:-translate-x-1/2 ${
hideOnDesktop ? "lg:hidden" : ""
}`}
>
{navItems.map((item) => (
<Link
key={item.link}
to={item.link}
prefetch="intent"
className="relative"
>
{typeof item.icon === "string" ? (
<img
src={item.icon}
className="h-6 w-6"
alt={item.link}
loading="eager"
/>
) : (
item.icon
)}
{item.badge ? (
<span className="absolute -top-1.5 -right-1.5 min-w-[16px] h-4 px-1 rounded-full bg-RED text-white text-[10px] font-bold flex items-center justify-center leading-none">
{item.badge > 99 ? "99+" : item.badge}
</span>
) : null}
</Link>
))}
<div className="h-[50px] flex gap-[48px] px-[20px] items-center justify-center">
{navItems.map((item) => (
<Link
key={item.link}
to={item.link}
prefetch="intent"
className="relative"
>
{typeof item.icon === "string" ? (
<img
src={item.icon}
className="h-6 w-6"
alt={item.link}
loading="eager"
/>
) : (
item.icon
)}
{item.badge ? (
<span className="absolute -top-1.5 -right-1.5 min-w-[16px] h-4 px-1 rounded-full bg-RED text-white text-[10px] font-bold flex items-center justify-center leading-none">
{item.badge > 99 ? "99+" : item.badge}
</span>
) : null}
</Link>
))}
</div>
</div>
</>
);

View File

@ -20,17 +20,19 @@ const ProfilePagesHeader = memo(function ProfilePagesHeader({
}, [navigate]);
return (
<div className="lg:hidden flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
{!hideBackButton && (
<ArrowRight
onClick={handleBack}
className="absolute top-5 right-5 cursor-pointer"
/>
)}
<p className="text-B16 font-bold">{title}</p>
{rightContent && (
<div className="absolute top-4 left-4">{rightContent}</div>
)}
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)]">
<div className="relative flex flex-row h-[65px] bg-WHITE w-full items-center justify-center border-b border-inner-border">
{!hideBackButton && (
<ArrowRight
onClick={handleBack}
className="absolute top-5 right-5 cursor-pointer"
/>
)}
<p className="text-B16 font-bold">{title}</p>
{rightContent && (
<div className="absolute top-4 left-4">{rightContent}</div>
)}
</div>
</div>
);
});

View File

@ -42,7 +42,7 @@ const SearchTopBar: React.FC<SearchTopBarProps> = ({
};
return (
<div className="flex flex-col gap-4 py-4 px-4 w-full sticky top-0 z-30 bg-WHITE">
<div className="flex flex-col gap-4 pb-4 pt-[calc(1rem_+_env(safe-area-inset-top))] px-4 w-full sticky top-0 z-30 bg-WHITE">
<div className="flex items-center justify-center gap-2 rounded-[10px] w-full relative">
<AppInput
inputIcon={SearchIcon}

View File

@ -14,7 +14,7 @@ export const CartHeader = ({ cart, step, setStep }: CartHeaderProps) => {
const navigate = useNavigate();
return (
<div
className={`flex flex-col gap-3 pt-4 sticky top-0 z-30 bg-WHITE w-full items-center justify-center ${step !== 0 ? "pb-4" : ""}`}
className={`flex flex-col gap-3 pt-[calc(1rem_+_env(safe-area-inset-top))] sticky top-0 z-30 bg-WHITE w-full items-center justify-center ${step !== 0 ? "pb-4" : ""}`}
>
<p className={"text-B16 font-bold px-4"}>
{step === 0
@ -29,7 +29,7 @@ export const CartHeader = ({ cart, step, setStep }: CartHeaderProps) => {
if (step > 0) setStep((p) => p - 1);
else safeGoBack(navigate);
}}
className="absolute top-4 right-4"
className="absolute top-[calc(1rem_+_env(safe-area-inset-top))] right-4"
/>
{cart && cart.items && cart.items.length > 0 && (
<>

View File

@ -41,7 +41,7 @@ const ExploreTopBar = memo(function ExploreTopBar({
}, [setSearchValue]);
return (
<div className="flex flex-col gap-4 py-4 px-4 w-full sticky top-0 z-30 bg-WHITE">
<div className="flex flex-col gap-4 pb-4 pt-[calc(1rem_+_env(safe-area-inset-top))] px-4 w-full sticky top-0 z-30 bg-WHITE">
<div className="flex items-center justify-center gap-2 rounded-[10px] w-full relative">
<AppInput
inputIcon={Search}

View File

@ -57,7 +57,7 @@ const HomePageTopBar = memo(() => {
);
return (
<div className="flex gap-2 flex-row h-[48px] w-full bg-WHITE items-center px-4 sticky top-0 z-30">
<div className="flex gap-2 flex-row h-[calc(48px_+_env(safe-area-inset-top))] pt-[env(safe-area-inset-top)] w-full bg-WHITE items-center px-4 sticky top-0 z-30">
<div className="flex relative items-center gap-2 justify-center w-full">
<div
className="absolute right-0"

View File

@ -17,10 +17,10 @@ const ChatHeader = ({
const { theme } = useRootData();
const navigate = useNavigate();
return (
<div className="max-w-md mx-auto flex flex-row h-[65px] bg-WHITE fixed top-0 left-0 w-full items-center justify-center border-b border-inner-border z-10">
<div className="max-w-md mx-auto flex flex-row h-[calc(65px_+_env(safe-area-inset-top))] pt-[env(safe-area-inset-top)] bg-WHITE fixed top-0 left-0 w-full items-center justify-center border-b border-inner-border z-10">
<ArrowRight
onClick={() => safeGoBack(navigate)}
className="absolute top-5 right-5"
className="absolute top-[calc(1.25rem_+_env(safe-area-inset-top))] right-5"
/>
{isConnected ? (
<Link

View File

@ -91,7 +91,7 @@ const MessageInput = ({
</button>
</div>
)}
<div className="max-w-md mx-auto fixed px-2 bottom-0 py-2 w-full bg-WHITE">
<div className="max-w-md mx-auto fixed px-2 bottom-0 pt-2 pb-[calc(0.5rem_+_env(safe-area-inset-bottom))] w-full bg-WHITE">
{replyToMessage && (
<div className="bg-GRAY3 rounded-lg p-2 flex justify-between items-center mb-2">
<div className="flex items-center">

View File

@ -254,7 +254,7 @@ export default function ThreadChatPage({ threadId }: ThreadChatPageProps) {
) : (
<>
<div
className={`hide-scrollbar flex-grow w-full max-h-[calc(100vh-${replyToMessage ? "120px" : "75px"})] flex flex-col`}
className={`hide-scrollbar flex-grow w-full max-h-[calc(100vh_-_${replyToMessage ? "120px" : "75px"}_-_env(safe-area-inset-top)_-_env(safe-area-inset-bottom))] flex flex-col`}
>
{/* Messages list */}
<MessageList

View File

@ -275,7 +275,9 @@ export const meta: MetaFunction = () => {
},
{ name: "author", content: "Vitrown" },
{ name: "robots", content: "index, follow" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
// viewport is defined globally in root.tsx (with viewport-fit=cover for
// iOS safe-area handling); don't override it here or the home page loses
// edge-to-edge / notch support.
// Open Graph
{ property: "og:type", content: "website" },