Files
study-bible-project/src/components/study/StudioShell.jsx
T
nmemmertandClaude Sonnet 5.5 d13f55faf7 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>
2026-10-01 14:08:08 -04:00

71 lines
4.3 KiB
React

// Shared frame for every stage of a study project (Outline, Study, Script,
// Export): themed like the Reader, with a slim bar that carries the project title,
// the stage switcher, and save/sync status.
import { useApp } from '../../context/AppContext.js';
import { themeVars, READER_THEMES } from '../reader/readerThemes.js';
import { ChevronLeft } from '../reader/icons.jsx';
export const STAGES = [
{ id: 'setup', label: 'Chunks' },
{ id: 'study', label: 'Study' },
{ id: 'outline', label: 'Outline' },
{ id: 'script', label: 'Script' },
{ id: 'export', label: 'Export' },
];
const THEME_ORDER = ['paper', 'sepia', 'dark'];
export default function StudioShell({ stage, loading = false, fixedHeight = false, children }) {
const {
goHome, setCurrentPage, project, allChunks,
saveStatus, syncStatus, retrySync, syncConflict, keepMyVersion, useServerVersion,
readerTheme, setReaderTheme,
} = useApp();
const noChunks = allChunks.length === 0;
const cycleTheme = () => setReaderTheme(THEME_ORDER[(THEME_ORDER.indexOf(readerTheme) + 1) % THEME_ORDER.length]);
return (
<div className={`bg-[var(--r-bg)] font-sans text-[var(--r-text)] ${fixedHeight ? 'fixed inset-0 flex flex-col overflow-hidden' : 'min-h-dvh'}`} style={themeVars(readerTheme)}>
<div className="safe-top z-30 shrink-0 border-b border-[var(--r-line)] bg-[var(--r-bar)]">
<div className="mx-auto flex h-12 max-w-[1600px] items-center gap-3 px-3">
<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>
<div className="min-w-0 max-w-[16rem] truncate text-sm font-medium">{project?.title ?? ''}</div>
<nav aria-label="Study stages" className="mx-auto flex gap-0.5">
{STAGES.map((s) => {
const disabled = s.id !== 'setup' && (!project || noChunks);
return (
<button key={s.id} type="button" disabled={disabled} aria-current={stage === s.id ? 'step' : undefined}
title={disabled ? 'Add a chunk in Chunks first' : undefined}
onClick={() => setCurrentPage(s.id)}
className={`rounded-full px-3.5 py-1.5 text-xs ${stage === s.id ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'} disabled:opacity-40 disabled:hover:bg-transparent`}>
{s.label}
</button>
);
})}
</nav>
<div className="flex min-w-[7rem] items-center justify-end gap-2 text-xs text-[var(--r-sub)]" aria-live="polite">
{loading ? <span>Loading…</span>
: saveStatus ? <span className="text-[var(--r-accent)]">{saveStatus}</span>
: syncStatus === 'syncing' ? <span>Syncing…</span>
: syncStatus === 'synced' ? <span>Saved</span>
: syncStatus === 'error' ? (
<span className="text-[var(--r-danger,#b42318)]">Sync failed <button type="button" onClick={retrySync} className="underline">Retry</button></span>
) : null}
<button type="button" onClick={cycleTheme} className="rounded-lg px-2 py-1 hover:bg-black/5" title="Change theme">{READER_THEMES[readerTheme]?.label}</button>
</div>
</div>
</div>
{syncConflict && project && syncConflict.id === project.id && (
<div role="alert" className="flex shrink-0 flex-wrap items-center justify-center gap-x-4 gap-y-2 border-b border-[var(--r-line)] bg-[var(--r-hl)] px-4 py-2.5 text-sm">
<span>This study was changed on another device. Your edits here are saved on this device, but not yet synced.</span>
<span className="flex gap-2">
<button type="button" onClick={keepMyVersion} className="rounded-full bg-[var(--r-text)] px-3 py-1 text-xs font-medium text-[var(--r-bg)] hover:opacity-90">Keep this version</button>
<button type="button" onClick={useServerVersion} className="rounded-full border border-[var(--r-text)] px-3 py-1 text-xs font-medium hover:bg-black/5">Use the other version</button>
</span>
</div>
)}
{children}
</div>
);
}