forked from akai/readest
fix(layout): use mobile footer bar in portrait mode without regressing phone panel animation, closes #3742 (#3759)
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 \`<div>\` 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) <noreply@anthropic.com>
This commit is contained in:
@@ -21,9 +21,14 @@ const SCREEN_BRIGHTNESS_LIMITS = {
|
||||
interface ColorPanelProps {
|
||||
actionTab: string;
|
||||
bottomOffset: string;
|
||||
forceMobileLayout: boolean;
|
||||
}
|
||||
|
||||
export const ColorPanel: React.FC<ColorPanelProps> = ({ actionTab, bottomOffset }) => {
|
||||
export const ColorPanel: React.FC<ColorPanelProps> = ({
|
||||
actionTab,
|
||||
bottomOffset,
|
||||
forceMobileLayout,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
@@ -72,7 +77,8 @@ export const ColorPanel: React.FC<ColorPanelProps> = ({ 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',
|
||||
|
||||
@@ -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<FooterBarChildProps> = ({
|
||||
progressValid,
|
||||
progressFraction,
|
||||
navigationHandlers,
|
||||
isMobileLayout,
|
||||
forceMobileLayout,
|
||||
onSpeakText,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
@@ -68,11 +69,10 @@ const DesktopFooterBar: React.FC<FooterBarChildProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isMobileLayout
|
||||
? 'hidden'
|
||||
: 'hidden h-8 w-full items-center gap-x-4 overflow-x-auto px-4 sm:flex'
|
||||
}
|
||||
className={clsx(
|
||||
'hidden h-8 w-full items-center gap-x-4 overflow-x-auto px-4',
|
||||
!forceMobileLayout && 'sm:flex',
|
||||
)}
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33}px` : '0px',
|
||||
scrollbarWidth: 'none',
|
||||
|
||||
@@ -32,6 +32,7 @@ interface FontLayoutPanelProps {
|
||||
actionTab: string;
|
||||
bottomOffset: string;
|
||||
marginIconSize: number;
|
||||
forceMobileLayout: boolean;
|
||||
}
|
||||
|
||||
export const FontLayoutPanel: React.FC<FontLayoutPanelProps> = ({
|
||||
@@ -39,6 +40,7 @@ export const FontLayoutPanel: React.FC<FontLayoutPanelProps> = ({
|
||||
actionTab,
|
||||
bottomOffset,
|
||||
marginIconSize,
|
||||
forceMobileLayout,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
@@ -91,7 +93,8 @@ export const FontLayoutPanel: React.FC<FontLayoutPanelProps> = ({
|
||||
}, []);
|
||||
|
||||
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',
|
||||
|
||||
@@ -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<FooterBarProps> = ({
|
||||
bookKey,
|
||||
@@ -194,9 +194,12 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
const footerBarRef = useRef<HTMLDivElement>(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<FooterBarProps> = ({
|
||||
progressValid,
|
||||
progressFraction,
|
||||
navigationHandlers,
|
||||
isMobileLayout,
|
||||
forceMobileLayout,
|
||||
onSetActionTab: handleSetActionTab,
|
||||
onSpeakText: handleSpeakText,
|
||||
};
|
||||
@@ -216,23 +219,24 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
|
||||
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 */}
|
||||
|
||||
@@ -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<FooterBarChildProps> = ({
|
||||
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 (
|
||||
<div className={clsx(isMobileLayout && 'force-mobile-layout')}>
|
||||
<ColorPanel actionTab={actionTab} bottomOffset={bottomOffset} />
|
||||
<>
|
||||
<ColorPanel
|
||||
actionTab={actionTab}
|
||||
bottomOffset={bottomOffset}
|
||||
forceMobileLayout={forceMobileLayout}
|
||||
/>
|
||||
<NavigationPanel
|
||||
bookKey={bookKey}
|
||||
actionTab={actionTab}
|
||||
@@ -33,21 +36,23 @@ const MobileFooterBar: React.FC<FooterBarChildProps> = ({
|
||||
navigationHandlers={navigationHandlers}
|
||||
bottomOffset={bottomOffset}
|
||||
sliderHeight={sliderHeight}
|
||||
forceMobileLayout={forceMobileLayout}
|
||||
/>
|
||||
<FontLayoutPanel
|
||||
bookKey={bookKey}
|
||||
actionTab={actionTab}
|
||||
bottomOffset={bottomOffset}
|
||||
marginIconSize={marginIconSize}
|
||||
forceMobileLayout={forceMobileLayout}
|
||||
/>
|
||||
<NavigationBar
|
||||
bookKey={bookKey}
|
||||
actionTab={actionTab}
|
||||
gridInsets={gridInsets}
|
||||
isMobileLayout={isMobileLayout}
|
||||
forceMobileLayout={forceMobileLayout}
|
||||
onSetActionTab={onSetActionTab!}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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<NavigationBarProps> = ({
|
||||
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<NavigationBarProps> = ({
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-200 z-30 mt-auto flex w-full justify-between px-8 py-4',
|
||||
!isMobileLayout && 'sm:hidden',
|
||||
!forceMobileLayout && 'sm:hidden',
|
||||
)}
|
||||
style={{
|
||||
paddingBottom: appService?.isAndroidApp
|
||||
|
||||
@@ -21,6 +21,7 @@ interface NavigationPanelProps {
|
||||
viewSettings?: ViewSettings;
|
||||
bottomOffset: string;
|
||||
sliderHeight: number;
|
||||
forceMobileLayout: boolean;
|
||||
}
|
||||
|
||||
export const NavigationPanel: React.FC<NavigationPanelProps> = ({
|
||||
@@ -32,6 +33,7 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
|
||||
viewSettings,
|
||||
bottomOffset,
|
||||
sliderHeight,
|
||||
forceMobileLayout,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
@@ -58,7 +60,8 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
|
||||
);
|
||||
|
||||
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',
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface FooterBarChildProps {
|
||||
progressValid: boolean;
|
||||
gridInsets: Insets;
|
||||
actionTab: string;
|
||||
isMobileLayout: boolean;
|
||||
forceMobileLayout: boolean;
|
||||
onSetActionTab: (tab: string) => void;
|
||||
onSpeakText: () => void;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user