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
@@ -1,3 +1,5 @@
|
||||
import { labelFor as outlineLabel } from './outlineLabels.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Study constants
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -187,6 +189,11 @@ export function buildExportHtml(project) {
|
||||
.greek th, .greek td { border: 1px solid #d1d5db; padding: 0.65rem; text-align: left; }
|
||||
.greek th { background: #f8fafc; }
|
||||
.definition { margin-top: 0.75rem; font-size: 0.95rem; line-height: 1.6; }
|
||||
.outline ul { list-style: none; margin: 0.25rem 0 0.25rem 1.5rem; padding: 0; }
|
||||
.outline > ul { margin-left: 0; }
|
||||
.outline li { margin: 0.3rem 0; line-height: 1.5; }
|
||||
.outline .lbl { display: inline-block; min-width: 2rem; font-weight: 700; }
|
||||
.outline .ref { color: #64748b; font-size: 0.9em; }
|
||||
.definition-block { margin-top: 1rem; padding: 1rem; border: 1px solid #e2e8f0; border-radius: 0.75rem; background: #f8fafc; }
|
||||
@media print {
|
||||
body { padding: 0; }
|
||||
@@ -197,6 +204,11 @@ export function buildExportHtml(project) {
|
||||
|
||||
const chapters = Array.isArray(project.chapters) ? project.chapters : [];
|
||||
|
||||
// The outline, if one was written: nested lists, labelled I. / A. / 1. like on the page
|
||||
const outlineHtml = (project.outline ?? []).length > 0
|
||||
? `<section class="outline"><h2 class="chapter-heading">Outline</h2>${outlineListHtml(project.outline, 0)}</section>`
|
||||
: '';
|
||||
|
||||
const chunksHtml = chapters.map((ch, chapterIndex) => {
|
||||
const chapterHeader = `<h2 class="chapter-heading">${escHtml(ch.book)} ${escHtml(ch.chapter)}</h2>`;
|
||||
const chunkSections = ch.chunks.map((chunk) => {
|
||||
@@ -284,12 +296,18 @@ export function buildExportHtml(project) {
|
||||
<div class="page">
|
||||
<h1>${escHtml(project.title)}</h1>
|
||||
<p>${escHtml(project.translation)}</p>
|
||||
${outlineHtml}
|
||||
${chunksHtml}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// <ul> of outline points, each with its label (I., A., 1. …) and passage
|
||||
function outlineListHtml(nodes, depth) {
|
||||
return `<ul>${nodes.map((n, i) => `<li><span class="lbl">${escHtml(outlineLabel(depth, i))}</span>${escHtml((n.text ?? '').trim() || '(untitled)')}${n.ref ? ` <span class="ref">(${escHtml(n.ref)})</span>` : ''}${n.children?.length ? outlineListHtml(n.children, depth + 1) : ''}</li>`).join('')}</ul>`;
|
||||
}
|
||||
|
||||
export function wordTableHtml(rows) {
|
||||
return `
|
||||
<table>
|
||||
@@ -341,7 +359,18 @@ export function renderVerseContent(content) {
|
||||
return content;
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
return content.map(renderVerseContent).join('');
|
||||
// A footnote marker ({ noteId }) often sits where the source had a space ("light,” [note] and there
|
||||
// was light"), which would run the words together. Put the space back when nothing else supplies one.
|
||||
let out = '';
|
||||
let afterNote = false;
|
||||
for (const part of content) {
|
||||
const piece = renderVerseContent(part);
|
||||
if (piece === '' && part && typeof part === 'object' && 'noteId' in part) { afterNote = true; continue; }
|
||||
if (afterNote && out && piece && !/\s$/.test(out) && /^[\p{L}\p{N}“‘"'(]/u.test(piece)) out += ' ';
|
||||
afterNote = false;
|
||||
out += piece;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (content && typeof content === 'object') {
|
||||
if (content.lineBreak) {
|
||||
|
||||
Reference in new issue
Block a user