fix(layout): fixed some layout issues for RSVP, closes #3199 (#3254)

This commit is contained in:
Huang Xin
2026-02-11 00:38:33 +08:00
committed by GitHub
parent 9444be7fcc
commit 226bf7033e
4 changed files with 48 additions and 37 deletions
@@ -270,6 +270,7 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
};
fetchDefinitions(lookupWord, langCode);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [_, lookupWord, lang]);
return (
@@ -250,7 +250,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
)}
<TTSControl bookKey={bookKey} gridInsets={gridInsets} />
<RSVPControl bookKey={bookKey} />
<RSVPControl bookKey={bookKey} gridInsets={gridInsets} />
</>
);
};
@@ -9,11 +9,13 @@ import { RSVPController, RsvpStartChoice, RsvpStopPosition } from '@/services/rs
import { eventDispatcher } from '@/utils/event';
import { useTranslation } from '@/hooks/useTranslation';
import { BookNote } from '@/types/book';
import { Insets } from '@/types/misc';
import RSVPOverlay from './RSVPOverlay';
import RSVPStartDialog from './RSVPStartDialog';
interface RSVPControlProps {
bookKey: string;
gridInsets: Insets;
}
// Helper to expand a range to include the full sentence
@@ -99,7 +101,7 @@ const expandRangeToSentence = (range: Range, doc: Document): Range => {
return range;
};
const RSVPControl: React.FC<RSVPControlProps> = ({ bookKey }) => {
const RSVPControl: React.FC<RSVPControlProps> = ({ bookKey, gridInsets }) => {
const _ = useTranslation();
const {
getView,
@@ -461,6 +463,7 @@ const RSVPControl: React.FC<RSVPControlProps> = ({ bookKey }) => {
portalContainer &&
createPortal(
<RSVPOverlay
gridInsets={gridInsets}
controller={controllerRef.current}
chapters={chapters}
currentChapterHref={currentChapterHref}
@@ -2,6 +2,7 @@
import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import clsx from 'clsx';
import { Insets } from '@/types/misc';
import { RsvpState, RsvpWord, RSVPController } from '@/services/rsvp';
import { useThemeStore } from '@/store/themeStore';
import { TOCItem } from '@/libs/document';
@@ -15,6 +16,7 @@ import {
IoAdd,
} from 'react-icons/io5';
import { useTranslation } from '@/hooks/useTranslation';
import { Overlay } from '@/components/Overlay';
interface FlatChapter {
label: string;
@@ -23,6 +25,7 @@ interface FlatChapter {
}
interface RSVPOverlayProps {
gridInsets: Insets;
controller: RSVPController;
chapters: TOCItem[];
currentChapterHref: string | null;
@@ -32,6 +35,7 @@ interface RSVPOverlayProps {
}
const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
gridInsets,
controller,
chapters,
currentChapterHref,
@@ -293,9 +297,10 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
aria-label={_('Speed Reading')}
className='fixed inset-0 z-[10000] flex select-none flex-col'
style={{
paddingTop: `${gridInsets.top}px`,
paddingBottom: `${gridInsets.bottom * 0.33}px`,
backgroundColor: bgColor,
color: fgColor,
// Ensure solid background - no transparency
backdropFilter: 'none',
// @ts-expect-error CSS custom properties
'--rsvp-accent': accentColor,
@@ -305,64 +310,66 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Header */}
<div className='rsvp-header flex shrink-0 items-center justify-between gap-2 p-3 md:gap-4 md:p-4'>
{/* ── Header ── */}
<div className='rsvp-header flex shrink-0 items-center gap-2 px-3 py-2 md:gap-3 md:px-5 md:py-3'>
<button
aria-label={_('Close Speed Reading')}
className='flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full border-none bg-transparent transition-colors hover:bg-gray-500/20 md:h-11 md:w-11'
onClick={onClose}
title={_('Close')}
className='flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-colors hover:bg-gray-500/20 active:scale-95'
onClick={onClose}
>
<IoClose className='h-5 w-5 md:h-6 md:w-6' />
<IoClose className='h-5 w-5' />
</button>
{/* Chapter selector */}
<div className='relative mx-2 min-w-0 max-w-[200px] flex-1 md:mx-4 md:max-w-[400px]'>
<div className='relative min-w-0 flex-1'>
<button
className='flex w-full cursor-pointer items-center justify-between gap-1 rounded-lg border border-gray-500/30 bg-gray-500/15 px-2 py-1.5 text-xs transition-colors hover:bg-gray-500/25 md:gap-2 md:px-3 md:py-2 md:text-sm'
className='flex w-full items-center gap-1.5 rounded-full border border-gray-500/20 bg-gray-500/10 px-3 py-1.5 text-sm transition-colors hover:bg-gray-500/20 active:scale-[0.98]'
onClick={() => setShowChapterDropdown(!showChapterDropdown)}
title={_('Select Chapter')}
>
<span className='overflow-hidden text-ellipsis whitespace-nowrap'>
<span className='min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-left'>
{getCurrentChapterLabel()}
</span>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
className='shrink-0 md:h-4 md:w-4'
strokeWidth='2.5'
className='h-3.5 w-3.5 shrink-0 opacity-50'
>
<path d='M6 9l6 6 6-6' />
</svg>
</button>
{showChapterDropdown && (
<div
className='absolute left-0 right-0 top-full z-[100] mt-1 max-h-[300px] overflow-y-auto rounded-lg border border-gray-500/30 shadow-lg'
style={{ backgroundColor: bgColor }}
>
{flatChapters.map((chapter, idx) => (
<button
key={`${chapter.href}-${idx}`}
className={clsx(
'block w-full cursor-pointer border-none bg-transparent px-3 py-2.5 text-left text-sm transition-colors hover:bg-gray-500/20',
isChapterActive(chapter.href) &&
'bg-[color-mix(in_srgb,var(--rsvp-accent)_20%,transparent)] font-semibold',
)}
style={{ paddingLeft: `${0.75 + chapter.level * 1}rem` }}
onClick={() => handleChapterSelect(chapter.href)}
>
{chapter.label}
</button>
))}
</div>
<>
<Overlay onDismiss={() => setShowChapterDropdown(false)} />
<div
className='absolute left-0 right-0 top-full z-[100] mt-1.5 max-h-64 overflow-y-auto rounded-2xl border border-gray-500/20 px-2 shadow-2xl'
style={{ backgroundColor: bgColor }}
>
{flatChapters.map((chapter, idx) => (
<button
key={`${chapter.href}-${idx}`}
className={clsx(
'block w-full rounded-md border-none bg-transparent px-4 py-2.5 text-left text-sm transition-colors first:rounded-t-2xl last:rounded-b-2xl hover:bg-gray-500/15',
isChapterActive(chapter.href) &&
'bg-[color-mix(in_srgb,var(--rsvp-accent)_15%,transparent)] font-semibold',
)}
style={{ paddingLeft: `${1 + chapter.level * 0.875}rem` }}
onClick={() => handleChapterSelect(chapter.href)}
>
{chapter.label}
</button>
))}
</div>
</>
)}
</div>
<div className='shrink-0 text-sm font-medium opacity-70 md:text-base'>
{_('{{number}} WPM', { number: state.wpm })}
{/* WPM badge */}
<div className='shrink-0 rounded-full border border-gray-500/20 bg-gray-500/10 px-3 py-1.5 text-sm tabular-nums'>
<span className='font-semibold'>{state.wpm}</span>
<span className='ml-0.5 text-xs opacity-50'>WPM</span>
</div>
</div>