Add Outline stage, parallel reading, offline saves, backup, sync-conflict handling and more
Study - New Outline stage (hierarchical I./A./1. points, linked to chunks, in all exports); the chunk stage is now "Chunks" - Home cards: passages shown as ranges, tags capped with "+N more" Reader - Side-by-side translations and a Compare tab per verse - Typeface, line spacing, verse-number settings; text and column scale with the screen - Highlights/bookmarks sheet with named colours (synced); BSB footnote spacing fix - Cmd-K palette (passages, studies, notes, marks), keyboard shortcuts and "?" help Flow and safety - Toasts with Undo replace most confirm()/alert() - Back/Forward move between pages - Server refuses saves over a newer copy (409); keep-mine / use-other banner; opening a study no longer re-stamps it - Latest chapter request wins when flipping quickly Offline and data - Save books, plan days or a whole BSB/NET for offline - Service worker fixes: helloao /api/ URLs were skipped and responses cloned too late - Settings: download/restore a full backup Accessibility - Sheets trap focus and close on Escape, skip link, focus ring, reduced motion, theme contrast raised to 4.5:1 (with a test) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1edbd364ea
commit
d13f55faf7
65 files changed
+2947
-208
No files matched your search
@@ -5,8 +5,9 @@ import Sheet from './Sheet.jsx';
|
||||
import { CommentaryTab, PeoplePlacesTab } from '../StudyPanel.jsx';
|
||||
import { BookmarkIcon, CopyIcon } from '../../utils/bibleUtils.jsx';
|
||||
import { formatCrossRef, speakOriginalWord } from '../../utils/wordStudy.js';
|
||||
import { READER_TRANSLATIONS, fetchChapter } from '../../utils/bibleText.js';
|
||||
|
||||
const TABS = [['commentary', 'Commentary'], ['original', 'Original'], ['refs', 'Cross-refs'], ['people', 'People & places']];
|
||||
const TABS = [['commentary', 'Commentary'], ['compare', 'Compare'], ['original', 'Original'], ['refs', 'Cross-refs'], ['people', 'People & places']];
|
||||
|
||||
function OriginalTab({ words }) {
|
||||
if (!words) return <p className="text-sm text-[var(--r-sub)]">No original-language data for this verse.</p>;
|
||||
@@ -49,6 +50,42 @@ function RefsTab({ refs, loading, onJump }) {
|
||||
);
|
||||
}
|
||||
|
||||
// This verse in every translation, one under the other. Each Bible loads the chapter it is in
|
||||
// (BSB and NET come from the free source; the rest through the server) and shows this verse.
|
||||
function CompareTab({ book, chapter, verse, current }) {
|
||||
const [rows, setRows] = useState({}); // id -> { text } | { error } | undefined while loading
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setRows({});
|
||||
READER_TRANSLATIONS.forEach((t) => {
|
||||
fetchChapter(t.id, book, chapter, { signal: controller.signal })
|
||||
.then(({ verses }) => {
|
||||
const text = verses.find((v) => v.number === verse)?.text;
|
||||
setRows((r) => ({ ...r, [t.id]: text ? { text } : { error: 'No verse here in this translation.' } }));
|
||||
})
|
||||
.catch((err) => { if (!controller.signal.aborted) setRows((r) => ({ ...r, [t.id]: { error: err.message || 'Unavailable.' } })); });
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [book, chapter, verse]);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{READER_TRANSLATIONS.map((t) => {
|
||||
const row = rows[t.id];
|
||||
return (
|
||||
<div key={t.id}>
|
||||
<p className="mb-0.5 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">{t.id}{t.id === current ? ' · reading' : ''}</p>
|
||||
{!row ? <p className="text-sm text-[var(--r-sub)]">Loading…</p>
|
||||
: row.error ? <p className="text-sm text-[var(--r-sub)]">{row.error}</p>
|
||||
: <p className="font-serif text-[15px] leading-relaxed">{row.text}</p>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<p className="border-t border-[var(--r-line)] pt-2 text-[10px] leading-snug text-[var(--r-sub)]">Copyright notices for each translation appear at the foot of the chapter when you read it.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function VerseSheet({
|
||||
verse, book, bookName, chapter, translation, words, crossRefs, crossRefsLoading, onLoadCrossRefs,
|
||||
bookmarkColor, onToggleBookmark, onCycleColor, onJump, onClose,
|
||||
@@ -86,15 +123,16 @@ export default function VerseSheet({
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<div className="mb-3 flex gap-1.5 overflow-x-auto pb-1">
|
||||
<div role="tablist" aria-label="About this verse" className="mb-3 flex gap-1.5 overflow-x-auto pb-1">
|
||||
{TABS.map(([id, label]) => (
|
||||
<button key={id} type="button" onClick={() => setTab(id)}
|
||||
<button key={id} type="button" role="tab" aria-selected={tab === id} onClick={() => setTab(id)}
|
||||
className={`shrink-0 rounded-full border px-3 py-1 text-xs ${tab === id ? 'border-[var(--r-text)] bg-[var(--r-text)] text-[var(--r-bg)]' : 'border-[var(--r-line)] text-[var(--r-sub)] hover:bg-black/5'}`}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{tab === 'commentary' && <CommentaryTab book={book} chapter={chapter} verse={verse.number} />}
|
||||
{tab === 'compare' && <CompareTab book={book} chapter={chapter} verse={verse.number} current={translation} />}
|
||||
{tab === 'original' && <OriginalTab words={words} />}
|
||||
{tab === 'refs' && <RefsTab refs={crossRefs} loading={crossRefsLoading} onJump={onJump} />}
|
||||
{tab === 'people' && <PeoplePlacesTab book={book} chapter={chapter} verse={verse.number} />}
|
||||
|
||||
Reference in new issue
Block a user