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:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 14:08:08 -04:00
1 parent 1edbd364ea
commit d13f55faf7
65 files changed
+2947 -208

No files matched your search

+18 -6
View File
@@ -1,4 +1,4 @@
const SHELL = 'bible-shell-v1';
const SHELL = 'bible-shell-v2';
const API = 'bible-api-v1';
// Chapter text, interlinear cross-refs, and (BSB) chapter audio all come from
// these origins — cache them so the reader, reading plans, and audio all keep
@@ -27,8 +27,10 @@ self.addEventListener('fetch', (e) => {
const { request } = e;
const url = request.url;
// Never intercept API routes — let Express handle them
if (new URL(url).pathname.startsWith('/api/')) return;
// Never intercept this app's own API routes — let Express handle them. (Only same-origin ones: the
// Bible text at bible.helloao.org/api/... must pass through to the offline cache below.)
const { origin, pathname } = new URL(url);
if (origin === self.location.origin && pathname.startsWith('/api/')) return;
// External Bible API (text, cross-refs, audio): network-first, fall back to cache.
// Audio elements often issue Range requests, which come back as 206 Partial
@@ -37,7 +39,11 @@ self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(request)
.then(res => {
if (res.status === 200) caches.open(API).then(c => c.put(request, res.clone()));
// Clone *now*: once the page starts reading the body a later clone() throws and nothing is saved.
if (res.status === 200) {
const copy = res.clone();
e.waitUntil(caches.open(API).then(c => c.put(request, copy)).catch(() => {}));
}
return res;
})
.catch(() =>
@@ -56,7 +62,10 @@ self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(request)
.then(res => {
if (res.ok) caches.open(SHELL).then(c => c.put(request, res.clone()));
if (res.ok) {
const copy = res.clone();
e.waitUntil(caches.open(SHELL).then(c => c.put(request, copy)).catch(() => {}));
}
return res;
})
.catch(() =>
@@ -75,7 +84,10 @@ self.addEventListener('fetch', (e) => {
c.match(request).then(hit => {
if (hit) return hit;
return fetch(request).then(res => {
if (res.ok) c.put(request, res.clone());
if (res.ok) {
const copy = res.clone();
e.waitUntil(c.put(request, copy).catch(() => {}));
}
return res;
});
})