From 64c3b568e5a839584ebfb2ff2bb0b0bb4f1ff912 Mon Sep 17 00:00:00 2001 From: Ankesh Bharti Date: Sat, 18 Apr 2026 10:59:19 +0530 Subject: [PATCH 1/5] Fix mobile download prompt detection timing --- components/mobile-download-prompt.tsx | 40 +++++++++------------------ 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/components/mobile-download-prompt.tsx b/components/mobile-download-prompt.tsx index 3b0fcb77..7c062c92 100644 --- a/components/mobile-download-prompt.tsx +++ b/components/mobile-download-prompt.tsx @@ -240,27 +240,19 @@ function MobileDownloadPromptOverlay({ isOpen, onClose, targetUrl }: MobileDownl export function useMobileDownloadPrompt() { const [isOpen, setIsOpen] = useState(false) - const [isMobile, setIsMobile] = useState(false) - - useEffect(() => { - setIsMobile(isLikelyMobileDevice()) - }, []) const targetUrl = "https://www.tiles.run" - const openMobileDownloadPrompt = useCallback( - (event?: DownloadIntentEvent) => { - if (!isMobile) { - return false - } + const openMobileDownloadPrompt = useCallback((event?: DownloadIntentEvent) => { + if (!isLikelyMobileDevice()) { + return false + } - event?.preventDefault() - triggerHaptic() - setIsOpen(true) - return true - }, - [isMobile], - ) + event?.preventDefault() + triggerHaptic() + setIsOpen(true) + return true + }, []) return { openMobileDownloadPrompt, @@ -271,19 +263,13 @@ export function useMobileDownloadPrompt() { } export function GlobalMobileDownloadPrompt() { - const [isMobile, setIsMobile] = useState(false) const [isOpen, setIsOpen] = useState(false) useEffect(() => { - setIsMobile(isLikelyMobileDevice()) - }, []) - - useEffect(() => { - if (!isMobile) { - return - } - const onDocumentClick = (event: MouseEvent) => { + if (!isLikelyMobileDevice()) { + return + } const target = event.target if (!(target instanceof Element)) { return @@ -329,7 +315,7 @@ export function GlobalMobileDownloadPrompt() { return () => { document.removeEventListener("click", onDocumentClick, true) } - }, [isMobile]) + }, []) return ( Date: Sat, 18 Apr 2026 11:48:00 +0530 Subject: [PATCH 2/5] Render mobile download prompt overlay in body portal --- components/mobile-download-prompt.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/mobile-download-prompt.tsx b/components/mobile-download-prompt.tsx index 7c062c92..26150b6b 100644 --- a/components/mobile-download-prompt.tsx +++ b/components/mobile-download-prompt.tsx @@ -2,6 +2,7 @@ import { CalendarPlus, Check, Link2, Monitor } from "lucide-react" import { useCallback, useEffect, useState } from "react" +import { createPortal } from "react-dom" import { triggerHaptic } from "@/lib/haptics" import { FaArrowUpFromBracket } from "react-icons/fa6" @@ -174,7 +175,11 @@ function MobileDownloadPromptOverlay({ isOpen, onClose, targetUrl }: MobileDownl return null } - return ( + if (typeof document === "undefined") { + return null + } + + return createPortal(
- + , + document.body, ) } From d64eda0b23c12241aa5323192c4e90c930176859 Mon Sep 17 00:00:00 2001 From: Ankesh Bharti Date: Sat, 18 Apr 2026 11:59:26 +0530 Subject: [PATCH 3/5] Use global mobile prompt flow on homepage download actions --- components/home-content.tsx | 9 +-------- components/site-header.tsx | 17 ++++------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/components/home-content.tsx b/components/home-content.tsx index 7ec9ea86..3b0b31f4 100644 --- a/components/home-content.tsx +++ b/components/home-content.tsx @@ -10,14 +10,12 @@ import { Button } from "@/components/ui/button" import { triggerHaptic } from "@/lib/haptics" import { themeAwareHeaderPrimaryCtaClasses } from "@/lib/header-primary-cta-classes" import { getPersonById, splitPersonDisplayName } from "@/lib/people" -import { useMobileDownloadPrompt } from "@/components/mobile-download-prompt" interface HomeContentProps { highlightReadTimes: Record } export function HomeContent({ highlightReadTimes }: HomeContentProps) { - const { openMobileDownloadPrompt, mobileDownloadPrompt } = useMobileDownloadPrompt() const sectionHeadingClass = "text-2xl font-semibold tracking-tight text-foreground sm:text-3xl" const alphaPillClass = "inline-flex items-center rounded-full border border-black/20 px-1.5 py-0.5 text-[0.62rem] tracking-[0.08em] text-black/60 dark:border-white/25 dark:text-[#B9B9B9] sm:text-[0.66rem]" @@ -139,10 +137,7 @@ export function HomeContent({ highlightReadTimes }: HomeContentProps) { > { - if (openMobileDownloadPrompt(event)) { - return - } + onClick={() => { triggerHaptic() }} className="group flex items-center gap-2" @@ -564,8 +559,6 @@ export function HomeContent({ highlightReadTimes }: HomeContentProps) { - - {mobileDownloadPrompt} ) diff --git a/components/site-header.tsx b/components/site-header.tsx index 77cdc925..1b646915 100644 --- a/components/site-header.tsx +++ b/components/site-header.tsx @@ -8,7 +8,6 @@ import { Download } from "lucide-react" import { MobileMenu } from "./mobile-menu" import { triggerHaptic } from "@/lib/haptics" import { themeAwareHeaderPrimaryCtaClasses } from "@/lib/header-primary-cta-classes" -import { useMobileDownloadPrompt } from "@/components/mobile-download-prompt" import { usePathname } from "next/navigation" /** Set to true to show the top banner (e.g. for announcements). */ @@ -30,7 +29,7 @@ const SiteHeaderChrome = memo(function SiteHeaderChrome({ isBannerVisible: boolean onDismissBanner: () => void onOpenMenu: () => void - onDownloadClick: (event: { preventDefault: () => void }) => void + onDownloadClick: () => void onHomeClick: (event: { preventDefault: () => void }) => void }) { const headerChrome = themeAware @@ -190,7 +189,6 @@ const SiteHeaderChrome = memo(function SiteHeaderChrome({ function SiteHeaderContent({ themeAware = true }: SiteHeaderProps) { const [isMenuOpen, setIsMenuOpen] = useState(false) const [isBannerVisible, setIsBannerVisible] = useState(false) - const { openMobileDownloadPrompt, mobileDownloadPrompt } = useMobileDownloadPrompt() const pathname = usePathname() useEffect(() => { if (typeof window === "undefined" || !BANNER_ENABLED) return @@ -207,15 +205,9 @@ function SiteHeaderContent({ themeAware = true }: SiteHeaderProps) { const openMenu = useCallback(() => setIsMenuOpen(true), []) const closeMenu = useCallback(() => setIsMenuOpen(false), []) - const handleDownloadClick = useCallback( - (event: { preventDefault: () => void }) => { - if (openMobileDownloadPrompt(event)) { - return - } - triggerHaptic() - }, - [openMobileDownloadPrompt], - ) + const handleDownloadClick = useCallback(() => { + triggerHaptic() + }, []) const handleHomeClick = useCallback( (event: { preventDefault: () => void }) => { triggerHaptic() @@ -247,7 +239,6 @@ function SiteHeaderContent({ themeAware = true }: SiteHeaderProps) { themeAware={themeAware} hasBanner={isBannerVisible} /> - {mobileDownloadPrompt} ) } From 2397a9cd64275bdd31e8dcccf5fdd1f084d02332 Mon Sep 17 00:00:00 2001 From: Ankesh Bharti Date: Sat, 18 Apr 2026 12:12:01 +0530 Subject: [PATCH 4/5] Mount mobile download overlay on document root --- components/mobile-download-prompt.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/mobile-download-prompt.tsx b/components/mobile-download-prompt.tsx index 26150b6b..1d4c3b7d 100644 --- a/components/mobile-download-prompt.tsx +++ b/components/mobile-download-prompt.tsx @@ -240,7 +240,7 @@ function MobileDownloadPromptOverlay({ isOpen, onClose, targetUrl }: MobileDownl , - document.body, + document.documentElement, ) } From 8115e59adb8db5e733ac093bb8bc66d2c3049993 Mon Sep 17 00:00:00 2001 From: Ankesh Bharti Date: Sat, 18 Apr 2026 12:17:27 +0530 Subject: [PATCH 5/5] Rework mobile prompt viewport anchoring and menu close icon --- components/mobile-download-prompt.tsx | 31 +++++++++++++++++++++++++-- components/mobile-menu.tsx | 11 ++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/components/mobile-download-prompt.tsx b/components/mobile-download-prompt.tsx index 1d4c3b7d..3f784690 100644 --- a/components/mobile-download-prompt.tsx +++ b/components/mobile-download-prompt.tsx @@ -99,6 +99,7 @@ interface MobileDownloadPromptOverlayProps { function MobileDownloadPromptOverlay({ isOpen, onClose, targetUrl }: MobileDownloadPromptOverlayProps) { const [isCopyConfirmed, setIsCopyConfirmed] = useState(false) + const [viewportBox, setViewportBox] = useState({ top: 0, height: 0 }) const closePrompt = useCallback(() => { triggerHaptic() onClose() @@ -125,6 +126,28 @@ function MobileDownloadPromptOverlay({ isOpen, onClose, targetUrl }: MobileDownl } }, [isOpen, onClose]) + useEffect(() => { + if (!isOpen) { + return + } + + const updateViewportBox = () => { + setViewportBox({ + top: window.scrollY || window.pageYOffset || 0, + height: window.innerHeight || 0, + }) + } + + updateViewportBox() + window.addEventListener("resize", updateViewportBox) + window.addEventListener("orientationchange", updateViewportBox) + + return () => { + window.removeEventListener("resize", updateViewportBox) + window.removeEventListener("orientationchange", updateViewportBox) + } + }, [isOpen]) + const onShare = useCallback(async () => { triggerHaptic() if (navigator.share) { @@ -181,11 +204,15 @@ function MobileDownloadPromptOverlay({ isOpen, onClose, targetUrl }: MobileDownl return createPortal(
, - document.documentElement, + document.body, ) } diff --git a/components/mobile-menu.tsx b/components/mobile-menu.tsx index 51b2f01a..8541a28b 100644 --- a/components/mobile-menu.tsx +++ b/components/mobile-menu.tsx @@ -3,8 +3,8 @@ import Link from "next/link" import Image from "next/image" import { Button } from "@/components/ui/button" -import { useEffect, useState } from "react" -import { Download } from "lucide-react" +import { useEffect } from "react" +import { Download, X } from "lucide-react" import { FaXTwitter, FaBluesky, FaInstagram, FaDiscord, FaGithub, FaRss } from "react-icons/fa6" import { SiHuggingface } from "react-icons/si" import { triggerHaptic } from "@/lib/haptics" @@ -185,8 +185,11 @@ export function MobileMenu({ isOpen, onClose, themeAware = false, hasBanner = fa aria-label="Close navigation menu" type="button" > - - +