// The page itself: chapter heading, translation notice, and verses set in a
// serif face. Tap a verse to open the verse sheet; select text to highlight.
import { formatCrossRef } from '../../utils/wordStudy.js';
// How each typeface choice is set. "Easy to read" is a sans face with extra letter and word spacing.
const FONT_STYLES = {
serif: { fontFamily: undefined },
sans: { fontFamily: "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif" },
easy: { fontFamily: "'Atkinson Hyperlegible', Verdana, 'Trebuchet MS', ui-sans-serif, sans-serif", letterSpacing: '0.02em', wordSpacing: '0.12em' },
};
const LINE_HEIGHTS = { snug: 1.6, normal: 1.85, airy: 2.15 };
// `parallel` ({ translation, name, notice, verses, loading, error }) shows a second Bible beside the first.
export default function ReaderText({
title, translation, translationId, notice, verses, loading, error, layout, fontSize, wide,
font = 'serif', spacing = 'normal', showVerseNumbers = true, parallel = null,
bookmarks, bookKey, chapter, selectedVerse, onSelectVerse, renderVerseText, searchLower,
crossRefs, showCrossRefs, onSelectionEnd, footer,
}) {
const handleTap = (n) => {
const sel = window.getSelection();
if (sel && !sel.isCollapsed && sel.toString().trim()) return; // user is selecting text, not tapping
onSelectVerse(selectedVerse === n ? null : n);
};
const verseProps = (v) => {
const bm = bookmarks[`${bookKey}-${chapter}-${v.number}`];
const selected = selectedVerse === v.number;
return {
id: `reader-verse-${v.number}`,
'data-verse-num': v.number,
onClick: () => handleTap(v.number),
// Verses can be reached and chosen from the keyboard too
role: 'button',
tabIndex: 0,
'aria-pressed': selected,
onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleTap(v.number); } },
className: 'cursor-pointer rounded-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[var(--r-accent)]',
style: selected
? { backgroundColor: 'var(--r-hl)' }
: bm ? { backgroundColor: `${bm}66`, boxShadow: `0 0 0 2px ${bm}66` } : undefined,
};
};
const chips = (v) => showCrossRefs && crossRefs?.[v.number]?.length > 0 && (
{crossRefs[v.number].slice(0, 4).map((ref, i) => (
{formatCrossRef(ref)}
))}
);
const num = (n) => (showVerseNumbers ? {n} : null);
// Both Bibles by verse number, so a verse one translation leaves out still lines up with the other.
const parallelRows = parallel && !parallel.error ? (() => {
const second = new Map(parallel.verses.map((v) => [v.number, v]));
const first = new Map(verses.map((v) => [v.number, v]));
return [...new Set([...first.keys(), ...second.keys()])].sort((a, b) => a - b)
.map((n) => ({ number: n, first: first.get(n), second: second.get(n) }));
})() : null;
const sideBySide = !!parallel;
const bodyStyle = {
...FONT_STYLES[font],
lineHeight: LINE_HEIGHTS[spacing] ?? LINE_HEIGHTS.normal,
};
// The page grows with the screen: type is 18px on a phone, ~20px on an iPad and ~24px on a big
// monitor (times your text-size choice), and the column is measured in the type's own width (em), so
// the line length stays comfortable (~70 characters) instead of becoming a narrow strip on a wide
// screen. Two columns / side-by-side take more room.
const pageStyle = {
fontSize: `calc(clamp(1.125rem, 0.9rem + 0.7vw, 1.55rem) * ${fontSize})`,
maxWidth: wide || sideBySide ? 'min(100%, 72em)' : 'min(100%, 40em)',
};
return (
{loading && }
{error && {error}
}
{!loading && !error && parallel?.error && (
Couldn’t load {parallel.name}: {parallel.error}
)}
{!loading && !error && sideBySide && !parallel.error && (
{translationId}{parallel.translation}
{parallelRows.map((row) => (
{row.first
? {num(row.first.number)}{renderVerseText(row.first.number, row.first.text, searchLower)}{chips(row.first)}
: {num(row.number)}—}
{parallel.translation}
{row.second ? <>{num(row.second.number)}{row.second.text}> : parallel.loading ? … : '—'}
))}
)}
{!loading && !error && !sideBySide && (
{layout === 'flow' ? (
{verses.map((v) => (
{num(v.number)}{renderVerseText(v.number, v.text, searchLower)}{chips(v)}{' '}
))}
) : (
verses.map((v) => (
{num(v.number)}{renderVerseText(v.number, v.text, searchLower)}{chips(v)}
))
)}
)}
{!loading && !error && {footer}
}
{notice && !loading && {notice}
}
{parallel?.notice && !loading && !parallel.loading && {parallel.notice}
}
);
}
// Grey bars the shape of a page, so a slow chapter load doesn't just show the word "Loading".
function ChapterSkeleton() {
return (
{[100, 96, 92, 98, 84, 100, 90, 70].map((w, i) =>
)}
Loading…
);
}