From 8b10e7fb1736a70d64828b9437ef62c070b992e2 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sun, 5 Apr 2026 23:40:36 +0800 Subject: [PATCH] fix(layout): use mobile footer bar in portrait mode without regressing phone panel animation, closes #3742 (#3759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On mobile tablets/foldables (Android or iOS) in portrait, the viewport width can exceed the Tailwind \`sm:\` breakpoint (640px), causing the desktop footer bar to show and hiding the brightness / font size / color / progress panels that only exist in the mobile layout. The previous fix in #3746 introduced a regression on phones: wrapping MobileFooterBar's children in a \`
\` changed the flex layout of the footer container, and panels no longer slid cleanly behind the navigation bar on dismissal. That PR was reverted. This re-implementation scopes the override narrowly: - \`forceMobileLayout\` is only true for mobile devices in portrait with innerWidth >= 640. Phones (innerWidth < 640) always get \`false\`, so every \`!forceMobileLayout && '…'\` expression evaluates to the original class string — phone classNames are set-equal to the pre-#3746 version. - MobileFooterBar keeps its Fragment return; no wrapper div is introduced anywhere in the tree, preserving the panel slide-down animation exactly as before. - DesktopFooterBar, NavigationBar, and the three panels (ColorPanel / FontLayoutPanel / NavigationPanel) gate their \`sm:hidden\` / \`sm:flex\` classes on \`!forceMobileLayout\` so they correctly show/hide on wide portrait tablets. - The orphaned \`.force-mobile-layout\` CSS override in globals.css is removed since we no longer rely on a class-based escape hatch. Co-authored-by: Claude Opus 4.6 (1M context) --- .../components/footerbar/ColorPanel.tsx | 10 ++++++-- .../components/footerbar/DesktopFooterBar.tsx | 12 +++++----- .../components/footerbar/FontLayoutPanel.tsx | 5 +++- .../reader/components/footerbar/FooterBar.tsx | 24 +++++++++++-------- .../components/footerbar/MobileFooterBar.tsx | 19 +++++++++------ .../components/footerbar/NavigationBar.tsx | 8 +++---- .../components/footerbar/NavigationPanel.tsx | 5 +++- .../app/reader/components/footerbar/types.ts | 2 +- apps/readest-app/src/styles/globals.css | 7 ------ 9 files changed, 53 insertions(+), 39 deletions(-) diff --git a/apps/readest-app/src/app/reader/components/footerbar/ColorPanel.tsx b/apps/readest-app/src/app/reader/components/footerbar/ColorPanel.tsx index 96cda389..d051445c 100644 --- a/apps/readest-app/src/app/reader/components/footerbar/ColorPanel.tsx +++ b/apps/readest-app/src/app/reader/components/footerbar/ColorPanel.tsx @@ -21,9 +21,14 @@ const SCREEN_BRIGHTNESS_LIMITS = { interface ColorPanelProps { actionTab: string; bottomOffset: string; + forceMobileLayout: boolean; } -export const ColorPanel: React.FC = ({ actionTab, bottomOffset }) => { +export const ColorPanel: React.FC = ({ + actionTab, + bottomOffset, + forceMobileLayout, +}) => { const _ = useTranslation(); const { envConfig, appService } = useEnv(); const { settings } = useSettingsStore(); @@ -72,7 +77,8 @@ export const ColorPanel: React.FC = ({ actionTab, bottomOffset }; const classes = clsx( - 'footerbar-color-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden', + 'footerbar-color-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all', + !forceMobileLayout && 'sm:hidden', actionTab === 'color' ? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out' : 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in', diff --git a/apps/readest-app/src/app/reader/components/footerbar/DesktopFooterBar.tsx b/apps/readest-app/src/app/reader/components/footerbar/DesktopFooterBar.tsx index aa713287..cecd2e1a 100644 --- a/apps/readest-app/src/app/reader/components/footerbar/DesktopFooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/footerbar/DesktopFooterBar.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import React, { useCallback, useEffect, useRef } from 'react'; import { FaHeadphones } from 'react-icons/fa6'; import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri'; @@ -17,7 +18,7 @@ const DesktopFooterBar: React.FC = ({ progressValid, progressFraction, navigationHandlers, - isMobileLayout, + forceMobileLayout, onSpeakText, }) => { const _ = useTranslation(); @@ -68,11 +69,10 @@ const DesktopFooterBar: React.FC = ({ return (
= ({ @@ -39,6 +40,7 @@ export const FontLayoutPanel: React.FC = ({ actionTab, bottomOffset, marginIconSize, + forceMobileLayout, }) => { const _ = useTranslation(); const { envConfig, appService } = useEnv(); @@ -91,7 +93,8 @@ export const FontLayoutPanel: React.FC = ({ }, []); const classes = clsx( - 'footerbar-font-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden', + 'footerbar-font-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all', + !forceMobileLayout && 'sm:hidden', actionTab === 'font' ? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out' : 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in', diff --git a/apps/readest-app/src/app/reader/components/footerbar/FooterBar.tsx b/apps/readest-app/src/app/reader/components/footerbar/FooterBar.tsx index a4248842..bf657ebc 100644 --- a/apps/readest-app/src/app/reader/components/footerbar/FooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/footerbar/FooterBar.tsx @@ -11,10 +11,10 @@ import { eventDispatcher } from '@/utils/event'; import { FooterBarProps, NavigationHandlers, FooterBarChildProps } from './types'; import { debounce } from '@/utils/debounce'; import { viewPagination } from '../../hooks/usePagination'; +import { RSVPControl } from '../rsvp'; import MobileFooterBar from './MobileFooterBar'; import DesktopFooterBar from './DesktopFooterBar'; import TTSControl from '../tts/TTSControl'; -import { RSVPControl } from '../rsvp'; const FooterBar: React.FC = ({ bookKey, @@ -194,9 +194,12 @@ const FooterBar: React.FC = ({ const footerBarRef = useRef(null); useSpatialNavigation(footerBarRef, isVisible); - const isPortrait = window.innerWidth <= window.innerHeight; - const isMobile = appService?.isMobile || window.innerWidth < 640; - const isMobileLayout = isMobile || (!!appService?.isAndroidApp && isPortrait); + // Force the mobile footer bar on mobile tablets/foldables in portrait mode + // where the viewport width exceeds the `sm:` (640px) breakpoint. Phones + // (innerWidth < 640) are intentionally excluded so their styling and panel + // slide-down animation remain exactly as before — see #3742 / #3746. + const forceMobileLayout = + !!appService?.isMobile && window.innerWidth >= 640 && window.innerWidth <= window.innerHeight; const commonProps: FooterBarChildProps = { bookKey, @@ -205,7 +208,7 @@ const FooterBar: React.FC = ({ progressValid, progressFraction, navigationHandlers, - isMobileLayout, + forceMobileLayout, onSetActionTab: handleSetActionTab, onSpeakText: handleSpeakText, }; @@ -216,23 +219,24 @@ const FooterBar: React.FC = ({ const containerClasses = clsx( 'footer-bar shadow-xs bottom-0 left-0 z-10 flex w-full flex-col', - isMobileLayout ? '' : 'sm:h-[52px]', - isMobileLayout ? '' : 'sm:bg-base-100 sm:border-none', + !forceMobileLayout && 'sm:h-[52px] sm:bg-base-100 sm:border-none', 'border-base-300/50 border-t', 'transition-[opacity,transform] duration-300', - isMobileLayout ? 'fixed' : window.innerWidth < 640 ? 'fixed' : 'absolute', + forceMobileLayout || window.innerWidth < 640 ? 'fixed' : 'absolute', appService?.hasRoundedWindow && 'rounded-window-bottom-right', !isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left', isHoveredAnim && 'hover-bar-anim', - !isMobileLayout && + !forceMobileLayout && (needHorizontalScroll ? 'sm:!bottom-3 sm:!h-10 sm:justify-end' : 'sm:justify-center'), isVisible ? 'pointer-events-auto translate-y-0 opacity-100' - : isMobileLayout + : forceMobileLayout ? 'pointer-events-none translate-y-full opacity-0' : 'pointer-events-none translate-y-full opacity-0 sm:translate-y-0', ); + const isMobile = appService?.isMobile || window.innerWidth < 640; + return ( <> {/* Hover trigger area */} diff --git a/apps/readest-app/src/app/reader/components/footerbar/MobileFooterBar.tsx b/apps/readest-app/src/app/reader/components/footerbar/MobileFooterBar.tsx index 93b37902..80d393d3 100644 --- a/apps/readest-app/src/app/reader/components/footerbar/MobileFooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/footerbar/MobileFooterBar.tsx @@ -1,4 +1,3 @@ -import clsx from 'clsx'; import React from 'react'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { FooterBarChildProps } from './types'; @@ -14,17 +13,21 @@ const MobileFooterBar: React.FC = ({ progressValid, progressFraction, navigationHandlers, - isMobileLayout, + forceMobileLayout, onSetActionTab, }) => { - const isMobile = isMobileLayout || window.innerWidth < 640 || window.innerHeight < 640; + const isMobile = forceMobileLayout || window.innerWidth < 640 || window.innerHeight < 640; const sliderHeight = useResponsiveSize(28); const marginIconSize = useResponsiveSize(20); const bottomOffset = isMobile ? `${gridInsets.bottom * 0.33 + 64}px` : '64px'; return ( -
- + <> + = ({ navigationHandlers={navigationHandlers} bottomOffset={bottomOffset} sliderHeight={sliderHeight} + forceMobileLayout={forceMobileLayout} /> -
+ ); }; diff --git a/apps/readest-app/src/app/reader/components/footerbar/NavigationBar.tsx b/apps/readest-app/src/app/reader/components/footerbar/NavigationBar.tsx index f95da81d..ffbfea37 100644 --- a/apps/readest-app/src/app/reader/components/footerbar/NavigationBar.tsx +++ b/apps/readest-app/src/app/reader/components/footerbar/NavigationBar.tsx @@ -16,7 +16,7 @@ interface NavigationBarProps { bookKey: string; actionTab: string; gridInsets: Insets; - isMobileLayout: boolean; + forceMobileLayout: boolean; onSetActionTab: (tab: string) => void; } @@ -24,10 +24,10 @@ export const NavigationBar: React.FC = ({ bookKey, actionTab, gridInsets, - isMobileLayout, + forceMobileLayout, onSetActionTab, }) => { - const isMobile = window.innerWidth < 640 || window.innerHeight < 640; + const isMobile = forceMobileLayout || window.innerWidth < 640 || window.innerHeight < 640; const _ = useTranslation(); const { appService } = useEnv(); const { getViewState } = useReaderStore(); @@ -40,7 +40,7 @@ export const NavigationBar: React.FC = ({
= ({ @@ -32,6 +33,7 @@ export const NavigationPanel: React.FC = ({ viewSettings, bottomOffset, sliderHeight, + forceMobileLayout, }) => { const _ = useTranslation(); const { appService } = useEnv(); @@ -58,7 +60,8 @@ export const NavigationPanel: React.FC = ({ ); const classes = clsx( - 'footerbar-progress-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden', + 'footerbar-progress-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all', + !forceMobileLayout && 'sm:hidden', actionTab === 'progress' ? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out' : 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in', diff --git a/apps/readest-app/src/app/reader/components/footerbar/types.ts b/apps/readest-app/src/app/reader/components/footerbar/types.ts index eaa1a63d..9de111f0 100644 --- a/apps/readest-app/src/app/reader/components/footerbar/types.ts +++ b/apps/readest-app/src/app/reader/components/footerbar/types.ts @@ -27,7 +27,7 @@ export interface FooterBarChildProps { progressValid: boolean; gridInsets: Insets; actionTab: string; - isMobileLayout: boolean; + forceMobileLayout: boolean; onSetActionTab: (tab: string) => void; onSpeakText: () => void; } diff --git a/apps/readest-app/src/styles/globals.css b/apps/readest-app/src/styles/globals.css index 6994453f..0d799a1a 100644 --- a/apps/readest-app/src/styles/globals.css +++ b/apps/readest-app/src/styles/globals.css @@ -441,13 +441,6 @@ foliate-fxl { mask: linear-gradient(180deg, black 0%, rgba(0, 0, 0, 0.9) 50%, rgba(0, 0, 0, 0.5) 100%); } -/* Force mobile footer bar layout on Android portrait (overrides sm:hidden breakpoint) */ -@media (min-width: 640px) { - .force-mobile-layout .sm\:hidden { - display: flex; - } -} - .visible-focus-inset-2:focus-visible { outline: none; box-shadow: