Files
readest/apps/readest-app/src/app/reader/components/SectionInfo.tsx
T
Huang Xin 2b524439bf fix(reader): keep running header/footer readable over light PDFs in dark mode (#4901) (#4911)
The running section title and page-number footer used text-neutral-content,
which is a light color in dark mode. A light-mode PDF stays white under a dark
theme (invertImgColorInDark defaults to false), so the light text sat on the
white page and became unreadable.

Blend the header/footer text against whatever is behind it using
mix-blend-mode: difference with a fixed white/75 anchor, so it inverts to dark
on a light page and stays light on a dark margin. white/75 matches the former
neutral-content brightness over the dark theme, so reflowable books look
unchanged. E-ink keeps its plain base-content text; StatusInfo and the sticky
progress bar manage their own colors and are left untouched.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:14:02 +02:00

126 lines
4.3 KiB
TypeScript

import clsx from 'clsx';
import React from 'react';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { eventDispatcher } from '@/utils/event';
interface SectionInfoProps {
bookKey: string;
section?: string;
showDoubleBorder: boolean;
isScrolled: boolean;
isVertical: boolean;
isEink: boolean;
horizontalGap: number;
contentInsets: Insets;
gridInsets: Insets;
}
const SectionInfo: React.FC<SectionInfoProps> = ({
bookKey,
section,
showDoubleBorder,
isScrolled,
isVertical,
isEink,
horizontalGap,
contentInsets,
gridInsets,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { hoveredBookKey, getView, getViewSettings, setHoveredBookKey } = useReaderStore();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const viewSettings = getViewSettings(bookKey)!;
const topInset = Math.max(
gridInsets.top,
appService?.isAndroidApp && systemUIVisible ? statusBarHeight / 2 : 0,
);
const handleNotchClick = () => {
if (eventDispatcher.dispatchSync('iframe-single-click')) return;
if (isScrolled) {
getView(bookKey)?.renderer.scrollToAnchor?.(0, 'anchor', true);
}
};
const handleSectionClick = () => {
if (eventDispatcher.dispatchSync('iframe-single-click')) return;
setHoveredBookKey(bookKey);
};
return (
<>
<div
className={clsx(
// Spans the grid cell and clips down to the top inset strip so the
// texture ::before (.notch-masked, see styles/textures.ts) shares
// .foliate-viewer::before's paint box — background-size cover/contain
// resolves against the element box, so a strip-sized box would
// mis-tile at the seam (#4486). clip-path also clips hit-testing,
// keeping the click target the inset strip only.
'notch-area absolute inset-0 z-10',
isScrolled && !isVertical && 'notch-masked bg-base-100',
)}
role='none'
tabIndex={-1}
onClick={handleNotchClick}
style={{
clipPath: `inset(0 0 calc(100% - ${topInset}px) 0)`,
}}
/>
<div
className={clsx(
'sectioninfo absolute flex items-center overflow-hidden font-sans',
// A light-mode PDF stays white under a dark theme, so the themed
// neutral-content title was unreadable on the page (#4901). Blend the
// text against whatever is actually behind it (page or margin) so it
// stays legible on any background. text-white/75 matches the previous
// neutral-content brightness over the dark theme, so nothing changes
// for reflowable books. E-ink keeps its plain base-content text.
isEink ? 'text-sm font-normal' : 'text-white/75 mix-blend-difference text-xs font-light',
isVertical ? 'writing-vertical-rl max-h-[85%]' : 'top-0',
)}
role='none'
tabIndex={-1}
onClick={handleSectionClick}
style={
isVertical
? {
top: `${(contentInsets.top - gridInsets.top) * 1.5}px`,
bottom: `${(contentInsets.bottom - gridInsets.bottom) * 1.5}px`,
right: showDoubleBorder
? `calc(${contentInsets.right}px)`
: `calc(${Math.max(0, contentInsets.right - 32)}px)`,
width: showDoubleBorder ? '32px' : `${contentInsets.right}px`,
}
: {
top: `${topInset}px`,
paddingInline: `calc(${horizontalGap / 2}% + ${contentInsets.left / 2}px)`,
width: '100%',
height: `${viewSettings.marginTopPx}px`,
}
}
>
<span
aria-label={section ? _('Section Title') + `: ${section}` : ''}
className={clsx(
'text-center',
isVertical ? '' : 'line-clamp-1',
!isVertical &&
(hoveredBookKey == bookKey || (hoveredBookKey && appService?.isMobile)) &&
'hidden',
)}
>
{section || ''}
</span>
</div>
</>
);
};
export default SectionInfo;