Preferences in Settings and a home page the user chooses
Build and Push Docker Image / build-and-push (push) Failing after 20s

Settings gets a Preferences section: theme, default Bible, the Bible new reading
plans start in, verse layout, commentary, text size, reading width, audio
narrator (the last three stay per device; text size and narrator are now
remembered). A Home page setting (My Studies, Bible Reader or Reading Plans)
decides where the app opens and where every Home button goes; already being
there means My Studies. Back arrows are labelled Home. Tests cover every page
leading back.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-09-29 10:55:07 -04:00
1 parent 760c1e568b
commit 8c4f54e033
15 files changed
+401 -23

No files matched your search

+1 -2
View File
@@ -39,6 +39,7 @@ export default function ReaderPage() {
readerInterlinear,
readerSelectedVerse, setReaderSelectedVerse,
readerFontSize, setReaderFontSize,
readerWide: readerWideLayout, setReaderWide: setReaderWideLayout,
readerBookmarks,
readerCrossRefs, setReaderCrossRefs,
readerCrossRefsLoading,
@@ -84,12 +85,10 @@ export default function ReaderPage() {
const [sheet, setSheet] = useState(null); // null | 'nav' | 'settings' | 'search' | 'bookmarks' | 'audio'
const [chromeVisible, setChromeVisible] = useState(true);
const [readerDrawMode, setReaderDrawMode] = useState(false);
const [readerWideLayout, setReaderWideLayout] = useState(() => localStorage.getItem('reader-wide') === '1');
const [selectionPicker, setSelectionPicker] = useState(null);
const swipeTouchRef = useRef(null);
const readerPageInkStrokes = readerInkByPage?.[`${readerBookAbbrev}_${readerChapter}`] ?? NO_STROKES;
useEffect(() => { localStorage.setItem('reader-wide', readerWideLayout ? '1' : '0'); }, [readerWideLayout]);
const chapterKey = `${readerBookAbbrev}.${readerChapter}`;
const label = (ref) => formatDayReadings([ref]);
+1 -1
View File
@@ -12,7 +12,7 @@ const ctx = {
readerVerses: [{ number: 1, text: 'Now there was a man of the Pharisees.' }, { number: 2, text: 'This man came to Jesus by night.' }],
readerTotalChapters: 21, readerLoading: false, readerError: '',
readerInterlinear: null, readerSelectedVerse: null, setReaderSelectedVerse: vi.fn(),
readerFontSize: 1, setReaderFontSize: noop, readerBookmarks: {},
readerFontSize: 1, setReaderFontSize: noop, readerWide: false, setReaderWide: noop, readerBookmarks: {},
readerCrossRefs: null, setReaderCrossRefs: noop, readerCrossRefsLoading: false,
readerShowCrossRefs: false, setReaderShowCrossRefs: noop,
readerSearch: '', setReaderSearch: noop, setReaderSearchActive: noop,
+3 -3
View File
@@ -40,7 +40,7 @@ export default function ReadingPlansPage() {
addCustomPlan, removeCustomPlan, updateCustomPlan,
openPlanReading, studyReadingPlanDay, studyBuildBusy,
readingPlanSyncStatus, readingPlanSyncError, retryReadingPlanSync,
readerTheme, setReaderTheme, readerTranslation,
readerTheme, setReaderTheme, readerTranslation, planTranslationPref,
} = useApp();
const [detail, setDetail] = useState(null); // { planId, progressId? }
@@ -62,7 +62,7 @@ export default function ReadingPlansPage() {
const cycleTheme = () => setReaderTheme(THEME_ORDER[(THEME_ORDER.indexOf(readerTheme) + 1) % THEME_ORDER.length]);
const actions = {
defaultTranslation: readerTranslation,
defaultTranslation: planTranslationPref && planTranslationPref !== 'same' ? planTranslationPref : readerTranslation,
setTranslation: setPlanTranslation,
toggleDay,
recalibrate: recalibratePace,
@@ -103,7 +103,7 @@ export default function ReadingPlansPage() {
<div className="min-h-screen bg-[var(--r-bg)] pb-16 text-[var(--r-text)]" style={themeVars(readerTheme)}>
<div className="sticky top-0 z-30 border-b border-[var(--r-line)] bg-[var(--r-bar)] font-sans">
<div className="mx-auto flex h-12 max-w-3xl items-center gap-1 px-2">
<button type="button" onClick={goHome} aria-label="Back to studies" className="flex h-9 w-9 items-center justify-center rounded-lg text-[var(--r-sub)] hover:bg-black/5"><ChevronLeft /></button>
<button type="button" onClick={goHome} aria-label="Back to home" className="flex h-9 items-center gap-0.5 rounded-lg pl-1 pr-2 text-[var(--r-sub)] hover:bg-black/5"><ChevronLeft /><span className="hidden text-xs sm:inline">Home</span></button>
<h1 className="mx-auto text-sm font-medium">Reading plans</h1>
{streak.current > 0 && <span className="text-xs text-[var(--r-accent)]">{streak.current} day streak</span>}
<button type="button" onClick={cycleTheme} className="ml-2 rounded-lg px-2 py-1.5 text-xs text-[var(--r-sub)] hover:bg-black/5" title="Change theme">{READER_THEMES[readerTheme]?.label}</button>
+82
View File
@@ -1,6 +1,9 @@
import { useState } from 'react';
import { useApp } from '../context/AppContext.js';
import PageShell from '../components/study/PageShell.jsx';
import { READER_TRANSLATIONS } from '../hooks/useBibleReader.js';
import { COMMENTARIES } from '../hooks/useVerseStudy.js';
import { READER_THEMES } from '../components/reader/readerThemes.js';
import {
startMfaSetup,
confirmMfaSetup,
@@ -12,10 +15,46 @@ import {
} from '../syncService.js';
import { exportJournalDocx, printJournalPdf } from '../utils/journalExport.js';
const FONT_SIZES = [['S', 0.875], ['M', 1], ['L', 1.125], ['XL', 1.25], ['XXL', 1.5]];
const selectCls = 'rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2 text-sm text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
// One labelled preference: what it is, whether it follows you between devices, and the control.
function PrefRow({ label, hint, synced, children }) {
return (
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-2 border-t border-[var(--r-line)] py-3 first:border-t-0 first:pt-0">
<div className="min-w-0 flex-1 basis-56">
<p className="text-sm font-medium">{label}</p>
<p className="text-xs text-[var(--r-sub)]">{hint} <span className="whitespace-nowrap">· {synced ? 'Synced to your account' : 'This device only'}</span></p>
</div>
{children}
</div>
);
}
function Segmented({ label, options, value, onChange }) {
return (
<div role="group" aria-label={label} className="flex overflow-hidden rounded-xl border border-[var(--r-line)]">
{options.map(([id, text]) => (
<button key={String(id)} type="button" aria-pressed={value === id} onClick={() => onChange(id)}
className={`px-3 py-1.5 text-sm ${value === id ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'}`}>{text}</button>
))}
</div>
);
}
export default function SettingsPage() {
const {
authUser, setAuthUser,
goHome,
readerTheme, setReaderTheme,
readerTranslation, setReaderTranslation,
planTranslationPref, setPlanTranslationPref,
startPagePref, setStartPagePref,
readerLayout, setReaderLayout,
readerFontSize, setReaderFontSize,
readerWide, setReaderWide,
readerCommentary, setReaderCommentary,
audioNarrator, setAudioNarrator,
} = useApp();
const [mfaSetup, setMfaSetup] = useState(null); // { secret, qrCodeDataUrl } while setup is in progress
const [mfaSetupCode, setMfaSetupCode] = useState('');
@@ -179,6 +218,49 @@ export default function SettingsPage() {
</p>
</section>
<section aria-label="Preferences" className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-6">
<h2 className="text-base font-medium">Preferences</h2>
<p className="mb-4 mt-1 text-sm text-[var(--r-sub)]">How the app looks and reads. Study projects always use the Berean Standard Bible.</p>
<PrefRow label="Home page" hint="Where the app opens, and where the Home buttons take you." synced>
<Segmented label="Home page" options={[['studies', 'My Studies'], ['reader', 'Bible Reader'], ['plans', 'Reading Plans']]} value={startPagePref} onChange={setStartPagePref} />
</PrefRow>
<PrefRow label="Theme" hint="Paper, sepia or dark." synced>
<Segmented label="Theme" options={Object.entries(READER_THEMES).map(([id, t]) => [id, t.label])} value={readerTheme} onChange={setReaderTheme} />
</PrefRow>
<PrefRow label="Default Bible" hint="What the Reader opens in." synced>
<select aria-label="Default Bible" value={readerTranslation} onChange={(e) => setReaderTranslation(e.target.value)} className={selectCls}>
{READER_TRANSLATIONS.map((t) => <option key={t.id} value={t.id}>{t.id} · {t.name}</option>)}
</select>
</PrefRow>
<PrefRow label="New reading plans" hint="The Bible a plan starts in. You can change it per plan." synced>
<select aria-label="New reading plans" value={planTranslationPref} onChange={(e) => setPlanTranslationPref(e.target.value)} className={selectCls}>
<option value="same">Same as my default Bible</option>
{READER_TRANSLATIONS.map((t) => <option key={t.id} value={t.id}>{t.id} · {t.name}</option>)}
</select>
</PrefRow>
<PrefRow label="Verse layout" hint="Verses run together like a page, or one to a line." synced>
<Segmented label="Verse layout" options={[['flow', 'Flowing'], ['verses', 'One per line']]} value={readerLayout} onChange={setReaderLayout} />
</PrefRow>
<PrefRow label="Commentary" hint="The one shown first in the Reader and the Study stage." synced>
<select aria-label="Commentary" value={readerCommentary} onChange={(e) => setReaderCommentary(e.target.value)} className={selectCls}>
{COMMENTARIES.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</PrefRow>
<PrefRow label="Text size" hint="Reader text.">
<Segmented label="Text size" options={FONT_SIZES.map(([l, n]) => [n, l])} value={readerFontSize} onChange={setReaderFontSize} />
</PrefRow>
<PrefRow label="Reading width" hint="One column, or two on a wide screen.">
<Segmented label="Reading width" options={[[false, 'Column'], [true, 'Two columns']]} value={readerWide} onChange={setReaderWide} />
</PrefRow>
<PrefRow label="Audio narrator" hint="For the Berean Standard Bible audio.">
<select aria-label="Audio narrator" value={audioNarrator} onChange={(e) => setAudioNarrator(e.target.value)} className={selectCls}>
<option value="david">David</option>
<option value="hays">Hays</option>
<option value="souer">Souer</option>
</select>
</PrefRow>
</section>
<section className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-6 space-y-4">
<div>
<h2 className="text-base font-medium text-[var(--r-text)]">Change password</h2>