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
@@ -3,12 +3,13 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useApp } from '../../context/AppContext.js';
|
||||
import { themeVars, READER_THEMES } from '../reader/readerThemes.js';
|
||||
import { ChevronLeft } from '../reader/icons.jsx';
|
||||
import { ChevronLeft, SearchIcon } from '../reader/icons.jsx';
|
||||
|
||||
// `short` is what a phone shows; the full label is still the button's accessible name.
|
||||
const NAV = [
|
||||
{ id: 'studies', label: 'Studies' },
|
||||
{ id: 'reader', label: 'Read Bible' },
|
||||
{ id: 'plans', label: 'Reading Plans' },
|
||||
{ id: 'studies', label: 'Studies', short: 'Studies' },
|
||||
{ id: 'reader', label: 'Read Bible', short: 'Bible' },
|
||||
{ id: 'plans', label: 'Reading Plans', short: 'Plans' },
|
||||
];
|
||||
|
||||
// Avatar button with a dropdown: who you are, the theme, Settings, Admin, Log out.
|
||||
@@ -61,25 +62,37 @@ function AccountMenu() {
|
||||
}
|
||||
|
||||
export default function PageShell({ title, onBack, actions, active = null, maxWidth = 'max-w-6xl', children }) {
|
||||
const { readerTheme, goHome, goStudies, openBibleReader, openReadingPlans } = useApp();
|
||||
const { readerTheme, goHome, goStudies, openBibleReader, openReadingPlans, openPalette } = useApp();
|
||||
const go = { studies: goStudies, reader: openBibleReader, plans: openReadingPlans };
|
||||
return (
|
||||
<div className="min-h-dvh bg-[var(--r-bg)] font-sans text-[var(--r-text)]" style={themeVars(readerTheme)}>
|
||||
<a href="#content" className="skip-link">Skip to content</a>
|
||||
<header className="safe-top sticky top-0 z-30 border-b border-[var(--r-line)] bg-[var(--r-bar)]">
|
||||
<div className={`mx-auto flex h-12 ${maxWidth} items-center gap-4 px-4`}>
|
||||
<div className={`mx-auto flex h-12 ${maxWidth} items-center gap-2 px-3 sm:gap-4 sm:px-4`}>
|
||||
<button type="button" onClick={goHome} className="flex shrink-0 items-center gap-2" aria-label="Bible Study home">
|
||||
<img src="/logo.svg" alt="" className="h-6 w-6" />
|
||||
<span className="hidden text-sm font-medium sm:inline">Bible Study</span>
|
||||
</button>
|
||||
<nav aria-label="Main" className="flex items-center gap-0.5">
|
||||
{NAV.map((n) => (
|
||||
<button key={n.id} type="button" onClick={go[n.id]} aria-current={active === n.id ? 'page' : undefined}
|
||||
className={`rounded-full px-3 py-1.5 text-sm ${active === n.id ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'}`}>{n.label}</button>
|
||||
<button key={n.id} type="button" onClick={go[n.id]} aria-current={active === n.id ? 'page' : undefined} aria-label={n.label}
|
||||
className={`whitespace-nowrap rounded-full px-2.5 py-1.5 text-sm sm:px-3 ${active === n.id ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'}`}>
|
||||
<span className="sm:hidden" aria-hidden="true">{n.short}</span><span className="hidden sm:inline" aria-hidden="true">{n.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div className="ml-auto"><AccountMenu /></div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{openPalette && (
|
||||
<button type="button" onClick={openPalette} aria-label="Search and go to"
|
||||
className="flex items-center gap-2 rounded-full border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-1 text-xs text-[var(--r-sub)] hover:bg-black/5">
|
||||
<SearchIcon /><span className="hidden sm:inline">Search</span><kbd className="hidden rounded border border-[var(--r-line)] px-1 text-[10px] md:inline">⌘K</kbd>
|
||||
</button>
|
||||
)}
|
||||
<AccountMenu />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div id="content" tabIndex={-1} className="focus:outline-none">
|
||||
<div className={`mx-auto flex ${maxWidth} flex-wrap items-center gap-3 px-4 pt-6`}>
|
||||
{onBack && (
|
||||
<button type="button" onClick={onBack} aria-label="Back" className="-ml-2 flex h-9 w-9 items-center justify-center rounded-lg text-[var(--r-sub)] hover:bg-black/5"><ChevronLeft /></button>
|
||||
@@ -88,6 +101,7 @@ export default function PageShell({ title, onBack, actions, active = null, maxWi
|
||||
{actions && <div className="ml-auto flex flex-wrap items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user