Add reader navigation: swipe, keyboard, side arrows, and wide layout
- Swipe left/right on mobile to go between chapters (60px threshold, ignores vertical-dominant gestures so scrolling still works) - Left/right arrow keys on desktop navigate chapters (skips inputs) - Fixed side ‹ › buttons at viewport midpoint for one-tap chapter nav; disappear when at the first/last chapter, hidden in draw mode - Wide toggle (⊞) switches between narrow reading column (max-w-3xl) and full-width two-column layout (max-w-7xl, md:columns-2); persisted to localStorage so the preference survives page reloads Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useApp } from '../context/AppContext.js';
|
import { useApp } from '../context/AppContext.js';
|
||||||
import { bookOptions, BookmarkIcon, CopyIcon } from '../App.jsx';
|
import { bookOptions, BookmarkIcon, CopyIcon } from '../App.jsx';
|
||||||
import DrawCanvas from './DrawCanvas.jsx';
|
import DrawCanvas from './DrawCanvas.jsx';
|
||||||
@@ -52,8 +52,42 @@ export default function ReaderPage() {
|
|||||||
|
|
||||||
const readerBook = bookOptions.find((b) => b.abbrev === readerBookAbbrev);
|
const readerBook = bookOptions.find((b) => b.abbrev === readerBookAbbrev);
|
||||||
const [readerDrawMode, setReaderDrawMode] = useState(false);
|
const [readerDrawMode, setReaderDrawMode] = useState(false);
|
||||||
|
const [readerWideLayout, setReaderWideLayout] = useState(() => localStorage.getItem('reader-wide') === '1');
|
||||||
const readerPageInkStrokes = readerInkByPage?.[`${readerBookAbbrev}_${readerChapter}`] ?? [];
|
const readerPageInkStrokes = readerInkByPage?.[`${readerBookAbbrev}_${readerChapter}`] ?? [];
|
||||||
const [selectionPicker, setSelectionPicker] = useState(null);
|
const [selectionPicker, setSelectionPicker] = useState(null);
|
||||||
|
const swipeTouchRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('reader-wide', readerWideLayout ? '1' : '0');
|
||||||
|
}, [readerWideLayout]);
|
||||||
|
|
||||||
|
// Swipe left/right to navigate chapters
|
||||||
|
const handleTouchStart = (e) => {
|
||||||
|
if (readerDrawMode) return;
|
||||||
|
const t = e.changedTouches[0];
|
||||||
|
swipeTouchRef.current = { x: t.clientX, y: t.clientY };
|
||||||
|
};
|
||||||
|
const handleTouchEnd = (e) => {
|
||||||
|
if (readerDrawMode || !swipeTouchRef.current) return;
|
||||||
|
const t = e.changedTouches[0];
|
||||||
|
const dx = t.clientX - swipeTouchRef.current.x;
|
||||||
|
const dy = t.clientY - swipeTouchRef.current.y;
|
||||||
|
swipeTouchRef.current = null;
|
||||||
|
if (Math.abs(dx) < 60 || Math.abs(dy) > Math.abs(dx) * 0.8) return;
|
||||||
|
if (dx < 0) readerGoToNextChapter();
|
||||||
|
else readerGoToPreviousChapter();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Arrow keys for desktop
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKey = (e) => {
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
|
||||||
|
if (e.key === 'ArrowLeft') readerGoToPreviousChapter();
|
||||||
|
else if (e.key === 'ArrowRight') readerGoToNextChapter();
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKey);
|
||||||
|
return () => window.removeEventListener('keydown', handleKey);
|
||||||
|
}, [readerGoToPreviousChapter, readerGoToNextChapter]);
|
||||||
|
|
||||||
// Dismiss the highlight picker when tapping outside it
|
// Dismiss the highlight picker when tapping outside it
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -167,7 +201,11 @@ export default function ReaderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className={`mx-auto px-4 py-8 sm:px-6 lg:px-8 ${readerDrawMode ? 'max-w-full' : 'max-w-3xl'}`}>
|
<main
|
||||||
|
className={`mx-auto px-4 py-8 sm:px-6 lg:px-8 ${readerDrawMode ? 'max-w-full' : readerWideLayout ? 'max-w-7xl' : 'max-w-3xl'}`}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
{/* Navigation + tools bar */}
|
{/* Navigation + tools bar */}
|
||||||
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-3xl border border-slate-200 bg-white p-4 shadow-panel">
|
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-3xl border border-slate-200 bg-white p-4 shadow-panel">
|
||||||
<label className="text-sm text-slate-600">
|
<label className="text-sm text-slate-600">
|
||||||
@@ -299,6 +337,13 @@ export default function ReaderPage() {
|
|||||||
>
|
>
|
||||||
✏ Draw
|
✏ Draw
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setReaderWideLayout((v) => !v)}
|
||||||
|
className={`rounded-lg px-3 py-1 text-xs font-semibold transition ${readerWideLayout ? 'bg-slate-900 text-white' : 'border border-slate-300 text-slate-600 hover:bg-slate-50'}`}
|
||||||
|
>
|
||||||
|
⊞ Wide
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Whole-Bible search results */}
|
{/* Whole-Bible search results */}
|
||||||
@@ -404,7 +449,8 @@ export default function ReaderPage() {
|
|||||||
return <p className="text-sm text-slate-500">No verses match "{readerSearch}".</p>;
|
return <p className="text-sm text-slate-500">No verses match "{readerSearch}".</p>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 leading-relaxed text-slate-800" style={{ fontSize: `${readerFontSize}em` }}
|
<div className={`leading-relaxed text-slate-800 ${readerWideLayout ? 'md:columns-2 md:gap-x-10 space-y-0' : 'space-y-3'}`}
|
||||||
|
style={{ fontSize: `${readerFontSize}em` }}
|
||||||
onPointerUp={handleSelectionEnd}>
|
onPointerUp={handleSelectionEnd}>
|
||||||
{filtered.map((verse) => {
|
{filtered.map((verse) => {
|
||||||
const chapterInterlinear = readerInterlinear?.[String(readerChapter)];
|
const chapterInterlinear = readerInterlinear?.[String(readerChapter)];
|
||||||
@@ -416,7 +462,7 @@ export default function ReaderPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={verse.number} id={`reader-verse-${verse.number}`} data-verse-num={verse.number}
|
<div key={verse.number} id={`reader-verse-${verse.number}`} data-verse-num={verse.number}
|
||||||
className="group rounded-xl transition"
|
className="group break-inside-avoid rounded-xl transition mb-3"
|
||||||
style={bmColor ? { backgroundColor: bmColor + '55', borderLeft: `3px solid ${bmColor}`, paddingLeft: '0.5rem' } : {}}>
|
style={bmColor ? { backgroundColor: bmColor + '55', borderLeft: `3px solid ${bmColor}`, paddingLeft: '0.5rem' } : {}}>
|
||||||
<div className="flex items-start gap-1">
|
<div className="flex items-start gap-1">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@@ -534,6 +580,30 @@ export default function ReaderPage() {
|
|||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* Side chapter nav — fixed left/right arrows */}
|
||||||
|
{!readerDrawMode && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={readerGoToPreviousChapter}
|
||||||
|
disabled={readerChapter <= 1}
|
||||||
|
aria-label="Previous chapter"
|
||||||
|
className="fixed left-3 top-1/2 -translate-y-1/2 z-10 flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 bg-white/80 text-slate-500 shadow-md backdrop-blur-sm transition hover:bg-white hover:text-slate-900 disabled:opacity-0 disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={readerGoToNextChapter}
|
||||||
|
disabled={readerChapter >= readerTotalChapters}
|
||||||
|
aria-label="Next chapter"
|
||||||
|
className="fixed right-3 top-1/2 -translate-y-1/2 z-10 flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 bg-white/80 text-slate-500 shadow-md backdrop-blur-sm transition hover:bg-white hover:text-slate-900 disabled:opacity-0 disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Highlight color picker — appears above text selection */}
|
{/* Highlight color picker — appears above text selection */}
|
||||||
{selectionPicker && (
|
{selectionPicker && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user