Redesign the study side: Outline, Study, Script and Export stages
Build and Push Docker Image / build-and-push (push) Successful in 4m15s
Build and Push Docker Image / build-and-push (push) Successful in 4m15s
Studies now move through a stage bar in the reader's themes. Outline splits chunks by clicking between verses (join merges notes) and suggests chunks from section headings. Study is three panes: outline, passage with an Observe / Interpret / Apply accordion, and commentary / word study / cross-refs / people & places for the chunk; original words are added by clicking them in the interlinear. Script drafts an outline from notes; Export holds files and the share link. Home, Import and the Pronunciation guide are restyled. Study projects are BSB only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1bc1c8ea4
commit
31abee0695
31 files changed
+1911
-2460
No files matched your search
@@ -0,0 +1,103 @@
|
||||
// Observe / Interpret / Apply as an accordion (one open at a time), plus a "More"
|
||||
// area for background notes and tags so the page stays quiet.
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const SECTIONS = [
|
||||
{ field: 'observation', label: 'Observe', hint: 'What does the text say?',
|
||||
placeholder: "What does the text actually say?\n• Who is speaking, and to whom?\n• What key words or phrases repeat?\n• What's the tone, structure, or literary style?" },
|
||||
{ field: 'interpretation', label: 'Interpret', hint: 'What does it mean?',
|
||||
placeholder: "What did this mean to its original audience?\n• What's the historical/cultural context?\n• How does it fit the surrounding argument?\n• What does it reveal about God's character?" },
|
||||
{ field: 'application', label: 'Apply', hint: 'What do I do with it?',
|
||||
placeholder: 'How should this shape your life today?\n• What attitude or action does this call for?\n• Is there a promise to trust or a warning to heed?\n• Who could you share this with?' },
|
||||
];
|
||||
|
||||
const areaCls = 'w-full resize-y rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3.5 py-3 font-serif text-[15px] leading-7 text-[var(--r-text)] placeholder:font-sans placeholder:text-[13px] placeholder:leading-5 placeholder:text-[var(--r-sub)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
||||
const words = (s) => (s ?? '').trim().split(/\s+/).filter(Boolean).length;
|
||||
|
||||
export default function NotesPane({ chunk, updateChunk, allTags }) {
|
||||
const [open, setOpen] = useState(() => {
|
||||
try { return localStorage.getItem('studyNotesOpen') || 'observation'; } catch { return 'observation'; }
|
||||
});
|
||||
const [more, setMore] = useState(false);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
useEffect(() => { setTagInput(''); }, [chunk.id]);
|
||||
useEffect(() => { try { localStorage.setItem('studyNotesOpen', open || ''); } catch { /* storage unavailable */ } }, [open]);
|
||||
|
||||
const tags = chunk.tags ?? [];
|
||||
const addTag = (tag) => {
|
||||
const t = tag.trim();
|
||||
if (!t || tags.some((x) => x.toLowerCase() === t.toLowerCase())) { setTagInput(''); return; }
|
||||
updateChunk(chunk.id, { tags: [...tags, t] });
|
||||
setTagInput('');
|
||||
};
|
||||
const suggestions = allTags.filter((t) => !tags.some((x) => x.toLowerCase() === t.toLowerCase()));
|
||||
|
||||
return (
|
||||
<section aria-label="Study notes" className="space-y-2">
|
||||
{SECTIONS.map(({ field, label, hint, placeholder }) => {
|
||||
const isOpen = open === field;
|
||||
const n = words(chunk[field]);
|
||||
return (
|
||||
<div key={field} className={`overflow-hidden rounded-2xl border ${isOpen ? 'border-[var(--r-accent)]' : 'border-[var(--r-line)]'}`}>
|
||||
<button type="button" onClick={() => setOpen(isOpen ? null : field)} aria-expanded={isOpen}
|
||||
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left">
|
||||
<span className="flex items-center gap-2.5">
|
||||
<span aria-hidden="true" className={`h-2.5 w-2.5 rounded-full border ${n ? 'border-[var(--r-accent)] bg-[var(--r-accent)]' : 'border-[var(--r-sub)]'}`} />
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
<span className="text-xs text-[var(--r-sub)]">{hint}</span>
|
||||
</span>
|
||||
<span className="text-xs text-[var(--r-sub)]">{n ? `${n} word${n === 1 ? '' : 's'}` : ''}</span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="px-4 pb-4">
|
||||
<textarea aria-label={label} value={chunk[field] ?? ''} rows={8} placeholder={placeholder}
|
||||
onChange={(e) => updateChunk(chunk.id, { [field]: e.target.value })} className={areaCls} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="rounded-2xl border border-[var(--r-line)]">
|
||||
<button type="button" onClick={() => setMore((m) => !m)} aria-expanded={more}
|
||||
className="flex w-full items-center justify-between px-4 py-3 text-left text-sm">
|
||||
<span><span className="font-medium">More</span> <span className="text-xs text-[var(--r-sub)]">background{tags.length ? ` · ${tags.length} tag${tags.length === 1 ? '' : 's'}` : ' and tags'}</span></span>
|
||||
<span className="text-xs text-[var(--r-sub)]">{more ? '▾' : '▸'}</span>
|
||||
</button>
|
||||
{more && (
|
||||
<div className="space-y-4 px-4 pb-4">
|
||||
<label className="block text-xs text-[var(--r-sub)]">
|
||||
Background / general notes
|
||||
<textarea value={chunk.generalNotes ?? ''} rows={5} onChange={(e) => updateChunk(chunk.id, { generalNotes: e.target.value })}
|
||||
placeholder="e.g. author, date, audience, historical context, key themes…" className={`mt-1 ${areaCls}`} />
|
||||
</label>
|
||||
<div>
|
||||
<p className="mb-1.5 text-xs text-[var(--r-sub)]">Tags</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{tags.map((tag) => (
|
||||
<span key={tag} className="inline-flex items-center gap-1 rounded-full bg-[var(--r-bar)] px-2.5 py-1 text-xs">
|
||||
{tag}
|
||||
<button type="button" aria-label={`Remove tag ${tag}`} onClick={() => updateChunk(chunk.id, { tags: tags.filter((t) => t !== tag) })} className="text-[var(--r-sub)] hover:text-red-600">×</button>
|
||||
</span>
|
||||
))}
|
||||
<input value={tagInput} onChange={(e) => setTagInput(e.target.value)} list="study-tag-suggestions"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addTag(tagInput); } }}
|
||||
placeholder="Add a tag and press Enter"
|
||||
className="min-w-[10rem] flex-1 rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2.5 py-1 text-xs text-[var(--r-text)] focus:outline-none" />
|
||||
</div>
|
||||
{suggestions.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1.5 text-xs">
|
||||
<span className="text-[var(--r-sub)]">Suggestions:</span>
|
||||
{suggestions.slice(0, 12).map((t) => (
|
||||
<button key={t} type="button" onClick={() => addTag(t)} className="rounded-full border border-[var(--r-line)] px-2.5 py-0.5 text-[var(--r-sub)] hover:bg-black/5">+ {t}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<datalist id="study-tag-suggestions">{allTags.map((t) => <option key={t} value={t} />)}</datalist>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user